Skip to main content

Vault Configuration for Self-Hosting

Switchbord stores per-workspace secrets in Supabase Vault — a Postgres extension that encrypts secrets at the database level. This page covers what self-hosters need to know to get vault working correctly, especially when running multiple workspaces.

Overview: Two Modes of Secret Resolution

Switchbord supports two approaches to supplying runtime secrets like the Meta access token: Vault mode (default, recommended) Secrets are stored per-workspace in vault.secrets, linked to each workspace via app_private.workspace_secret_bindings. The worker reads them at runtime using Supabase RPC functions. This is the correct approach for any deployment with more than one workspace. Env var fallback (single-workspace only) If SELF_HOSTED=true is set and a vault read fails, the worker falls back to WHATSAPP_META_ACCESS_TOKEN from the environment. This is a convenience for operators running a single workspace who want to skip vault setup entirely.
SELF_HOSTED=true bypasses vault lookups and serves one shared credential to all workspaces. If your deployment has more than one workspace, this will cause workspace A’s messages to be sent using workspace B’s token. Do not use env var fallback in multi-workspace deployments.

Why Multi-Tenant Deployments Must Use Vault

When the worker processes an outbound message, it resolves the Meta access token by looking up the workspace that owns the conversation. In vault mode, it calls sb_read_workspace_secret(workspace_id, 'meta-access-token') — a scoped lookup that returns only the secret belonging to that workspace. With SELF_HOSTED=true, the lookup is skipped entirely and the environment variable value is returned regardless of which workspace is making the request. There is no per-workspace isolation. A second workspace added later will silently use the first workspace’s token, which will fail or — worse — succeed with the wrong account.
If you initially deployed with SELF_HOSTED=true for a single workspace and are now adding a second workspace, you must remove that flag, configure vault secrets for both workspaces, and restart the worker before the second workspace will function correctly.

How Secrets Flow Through the System

1

Operator saves a credential in Settings

The operator opens Settings → Provider and enters a Meta access token (or API key for an AI provider). The Switchbord web app sends this to /api/settings/secrets.
2

API stores the secret in Supabase Vault

The API route calls storeWorkspaceSecret(workspaceId, secretKind, value), which calls vault.create_secret() and inserts a row into app_private.workspace_secret_bindings mapping the workspace ID and secret kind to the vault secret ID.
3

Worker reads the secret at send time

When the worker is about to send a message, it calls the Supabase RPC function sb_read_workspace_secret with the workspace ID and secret kind. The function runs with SECURITY DEFINER privileges, reads from vault.secrets, and returns the decrypted value. No direct database connection is required from the worker.
The worker never caches secrets across requests. Each send operation triggers a fresh vault read, so rotating a secret in Settings takes effect immediately.

Supabase Setup Requirements

Vault Extension

Supabase Vault is enabled by default on all Supabase Cloud projects. If you are running a self-hosted Supabase instance, confirm the extension is enabled:
If the query returns no rows, enable it:
Switchbord migrations also create the app_private schema and the workspace_secret_bindings table automatically when you run supabase db push or apply migrations. You do not need to create these manually.

RPC Functions

The worker uses two public RPC wrapper functions that must be present in your database:
  • sb_read_workspace_secret(p_workspace_id uuid, p_kind text) — returns the decrypted secret value for a workspace and secret kind
  • sb_resolve_workspace_by_verify_token(p_verify_token text) — returns the workspace ID matching a given Meta verify token
These functions are created by Switchbord’s migrations. If you encounter function does not exist errors, re-run your migrations against the target database:

Storing Secrets

There are two ways to populate vault secrets for a workspace: Settings UI (recommended) Open Settings → Provider in the Switchbord web app and enter credentials directly. The UI calls the vault write API automatically. No direct database access is needed. Supabase Dashboard SQL editor For initial setup or bulk loading, you can insert secrets directly:
Do not insert plaintext secrets into any public schema table. Always use vault.create_secret() so the value is encrypted before storage.

Worker Environment Variables

The worker needs only a Supabase service role key to read from vault. No direct database connection is required for reads. Required: Optional:
If you are deploying on Railway, set these as service variables in your worker service’s environment panel. The worker will pick them up on next deploy without any code changes.

Secret Kinds Reference


Troubleshooting

vault_unavailable

The vault extension is not enabled or the app_private schema is missing.
  • Confirm the vault extension exists (see the SQL check above)
  • Re-run Switchbord migrations: supabase db push
  • Confirm the worker’s SUPABASE_SECRET_KEY is a valid service role key, not the anon key

vault_read_error

The RPC function exists but returned an error when attempting to read a secret.
  • Check that sb_read_workspace_secret was created successfully by your migrations
  • Confirm SUPABASE_SECRET_KEY has service role permissions (the anon key cannot call SECURITY DEFINER functions that check for service_role)
  • In the Supabase SQL editor, verify the function exists: \df sb_read_workspace_secret

vault_secret_missing

The function ran successfully but found no binding for the requested workspace and secret kind. The secret was never stored.
  • Open Settings → Provider for the affected workspace and save the missing credential
  • Or insert the binding manually using the SQL above
  • Verify the workspace ID in app_private.workspace_secret_bindings matches the workspace making the request

Worker sends messages with the wrong account

This is a symptom of SELF_HOSTED=true being set in a multi-workspace deployment. The env var fallback credential is being used instead of per-workspace vault secrets. Remove SELF_HOSTED=true from the worker environment and ensure all workspaces have their secrets configured in vault.