Appearance
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;versionandapi_versionare deliberate. - [ ]
permissionsis the smallest set that covers actual SDK calls. - [ ]
runtime_capabilities, user-facingcapabilities, 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:docspasses 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
userargument. - [ ] 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.filesand 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
| Need | Primitive |
|---|---|
| Request/response initiated by a panel | frontend sdk.request → backend plugin.handle |
| Durable plugin-to-plugin signal with ack | plugin.events.publish/subscribe |
| Best-effort live update to connected clients | plugin.broadcast → frontend sdk.on |
| Live sidebar mutation | plugin.sidebar.emitDelta |
| Ephemeral viewers/typing membership | plugin.presence |
| Durable user notification | backend plugin.notifications |
| Current-device feedback | frontend 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.internalis never presented as universal. - [ ] Transport-only features such as
plugin.netand dedicated mount hostnames have a useful unavailable state. - [ ] The plugin does not confuse server
visibilitywith network reach.
Make the UI belong in UnCorded
- [ ] Load
/sdk/plugin-frontend.jsfrom 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.