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

# Secrets Management & Encryption

> How Switchbord stores tenant secrets in Supabase Vault, enforces security headers, and protects data in transit and at rest.

## Supabase Vault for Tenant Secrets

All tenant secrets are stored in [Supabase Vault](https://supabase.com/docs/guides/database/vault), a Postgres extension that encrypts secrets at the database level. Secrets are scoped per workspace and never stored in public schema tables.

### Secret Kinds

Each workspace can store the following secret types in Vault:

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

### Vault Access Pattern

Secrets are read from Vault only at the point of use, scoped to the requesting workspace:

```typescript theme={null}
async function getWorkspaceSecret(
  workspaceId: string,
  kind: SecretKind
): Promise<string> {
  const { data, error } = await supabase.rpc('vault_read_secret', {
    p_workspace_id: workspaceId,
    p_kind: kind,
  });

  if (error || !data) throw new SecretNotFoundError(kind);
  return data.secret_value;
}
```

Secrets from workspace A are structurally inaccessible to workspace B — the vault read function is parameterized by workspace ID, which is always derived from the authenticated session.

## No Environment Variable Fallback in Production

Environment variable fallbacks for secrets are **gated to non-production environments only**:

```typescript theme={null}
async function resolveMetaToken(workspaceId: string): Promise<string> {
  // Always try Vault first
  const vaultSecret = await getWorkspaceSecret(workspaceId, 'meta-access-token')
    .catch(() => null);

  if (vaultSecret) return vaultSecret;

  // Fallback only permitted outside production
  if (process.env.NODE_ENV !== 'production' && process.env.META_ACCESS_TOKEN) {
    return process.env.META_ACCESS_TOKEN;
  }

  throw new ConfigurationError('No Meta access token configured for this workspace');
}
```

This ensures that a misconfigured production deployment will fail loudly rather than silently fall back to a shared credential.

<Warning>
  In production, if Vault returns no secret for a workspace, the operation fails with an error. There is no global fallback credential that could inadvertently be used by multiple tenants.
</Warning>

## API Key Storage

API keys are never stored in plaintext. The storage scheme:

1. **Generation:** A cryptographically random token is generated
2. **Hashing:** The token is hashed with SHA-256 before database storage
3. **Prefix:** The first 8 characters of the raw token are stored separately for display identification
4. **Display:** The full raw token is shown to the user exactly once at creation time

```typescript theme={null}
import { createHash, randomBytes } from 'crypto';

function generateApiKey() {
  const raw = randomBytes(32).toString('hex'); // 64-char hex string
  const hash = createHash('sha256').update(raw).digest('hex');
  const prefix = raw.slice(0, 8);
  return { raw, hash, prefix };
}
```

### Timing-Safe Validation

Key validation uses `crypto.timingSafeEqual` to prevent timing oracle attacks:

```typescript theme={null}
import { timingSafeEqual, createHash } from 'crypto';

function isValidApiKey(providedRaw: string, storedHash: string): boolean {
  const providedHash = createHash('sha256').update(providedRaw).digest('hex');
  const a = Buffer.from(providedHash, 'hex');
  const b = Buffer.from(storedHash, 'hex');

  // Length check must not short-circuit before timingSafeEqual
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}
```

## Security Headers

All HTTP responses from the Switchbord application include the following security headers:

### Content Security Policy

```
Content-Security-Policy: default-src 'self';
  script-src 'self' 'nonce-{nonce}';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://*.supabase.co wss://*.supabase.co;
  frame-ancestors 'none';
```

### HTTP Strict Transport Security

```
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
```

The `preload` directive submits the domain to browser HSTS preload lists, ensuring HTTPS-only connections even on first visit.

### Additional Headers

| Header                   | Value                                      | Purpose                           |
| ------------------------ | ------------------------------------------ | --------------------------------- |
| `X-Frame-Options`        | `DENY`                                     | Prevents clickjacking attacks     |
| `X-Content-Type-Options` | `nosniff`                                  | Prevents MIME-type sniffing       |
| `Referrer-Policy`        | `strict-origin-when-cross-origin`          | Controls referrer information     |
| `Permissions-Policy`     | `camera=(), microphone=(), geolocation=()` | Disables unnecessary browser APIs |

## CORS Policy

CORS is restricted to an explicit allowlist of known origins:

```typescript theme={null}
const ALLOWED_ORIGINS = [
  'https://app.switchbord.ai',
  ...(process.env.NODE_ENV !== 'production' ? ['http://localhost:3000'] : []),
];

function corsMiddleware(req: Request): Response | null {
  const origin = req.headers.get('Origin');
  if (origin && !ALLOWED_ORIGINS.includes(origin)) {
    return new Response('Forbidden', { status: 403 });
  }
  return null; // proceed
}
```

<Warning>
  CORS restrictions prevent browser-based cross-origin requests from unauthorized domains. They do not prevent direct API calls (e.g., from curl or server-to-server). API authentication is the primary access control.
</Warning>

## Encryption at Rest

Supabase provides transparent disk encryption for all data stored in its managed PostgreSQL instances. This covers the `contacts`, `messages`, `workspace_members`, `audit_logs`, and all other tables.

**Planned enhancement (BORD-171):** Column-level encryption for PII fields (phone numbers, names, message bodies) using Supabase's pgsodium extension. This would provide application-level encryption in addition to disk-level encryption, protecting against scenarios where database credentials are compromised.

## TLS in Transit

All connections to and from Switchbord components are encrypted using TLS:

* **Vercel (web/API):** TLS 1.2+ enforced by Vercel's edge network
* **Supabase:** TLS required for all database and API connections
* **Railway (worker):** TLS enforced for all outbound HTTP calls
* **Meta Cloud API:** All WhatsApp API calls made over HTTPS
* **AI providers:** All API calls to OpenAI, Anthropic, OpenRouter made over HTTPS

## Open Redirect Protection

The auth callback endpoint validates the `next` redirect parameter to prevent open redirect attacks:

```typescript theme={null}
function isSafeRedirect(url: string): boolean {
  // Allow relative paths
  if (url.startsWith('/') && !url.startsWith('//')) return true;

  // Allow known origins
  const allowedOrigins = ['https://app.switchbord.ai'];
  try {
    const parsed = new URL(url);
    return allowedOrigins.some(origin => parsed.origin === origin);
  } catch {
    return false;
  }
}

// In auth callback
const next = searchParams.get('next') ?? '/';
const redirectTo = isSafeRedirect(next) ? next : '/';
```

## Input Validation

All key API request bodies are validated with Zod schemas before processing:

```typescript theme={null}
const CreateContactSchema = z.object({
  phoneNumber: z.string().regex(/^\+[1-9]\d{1,14}$/),
  name: z.string().min(1).max(255).optional(),
  customFields: z.record(z.string()).optional(),
});

export async function POST(req: Request) {
  const body = await req.json();
  const parsed = CreateContactSchema.safeParse(body);

  if (!parsed.success) {
    return Response.json({ error: 'Invalid request' }, { status: 400 });
    // Zod errors are logged server-side but not returned to clients
  }
  // ...
}
```

Error responses are sanitized — raw Supabase or database errors are never returned to clients.

## Compliance Mapping

| Standard  | Control                                        | Implementation                                            |
| --------- | ---------------------------------------------- | --------------------------------------------------------- |
| ISO 27001 | A.10.1 — Cryptographic controls                | SHA-256 API key hashing, TLS in transit, Vault encryption |
| ISO 27001 | A.13.1 — Network security management           | CORS allowlist, security headers                          |
| ISO 27001 | A.13.2 — Information transfer                  | TLS enforced for all connections                          |
| ISO 27001 | A.14.2 — Security in development               | Input validation (Zod), output sanitization               |
| GDPR      | Art.32 — Security of processing                | Encryption at rest and in transit, access controls        |
| HIPAA     | §164.312(a)(2)(iv) — Encryption and decryption | TLS in transit, Vault encryption at rest                  |
| HIPAA     | §164.312(e)(2)(ii) — Encryption in transit     | TLS enforced on all external connections                  |
| SOC 2     | CC6.1 — Logical access controls                | API key hashing, timing-safe comparison                   |
| SOC 2     | CC6.7 — Data in transit and at rest            | TLS + disk encryption + planned column-level encryption   |

<Tip>
  For the full compliance control mapping, see the [Compliance Matrix](/security/compliance-matrix).
</Tip>
