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.
| Plugin | Server | Admin | Owns tables |
|---|---|---|---|
| Database | database | — | yes |
| Identity | identity-server | identity-admin | yes |
| Workspaces | workspaces-server | workspaces-admin | yes |
| Activity | activity-server | activity-admin | yes |
| Users | users-server | users-admin | no |
| Content | content-server | content-admin | yes |
| Saved views | content-server | content-admin | yes |
| GraphQL | content-graphql | — | no |
| Media | media-server | media-admin | yes |
| Localization | i18n-server | i18n-admin | yes |
| Transfer | transfer-server | transfer-admin | no |
| Alarms | alarms-server | alarms-admin | yes |
| Segments | segments-server | segments-admin | yes |
| Protection | protection-server | protection-admin | yes |
| Webhooks | webhooks-server | webhooks-admin | yes |
| Copilot | copilot-server | copilot-admin | yes |
| MCP | mcp-server | — | no |
| Shell | — | shell-admin | no |
| Rich text | — | wysiwyg-admin | no |
| Insights | — | insights-admin | no |
| API tokens | — | api-tokens-admin | no |
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.
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.