How the Plugin Loader Discovers & Validates Plugins
Your plugin isn't wired in by hand — the plugin loader discovers it at API startup, validates it, and mounts it. Knowing what the loader does (and checks) explains both how a plugin "just works" once its files are in place, and why a subtle mistake makes a plugin silently vanish.
The startup sequence
The order is strict, and it matters:
- The API connects to Postgres.
- Core migrations have already run (at deploy time, before the process starts).
- Plugin migrations have already run (after core migrations, still before the process starts).
- The loader runs inside the API process — discovering, validating, seeding, mounting.
Because migrations run before the loader, by the time the loader checks "does this table exist?", it does. (In local development, you run migrations manually before npm run dev.)
What the loader does per plugin
At startup, for each plugins/*/manifest.ts it finds, the loader:
- reads and validates the manifest;
- upserts the
entityrow from the manifest's identity; - checks
entity.is_enabled— iffalse, the plugin is skipped (no routes, no statuses); to disable a plugin you set this directly in the database; - seeds the workspace statuses into
workspace_statuswith the entity's id; - mounts the routes from
createRouter()at/api{apiRoutePrefix}; - registers the extension points in memory so the core can call them.
What it validates
The checks are worth knowing, because failing any one skips your plugin:
- the manifest shape — all required fields present and correctly typed;
- entity-slug uniqueness across plugins;
- workspace-status slug uniqueness within your plugin (cross-plugin collisions are fine — two plugins may both have
new); - the declared tables exist (
entityObjectTable,joinTable); - the join table has the required columns (
{entity_slug}_id,workspace_id,is_deleted,is_archived); - the initial status is in the declared list;
- the route prefix doesn't collide with a core route or another plugin.
Fail-safe by design
If validation fails, your plugin isn't loaded — its routes aren't mounted, its statuses aren't seeded — and the core logs a clear error identifying the failed check. Crucially, the rest of the platform still starts: one broken plugin never brings down the system or the other plugins. On success, the entity is enabled, statuses are seeded, routes are mounted, and your extension points are live.
This is why a mistyped slug or a missing join-table column shows up as "my plugin's routes 404" rather than a crash — start with the troubleshooting guide, and check the startup logs for the validation error.