Edge Functions & encryption
How privileged operations run on Supabase Edge Functions with AES-256-GCM at rest and SHA-256 at the boundary.
Every privileged operation in Teleperson runs in a Supabase Edge Function (Deno + TypeScript). The extension never executes a privileged operation directly — it always asks an Edge Function. This is where:
- Plaid access tokens get encrypted before going to Postgres.
- Stripe API calls happen with the platform secret key.
- Anthropic calls happen for cached company-level operations.
- Vendor OAuth token exchanges happen.
- Audit log entries get written.
Why Edge Functions
- Secret isolation. API keys (Stripe, Anthropic, Plaid client secret, ElevenLabs) live as environment variables on the Edge Function side, never reaching the extension.
- AES-256-GCM availability. The Web Crypto API used by
_shared/crypto.tsis the same crypto in browsers, so the encryption code is symmetric in shape but the key lives only on the server. - Authoritative plan checks. Plan gating happens server-side in the function itself, not in extension UI alone — the UI gate is a hint, the function gate is the policy.
Encryption at rest
For data that must persist but should never be retrievable in plaintext without the key:
// supabase/functions/_shared/crypto.ts
export async function encrypt(plaintext: string, keyBase64: string) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await importKey(keyBase64);
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
new TextEncoder().encode(plaintext)
);
return { ciphertext: new Uint8Array(ciphertext), iv };
}Stored as (ciphertext, iv) pairs:
| Table | Field | Key env var |
|---|---|---|
user_plaid_connections | access_token_ciphertext, token_iv | PLAID_TOKEN_ENCRYPTION_KEY |
user_vendor_connections | access_token_ciphertext, refresh_token_ciphertext, token_iv | VENDOR_TOKEN_ENCRYPTION_KEY |
Plaintext access tokens exist only inside an Edge Function request — they never leave the function.
Hashing at the boundary
For things we don't need to recover, hashes:
| Table | Field | Algorithm |
|---|---|---|
extension_tokens | token_hash | SHA-256 |
voice_call_log | to_number_hash | SHA-256 |
Lost tokens can't be recovered, only revoked and replaced — by design.
Key rotation
Encryption keys are versioned via a tag on the ciphertext (v1:,
v2:, …). Rotation:
- Add the new key to the Edge Function environment as
*_KEY_V2. - Re-encrypt rows lazily on next access (reads with V1 → re-encrypt under V2 → write back).
- Once all rows are re-encrypted (verified via a SQL count of
v1:prefixed rows = 0), retire the V1 env var.
The same pattern applies to the SHA-256 hashing of tokens — though hash-only fields don't need re-hashing; you simply roll the salt forward on new issuance.
What Edge Functions can't do
- No long-running jobs. Edge Functions have time limits. Long-running work (multi-step Plaid sync, retroactive merchant normalization) is decomposed into iterations and driven by background queues.
- No filesystem. Persistent state lives in Postgres or external object storage.
- No raw TCP. Outbound HTTP only.
Related
- Authentication → — how the extension proves it's the right user.
- Supabase data model & RLS → — what the functions read and write.