Building plugins
Build an optional extension or integration on top of the Work Order Systems API. Plugins are separate services or UIs that call the public API; they do not change the database schema.
New to plugins? Start with the step-by-step guide: How to build a plugin.
What is a plugin?
- Module: First-party core capability that owns schema and migrations.
- Plugin: Optional behavior or UI on top of existing modules. Uses the public API (views for reads, RPCs for writes). No schema changes.
- Integration: A plugin that connects to external systems (e.g. ticketing, messaging, CMMS, BI tools).
Your plugin runs as its own app or service (serverless function, worker, backend). Use server-side Supabase credentials for catalog and admin operations, and end-user sessions for tenant-scoped actions (e.g. from your frontend or admin app). All reads and writes go through the public API (views + RPCs).
Plugin lifecycle
- Define your plugin: key, name, description, and whether it's an integration.
- Register it in the catalog: call
rpc_register_pluginfrom a trusted backend (never from the browser or with the anon key). - Expose UI: let tenant admins install and configure the plugin (e.g. with
client.pluginsfrom their browser or your admin app). - Run plugin logic: in your service, use the public views and RPCs to read and write data.
Register plugin in the catalog
Registration is backend-only. Use a Supabase client with server-side credentials. Don't use the @workorder-systems/sdk client or the anon key from the browser.
Registration must happen from a secure backend. Don't use the browser SDK or anon key.
Register or update plugin in catalog (backend)
import { createClient } from '@supabase/supabase-js'
// Use your backend Supabase credentials (server-side only; never expose to the client).
// The key must be allowed to call rpc_register_plugin.
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_BACKEND_KEY!
)
// idempotent: insert or update plugin in catalog
const { data, error } = await supabase.rpc('rpc_register_plugin', {
p_key: 'my-integration',
p_name: 'My Integration',
p_description: 'Sync work orders with an external system',
p_is_integration: true,
p_is_active: true,
})
if (error) throw error
const pluginId = data as string
Install plugin for a tenant
Tenant admins install plugins from your frontend or admin app. Use the public SDK:
Install plugin for tenant with client.plugins.install(params)
// In your admin UI after sign-in and setTenant(tenantId)
const installationId = await client.plugins.install({
tenantId,
pluginKey: 'my-integration',
secretRef: 'vault-ref-123', // optional opaque reference to secret storage
config: {
apiEndpoint: 'https://api.example.com',
},
})
List and update installations with client.plugins.listInstallations() and client.plugins.updateInstallation(params). See Plugins.
Outbound webhooks (audit-driven)
The database enqueues outbound deliveries when rows appear in audit.entity_changes, but only for installations that have explicit subscription allowlists (int.plugin_webhook_subscriptions via rpc_upsert_plugin_webhook_subscription). This keeps noise low: without subscriptions, nothing is sent.
- Vault: Create a secret in Supabase Vault with a stable
name(for examplemy_plugin_hmac_prod). The plaintext is your shared signing secret. - Install: Set
secretRefon the installation to that same name (not the secret value). Setconfig.webhook_urlto your HTTPS endpoint. Optional:config.timeout_ms,config.headers(non-secret only). - Subscribe: Upsert a subscription for each
(table_schema, table_name)andoperations(INSERT/UPDATE/DELETE). ForUPDATE, you can passchanged_fields_allowlistso only relevant column changes fire events. - Payload: Subscribers receive JSON with
v,tenant_id,installation_id,plugin_key,audit(id, schema, table, record_id, operation, changed_fields), andsnapshot(either metadata-only or fullold_data/new_datawheninclude_payloadis true). HMAC-SHA256 (hex) ofpayload::text(UTF-8) is sent as headerX-Plugin-Signaturewhen a Vault secret resolves. - Delivery:
pg_netposts asynchronously;rpc_process_plugin_deliveries(scheduled bypg_cronevery minute) collects responses, retries with exponential backoff, and marksdeadafter repeated failures. Treat delivery as at-least-once; dedupe usingaudit.idandX-Plugin-Delivery-Id.
Inbound webhooks (HMAC)
Partners can call rpc_plugin_ingest_webhook with the anon or authenticated role (no end-user session required). Arguments: p_plugin_key, p_installation_id, p_payload (jsonb), p_signature (hex). The signature must match HMAC-SHA256 over p_payload::text using the same Vault secret referenced by secret_ref. Rate limiting is applied using the installation’s installed_by user when present.
Allowlisted actions today include noop and work_order.create (with data.title and optional catalog-valid fields). Extend with care—never execute arbitrary SQL from webhook bodies.
Use the public API from your plugin
Treat the Work Order Systems API as your backend:
- Reads: Query public views like
v_work_orders,v_assets,v_work_order_attachments,v_pm_*,v_dashboard_*, and so on. - Writes: Call public RPCs like
rpc_create_work_order,rpc_log_work_order_time,rpc_install_plugin,rpc_update_plugin_installation,rpc_upsert_plugin_webhook_subscription,rpc_plugin_ingest_webhook,rpc_create_pm_schedule, and others.
Use @supabase/supabase-js in your backend with server-side credentials for tenant-admin or system operations. Use end-user JWTs when acting on behalf of a specific user.
Public contract: Views use the v_* prefix for reads; RPCs use the rpc_* prefix for writes and actions. For full reference, see the resource pages in this docs site (Work orders, Assets, PM, Dashboard, Plugins, Authorization, etc.).
Next steps
- How to build a plugin: ordered checklist from catalog to first delivery.
- Local webhook receiver sample app:
plugins/example(pnpm --filter work-order-systems-example dev). See its README forhost.docker.internaland HMAC env vars. - Plugins: list and manage plugins
- Work orders: work order API reference
- Authentication: sign in with Supabase Auth