# Build and install an Inkling plugin Inkling already includes a plugin system. Plugins can add content types, settings, admin panels, routes, database migrations, and hooks. Bundled plugins ship with Inkling; an operator enables them under **Advanced setup → Plugins**. Their screens appear under **More tools**. ## Define a plugin Import the public API from `inkling/plugins`. It exports `definePlugin`, the `Plugin`, `PluginContext`, setting and panel types, and route guards. Install Inkling and Atlas as dependencies of the project that contains your plugin. Use the Atlas version pinned by your Inkling release. ```ts import { get, json, pipeline } from "atlas/server" import { can, definePlugin, requireAuth, requireCan } from "inkling/plugins" export default definePlugin({ name: "hello", version: "1.0.0", label: "Hello", settings: [{ key: "greeting", label: "Greeting", type: "text", default: "Hello" }], panels: [{ id: "settings", label: "Settings", kind: "settings" }], routes: ctx => { const read = pipeline(requireAuth(ctx.db), requireCan(can.readContent, "read content")) return [ get("/greet", read(async c => json(c, 200, { message: await ctx.getSetting("greeting", "Hello") }), )), ] }, }) ``` The name must contain lowercase letters and digits, start with a letter, and match its directory. The default export or a named `plugin` export is loaded. The example answers at `/ext/hello/greet`, requires a signed-in account with content-read permission, and offers an editable greeting in its Settings panel. Plugin routes are **not automatically authenticated**. Apply the guards for the audience: `requireAuth` plus `requireCan` for staff, or `requireApiKey` for a published-content integration. `auth(c)` reads the identity after authentication; `can` contains Inkling's role predicates. Public routes need their own input validation and rate limiting. ## Install it In a standalone Inkling checkout, put the file at `plugins/hello/index.ts`. A plugin installed from another package can use a wrapper there: ```ts export { default } from "your-plugin-package" ``` For an embedded site, create a plugin directory in the **host project** and pass its absolute path: ```ts import { createInkling } from "inkling" const inkling = await createInkling({ adminBase: "/admin", pluginDir: `${import.meta.dir}/plugins`, }) ``` The directory contains one subdirectory per plugin. `pluginDir` replaces the bundled directory; it does not merge with it. Keep any plugins the site still needs in the selected directory, including their migrations. Without an explicit directory, Inkling loads the plugins shipped inside its own package. Relative `pluginDir` and `PLUGIN_DIR` values resolve against the Inkling package, not the host project's working directory. After adding a file to a running install, restart it or call `POST /api/plugins/reload` with an account allowed to manage plugins. Then open **Advanced setup → Plugins** and press **Enable**. Enabling and disabling a known plugin take effect without restarting. Changes to already imported code need a process restart; rescanning is not a code hot-reload mechanism. This is a trusted server-code extension system. There is no upload-from-browser installer or marketplace. The operator installs the code; site managers switch its features on and off. ## Settings and screens Settings are scoped to the plugin. Use `ctx.getSetting`, `ctx.setSetting`, and `ctx.allSettings`. Declare credentials as `type: "secret"`: Inkling seals them at rest, masks them in API responses, and gives the plugin plaintext. A blank secret field preserves the current value; `null` explicitly removes it. Panels are declarative, using `settings`, `collection`, `table`, `stats`, `connections`, or `guide`. A plugin does not inject React into the admin bundle. Use a `guide` for setup steps and `connections` for account authorization. All panel endpoints still need appropriate authentication guards. ## Hooks, migrations, and lifecycle - `register(ctx)` attaches notification handlers with `ctx.on` or transformations with `ctx.filter`. Notification failures are logged; failing filters preserve the input. The registry clears plugin hooks before registering them again. - `requires` lists plugins that must be enabled first. Disable is refused while an enabled plugin still depends on this one. - `migrations//up.sql` and `down.sql` live inside the scanned plugin directory. Their migration names are scoped as `plugin:/`. - `install(ctx)` runs when enabling a version that differs from the recorded installed version, after migrations and declared content are provisioned. - Disable stops routes and hooks and hides plugin panels while retaining data. It does not delete the plugin's content types or unpublish existing content. - Uninstall is destructive: it invokes `uninstall`, rolls back plugin migrations, and removes owned content types, taxonomies, settings, and the registry row. It requires a disabled plugin and an explicit name confirmation in the API. File uploads currently use Inkling's local-disk or S3-compatible storage driver. Plugins can observe `media.afterUpload` and `media.afterDelete`; these are notifications, not storage replacement hooks. No Stohr integration ships in this release. See the [plugin tutorial](https://wess.io/inkling/tutorials/#plugin), [architecture](https://raw.githubusercontent.com/wess/inkling/main/docs/ARCHITECTURE.md), and [bundled examples](https://github.com/wess/inkling/tree/main/plugins). The Ecommerce plugin (`commerce`) provides the existing catalog plus editorial shop pages. The optional `square` plugin connects a merchant account and supplies live catalog data and hosted checkout. It requires `commerce`, remains off until enabled, and starts with checkout paused. See [Ecommerce and Square](commerce.md) for setup, storefront routes, and supported product types.