Filestash plugin development

  1. General Idea for Plugins
  2. Plugin Discovery
  3. Compiled Plugin
    1. Structure of a Compiled Plugin
    2. Available Interfaces
  4. Runtime plugins
    1. Structure of a Runtime Plugin
    2. Available Interfaces
  5. Managing Plugin Configuration

General Idea for Plugin

You may have seen this diagram before:

If you wonder how is this possible to provide support for all those systems, the answer is one word: “polymorphism”. All the bubbles on the left side represent storage plugins. Each one is an implementation of the IBackend interface. The bubbles on the right represent plugins that implement another interface (called IAuthentication). Filestash itself is simply the engine that runs everything and connects all the pieces together.

The actual scope of plugins is much wider than what this diagram suggests. This page is intended as a reference for plugin developers who want to customise or extend Filestash. Once you look under the hood, you will find that Filestash provides two very different kinds of plugins:

This guide explains how both kind of plugins work, what they can do, and how to create your own extensions for Filestash.

Plugin Discovery

How do you know which plugins are active in your Filestash instance? You can check this from the /about page:

To add, remove, or change the plugins you are using, the procedure depends on the plugin type:

Compiled Plugin

Compiled plugins are written in Go and must be built directly into the Filestash binary [*]. We have developed many of them, which you can browse on the plugin documentation page. If you want a starting point for exploring the code, you can look through some examples in the repository.

There are essentially two kinds of compiled plugins:

  1. Plugins that implement an interface to customise or extend a part of Filestash. These hook into the core engine through one of the available interfaces and handling one particular aspect of Filestash which are all details in the available interfaces section
  2. Plugins that could live as standalone services, but are shipped alongside Filestash for convenience. A good example is plg_gateway_sftp, which exposes your Filestash instance as its own SFTP server. It may as well live in a separate process as it only rely on the public API

[*]: There is one special case exception. The plg_dlopen plugin can load .so files dynamically to provide a form of runtime extension. This approach is not beginner friendly and requires compiling and deploying the core binary and all plugins together as a single unit. It is expected to be deprecated once runtime plugins gain support for backend functionalities.

Structure of a Compiled Plugin

The scaffold for a compiled plugin always follows the same pattern. You register your implementation of a core interface using Hooks.Register:

package plg_interface_example

import (
	. "github.com/mickael-kerjean/filestash/server/common"
)

func init() {
	Hooks.Register.XXX(Impl{})
}

type Impl struct {}

func (this Impl) ...

If you prefer to start from a minimal working example, there are documented sample plugins you can copy from. Two good ones are: plg_search_example and plg_authorisation_example

Available Interfaces

Compiled plugins hook into the Filestash engine by implementing one or more interfaces. The full list of hooks is documented here. Below is a practical overview of what you can find in there:


We have already covered the most used plugins but there are some other niche one like:

Runtime plugins

Structure of a Runtime Plugin

Runtime plugins are packaged as ZIP files and placed inside the plugins/ directory. Each plugin must contain a manifest.json file, which declares the modules that make up your plugin. A typical manifest looks like this:

{
    "author": "Filestash Pty Ltd",
    "version": "v0.0",
    "modules": [
        {
            "type": "css",
            "entrypoint": "index.css"
        },
        {
            "type": "patch",
            "entrypoint": "index.diff"
        },
        {
            "type": "favicon",
            "entrypoint": "favicon.png"
        }
    ]
}

The modules array defines the functionalities your plugin provides. Each entry must include:

  1. type: the module type (eg: css, patch, favicon, xdg-open)
  2. entrypoint: the path to the file implementing that module, which must exist inside the ZIP archive

Filestash reads the manifest at startup and automatically loads the modules your plugin declares.

Common Error : Do not unzip anything inside the plugins/ directory. Keep the ZIP file in its original form as a .zip file, Filestash loads modules directly from the archive.

Available Interfaces

Runtime plugins currently focus on frontend customisation. Over time, their capabilities will expand to match what compiled plugins can do, but as of today the following module types are supported:

xdg-open plugins in depth

xdg-open plugins is to open specific file types using your own viewer / application. There are many examples of these plugins in the repository. A typical manifest looks like this:

