> ## Documentation Index
> Fetch the complete documentation index at: https://docs.switchbord.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Vault Configuration for Self-Hosting

> How to configure Supabase Vault for self-hosted Switchbord deployments — covering multi-tenant requirements, worker environment variables, and troubleshooting vault errors.

# Vault Configuration for Self-Hosting

Switchbord stores per-workspace secrets in [Supabase Vault](https://supabase.com/docs/guides/database/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.

<Warning>
  `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.
</Warning>

***

## 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.

<Info>
  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.
</Info>

***

## How Secrets Flow Through the System

<Steps>
  <Step title="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`.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

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:

```sql theme={null}
-- Run in your Supabase SQL editor
SELECT * FROM pg_extension WHERE extname = 'supabase_vault';
```

If the query returns no rows, enable it:

```sql theme={null}
CREATE EXTENSION IF NOT EXISTS supabase_vault SCHEMA vault;
```

<Info>
  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.
</Info>

### 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:

```bash theme={null}
supabase db push --db-url "$SUPABASE_DB_URL"
```

***

## 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:

```sql theme={null}
-- Insert a secret and get back its vault ID
SELECT vault.create_secret(
  'your-token-value-here',
  'meta-access-token for workspace abc123',
  null
) AS vault_secret_id;

-- Then bind it to a workspace
INSERT INTO app_private.workspace_secret_bindings
  (workspace_id, secret_kind, vault_secret_id)
VALUES
  ('your-workspace-uuid', 'meta-access-token', 'the-vault-secret-id-from-above');
```

<Warning>
  Do not insert plaintext secrets into any public schema table. Always use `vault.create_secret()` so the value is encrypted before storage.
</Warning>

***

## 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:**

| Variable                               | Description                                                |
| -------------------------------------- | ---------------------------------------------------------- |
| `NEXT_PUBLIC_SUPABASE_URL`             | Your Supabase project URL, e.g. `https://xxxx.supabase.co` |
| `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` | Supabase anon key                                          |
| `SUPABASE_SECRET_KEY`                  | Supabase service role key — used for vault reads via RPC   |
| `WHATSAPP_PLATFORM_DATA_MODE`          | Set to `supabase`                                          |
| `WORKER_POLL_INTERVAL_MS`              | Poll interval in milliseconds, e.g. `5000`                 |

**Optional:**

| Variable                     | Description                                                                                                                                       |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SUPABASE_DB_URL`            | Direct Postgres connection string. Only needed if you are using vault writes from the worker directly (not typical — the web app handles writes). |
| `SELF_HOSTED=true`           | Enables env var fallback for single-workspace deployments. Do not set this if you have more than one workspace.                                   |
| `WHATSAPP_META_ACCESS_TOKEN` | Required only when `SELF_HOSTED=true` is set. The fallback Meta access token.                                                                     |

<Tip>
  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.
</Tip>

***

## Secret Kinds Reference

| Secret Kind              | Purpose                                                        |
| ------------------------ | -------------------------------------------------------------- |
| `meta-access-token`      | Meta (WhatsApp Business API) access token for sending messages |
| `webhook-signing-secret` | Meta webhook payload signing secret for request verification   |
| `meta-verify-token`      | Meta webhook verification token                                |
| `openai-api-key`         | OpenAI API key for AI draft generation                         |
| `anthropic-api-key`      | Anthropic API key for Claude-based AI generation               |
| `openrouter-api-key`     | OpenRouter API key for multi-model AI routing                  |

***

## 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.

***

## Related Guides

* [Vault Architecture](/security/vault-architecture) — internals, schema design, and encryption details
* [Secrets and Encryption](/security/secrets-and-encryption) — security policy and access controls
* [Meta Credential Setup](/operations/meta-credential-setup) — how to obtain the credentials you will store in vault
* [Webhooks](/operations/webhooks) — Meta webhook verification token setup
