Features Apograph CMS on GitHub

Plugins

Every plugin a deployment can register, and the two arrays that decide what your CMS is.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

An Apograph deployment is its plugin list. Two arrays decide what the API serves and what the admin renders:

// apps/server/src/plugins.ts
export function buildPlugins(config: ApographConfig): ServerPlugin[] {
    return [DatabasePlugin({ connectionString: config.database.url }), /* … */];
}
// apps/admin/src/plugins.ts
export function buildPlugins(): AdminPlugin[] {
    return [IdentityPlugin(), ShellPlugin(), /* … */];
}

Adding a capability is installing a package and adding a line. Removing one is deleting a line — plugins that extend each other do so through optional ports, so removing a plugin degrades the feature rather than failing the boot.

The catalogue

Each page below answers the same four questions: what the plugin contributes, what to install, how to register it, and what it reads from the environment.

PluginServerAdminOwns tables
Databasedatabaseyes
Identityidentity-serveridentity-adminyes
Workspacesworkspaces-serverworkspaces-adminyes
Activityactivity-serveractivity-adminyes
Usersusers-serverusers-adminno
Contentcontent-servercontent-adminyes
Saved viewscontent-servercontent-adminyes
GraphQLcontent-graphqlno
Mediamedia-servermedia-adminyes
Localizationi18n-serveri18n-adminyes
Transfertransfer-servertransfer-adminno
Alarmsalarms-serveralarms-adminyes
Segmentssegments-serversegments-adminyes
Protectionprotection-serverprotection-adminyes
Webhookswebhooks-serverwebhooks-adminyes
Copilotcopilot-servercopilot-adminyes
MCPmcp-serverno
Shellshell-adminno
Rich textwysiwyg-adminno
Insightsinsights-adminno
API tokensapi-tokens-adminno

Every package is published to npm under the @apograph scope, in lockstep — one version number across the whole set.

Saved views is the one row that is not its own package: it is a second ServerPlugin entry exported from @apograph/content-server, because a plugin carries one migration descriptor and content’s already points at the host’s generated tables.

Three plugins take a pluggable backend

Media, the copilot and identity each name a port rather than a vendor, and the backend is an argument to the plugin — constructed at the composition root, where the import that decides which one you are using is visible on one line.

PluginPortShipped adapters
MediaStorageProviderlocal, S3, Azure, GCS, Vercel, memory
CopilotModelProviderClaude, OpenAI-wire
IdentitySsoProviderOIDC, GitHub, SAML, scripted

Each has a page on writing one of your own — storage, model, SSO — because all three ports come with a conformance kit, and an adapter that has not run it is an adapter nobody has checked.

One pattern holds across all three: a capability is a claim the plugin checks, not a hint — declaring one a backend cannot honour fails the boot rather than the request.

What does not hold any more is a scripted adapter behind every port. Media ships an in-memory store and identity ships a scripted provider, both published; the copilot’s scripted backend is a private test fixture and is registered nowhere, so a clone with no key has no copilot rather than a canned one.

Upgrade them together

@apograph/* dependencies are pinned exactly, with no caret, and a generated app’s README says to upgrade them as a set.

A partial upgrade can leave two copies of a shared package in node_modules — two React context instances, and an admin whose sidebar silently stops talking to its provider. Nothing errors.

The order is migration order

Server plugins are registered in the order their migrations must run. There is no transaction spanning plugins, so a plugin whose tables reference another’s must come after it.

WorkspacesPlugin follows IdentityPlugin because its memberships table references identity’s users. Put it first and a fresh apograph migrate fails with relation "users" does not exist — while an already-migrated database migrates perfectly happily, so the mistake ships and bites the next clean install.

Add new plugins at the end unless you have a reason not to.

Order is not dependency injection

Every plugin module is global and every onPluginInit runs before the Nest app is created, so no provider can be constructed before the database connection is open. The order decides migrations and, in the admin, the order slot contributions are collected — nothing else.

Two kinds of configuration

A plugin takes at most two things, and the split is deliberate.

Config is the typed view of the environment, assembled in apograph.config.ts — the one file in the app that reads process.env. It is data: strings, numbers, booleans.

Registrations are constructed objects, passed as a second argument. A storage backend, a model provider, an identity provider. An adapter instance is not an environment value, so it does not live in the config file; it is constructed at the composition root, in plugins.ts, where the import that decides which backend you are using is visible on one line.

MediaServerPlugin({
    provider: createS3StorageProvider(config.plugins.media.storage),
    config: config.plugins.media
});

See configuration for the complete environment table, and writing a plugin for the two contracts a plugin of your own implements.