{
    "author": "Filestash Pty Ltd",
    "version": "v0.0",
    "modules": [
        {
            "type": "xdg-open",
            "entrypoint": "/loader_swf.js",
            "mime": "application/x-shockwave-flash",
            "application": "skeleton"
        },
        {
            "type": "xdg-open",
            "entrypoint": "/loader_psd.js",
            "mime": "image/vnd.adobe.photoshop",
            "application": "image"
        },
        ...

Each xdg-open module includes the standard type and entrypoint fields used by all runtime plugin types, plus two additional fields:

  1. mime: which mime type this loader handles
  2. application: which Filestash application shell should host the viewer

In the example above:

  1. flash / swf files are handled by loader_swf.js using the skeleton application. Using skeleton means you start from a blank canvas. Filestash only expects your loader to export a default function from which you build the entire UI yourself

  2. psd files are handled by loader_psd.js using the built-in image application. Using image gives you a full image viewer UX for free with zoom handling with mouse or pinch, keyboard navigation, layout and more. Your loader only needs to implement the IImage interface and convert the PSD into something the browser can render natively

Except skeleton, the other application shells define small interfaces you can implement to generate pre made viewer apps that you can then customise. Filestash provides several shells depending on the content you want to support:

  1. table: for tabular formats. You can see it in action with: parquet viewer, and sqlite viewer (which even lets you query the db). The interface to implement is available here

  2. map: for geographic formats. Example: the shp viewer and the interface to implement

  3. 3d: for Three.js based 3d viewers. Example: this package adds support for many 3d formats. The interface to implement is available here

  4. image: for image based formats. Example: the psd viewer. The interface to implement is available here

  5. skeleton: for building your own viewer from scratch. Example: this docx viewer

Patch plugins in depth

Patch plugins are your go-to method for updating JavaScript so the software looks and behaves exactly the way your use case requires. With a patch plugin, you ship a git style diff of the frontend code; Filestash will execute your patch dynamically so your changes get reflected in the js code. Because patches operate on the full frontend assets, the surface area is effectively 100% of the frontend code, you can change anything. The possibilities are limited only by your own creativity.

A couple real exampless of what you can do with them:

Note on patch plugins: Patch plugins were one of the primary expected benefits after the completion of the 2025 frontend rewrite, when we moved from React to vanilla JS. Under React, supporting frontend plugins would have required exposing a huge number of extension hooks. But no matter how many hooks you expose, you never reach 100% coverage, someone will always need something impossible within the constraints of the API. That’s exactly what happened with Cursor: it could have been “just” a VS Code plugin if the VS Code plugin surface area had been sufficient, but it wasn’t, which forced them to fork the entire editor. Meanwhile, every extra hook adds overhead for everyone, and this become a 1% of users needing something kind of problem that lead straight onto the classic JIRA problem. So instead of inflating the plugin API, we support git-style patches on the frontend. In a non-Lisp world, this is the only realistic way to give plugin authors complete freedom without turning the core product into an unmaintainable heap. Patch plugins are the embodiment of that philosophy: total flexibility, zero bloat.

Managing Plugin Configuration

By default, Filestash stores its entire configuration in a single file: config.json. The admin console is essentially a friendly UI wrapper around this file. It reads and writes configuration entries so administrators don’t have to manually edit such JSON directly.

Plugins are allowed to extend this configuration. This allows you to expose settings in the admin console so users can enable, disable, or customise the behaviour of your plugin. A simple example of this pattern can be seen in plg_handler_site, which defines configuration fields controlling whether shared links should be exposed as static websites with various options to enable autoindex and cors.

When a plugin needs configuration, the standard approach is to create a config.go file inside your plugin package and register configuration fields during the Onload phase. The structure typically looks like this:

package plg_xxx_xxx

import (
	. "github.com/mickael-kerjean/filestash/server/common"
)

func init() {
	Hooks.Register.Onload(func() {
		PluginEnable()
        // ... add more config options
	})
}

var PluginEnable = func() bool {
	return Config.Get("features.foobar.enable").Schema(func(f *FormElement) *FormElement {
		if f == nil {
			f = &FormElement{}
		}
		f.Name = "enable"
		f.Type = "enable"
		f.Target = []string{}
		f.Description = "Enable/Disable the foobar feature"
		f.Default = false
		return f
	}).Bool()
}

// ... add more config options

This pattern ensures your configuration fields are registered early during startup, appear automatically in the admin console, and are cleanly accessible from your plugin code.