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.

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

  1. Define your plugin: key, name, description, and whether it's an integration.
  2. Register it in the catalog: call rpc_register_plugin from a trusted backend (never from the browser or with the anon key).
  3. Expose UI: let tenant admins install and configure the plugin (e.g. with client.plugins from their browser or your admin app).
  4. 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.

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.

  1. Vault: Create a secret in Supabase Vault with a stable name (for example my_plugin_hmac_prod). The plaintext is your shared signing secret.
  2. Install: Set secretRef on the installation to that same name (not the secret value). Set config.webhook_url to your HTTPS endpoint. Optional: config.timeout_ms, config.headers (non-secret only).
  3. Subscribe: Upsert a subscription for each (table_schema, table_name) and operations (INSERT / UPDATE / DELETE). For UPDATE, you can pass changed_fields_allowlist so only relevant column changes fire events.
  4. Payload: Subscribers receive JSON with v, tenant_id, installation_id, plugin_key, audit (id, schema, table, record_id, operation, changed_fields), and snapshot (either metadata-only or full old_data/new_data when include_payload is true). HMAC-SHA256 (hex) of payload::text (UTF-8) is sent as header X-Plugin-Signature when a Vault secret resolves.
  5. Delivery: pg_net posts asynchronously; rpc_process_plugin_deliveries (scheduled by pg_cron every minute) collects responses, retries with exponential backoff, and marks dead after repeated failures. Treat delivery as at-least-once; dedupe using audit.id and X-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 for host.docker.internal and HMAC env vars.
  • Plugins: list and manage plugins
  • Work orders: work order API reference
  • Authentication: sign in with Supabase Auth

Was this page helpful?