Skip to content

Production plugin checklist ​

A plugin is production-ready when it is predictable under denied permissions, restarts, slow startup, multiple clients, host changes, and hostile input—not just when its happy path renders once.

Start with the contract ​

  • [ ] The folder name equals manifest name; version and api_version are deliberate.
  • [ ] permissions is the smallest set that covers actual SDK calls.
  • [ ] runtime_capabilities, user-facing capabilities, and host permissions are not mixed together.
  • [ ] Every setting has a useful label, bounds/enum where possible, and type: "secret" for credentials.
  • [ ] bun run check:docs passes in the platform repo when the SDK or schema was changed.

Use the manifest reference rather than copying a stale manifest from a plan or issue.

Make authorization backend-owned ​

The frontend is presentation and input collection. It is never an authority.

ts
plugin.handle("deleteThing", async (params, user) => {
  if (!(await plugin.permissions.check(user.id, "my-plugin.delete"))) {
    throw new SdkError("permission_denied", "You cannot delete this item");
  }
  // Validate params, load the row, verify its scope, then mutate.
});
  • [ ] Every privileged handler uses the authenticated user argument.
  • [ ] Object ownership/scope is checked after loading the target—not accepted from a client-supplied user or server id.
  • [ ] Inputs have type, length, range, and cardinality bounds.
  • [ ] Errors expose stable codes and safe messages; secrets and SQL details stay out of client responses and logs.

Design startup and shutdown ​

Register handlers synchronously before the first await. If caches or external state must be hydrated, set serve_ready_handshake: true, perform bounded startup work, and call plugin.serveReady() only after the plugin can answer.

  • [ ] Startup is idempotent and tolerates already-created registrations.
  • [ ] Long work is async/chunked so watchdog pongs are not starved.
  • [ ] Scheduled jobs are idempotent; a retry or overlap cannot duplicate a destructive effect.
  • [ ] Writes are committed in handler/job paths, not deferred to an exit hook.

Own data deliberately ​

  • [ ] SQLite migrations are append-only, ordered, and tested on both an empty database and an upgraded fixture.
  • [ ] Queries name columns instead of SELECT *.
  • [ ] Cross-plugin reads use public_schema + plugin.data; there are no cross-plugin filesystem or write dependencies.
  • [ ] Files use plugin.files and signed URLs; the database stores the stable filename/metadata, not a tunnel hostname.
  • [ ] Secrets use manifest settings and never enter broadcasts, public schema, or console output.

Choose the right realtime primitive ​

NeedPrimitive
Request/response initiated by a panelfrontend sdk.request → backend plugin.handle
Durable plugin-to-plugin signal with ackplugin.events.publish/subscribe
Best-effort live update to connected clientsplugin.broadcast → frontend sdk.on
Live sidebar mutationplugin.sidebar.emitDelta
Ephemeral viewers/typing membershipplugin.presence
Durable user notificationbackend plugin.notifications
Current-device feedbackfrontend sdk.platform.toast / local notification

After a best-effort delta, keep a full read/snapshot path so clients can recover after reconnect or a missed update.

Be host- and reachability-independent ​

  • [ ] No Docker/WSL/systemd commands or hard-coded host data paths.
  • [ ] Reverse-proxy upstreams are settings, not compiled hostnames.
  • [ ] Documentation explains native and Docker upstream differences; host.docker.internal is never presented as universal.
  • [ ] Transport-only features such as plugin.net and dedicated mount hostnames have a useful unavailable state.
  • [ ] The plugin does not confuse server visibility with network reach.

See Platform & hosting model.

Make the UI belong in UnCorded ​

  • [ ] Load /sdk/plugin-frontend.js from the runtime.
  • [ ] Apply --uncorded-* theme tokens from first paint; verify light, dark, accent, and reduced-width layouts.
  • [ ] Use shell-owned user cards, file preview/download, modal/rail surfaces, confirm/alert, and toasts instead of recreating inconsistent platform UI.
  • [ ] Unsubscribe event/broadcast/navigation listeners during teardown.
  • [ ] Empty, loading, denied, offline, and recoverable-error states tell the user what they can do next.

Package and prove it ​

  • [ ] The installed artifact contains built frontend assets and every backend dependency; installation never relies on an internet-time bun install.
  • [ ] Unit tests cover validation and permission branches.
  • [ ] Integration tests cover handler → data mutation → event/broadcast → read recovery.
  • [ ] The plugin has been restarted repeatedly and exercised with two clients.
  • [ ] Proxy/realtime plugins have an hours-long soak path and an upstream restart test.
  • [ ] Logs contain plugin slug/request context and no sensitive values.

Before publishing, compare the small Noteboard example for structure and the text-channels walkthrough for production lifecycle, permissions, migrations, presence, files, and cleanup.