Security & Architecture

Zeranda is built with privacy and security as first-class concerns. This page documents our trust boundaries, how your data flows through the system, and the controls we use to protect it.

Last updated: May 2026

TAC CASA Tier 2 Certified

Zeranda has passed the Cloud Application Security Assessment (CASA) Tier 2 evaluation by the App Defense Alliance — independently verifying our security controls meet industry standards for cloud application security.

Trust Boundaries

Every component in Zeranda sits behind a clearly defined trust boundary. No access decision is made in the browser — all authorization is enforced server-side.

Your Browser

Untrusted
  • React UI — rendering only, no access decisions
  • Passphrase entry and AES-GCM key derivation via Web Crypto
  • Email provider tokens encrypted before leaving the browser
  • Supabase JS SDK for session management (PKCE)
  • Encryption key held in memory / IndexedDB — never sent to server

Vercel / Next.js Server

Trusted
  • requireAuth() verifies JWT via supabase.auth.getUser()
  • requireAdmin() / requireEditor() enforce role-based access
  • Server Actions and API routes enforce access before any DB query
  • Cron routes require Authorization: Bearer CRON_SECRET
  • No secrets or service-role key in client bundle
  • File uploads validated by MIME type and size before storage

Supabase (Auth + Postgres)

Trusted
  • Row-Level Security on every user-owned table
  • Service-role key only used in server-side scanner/cron paths
  • AES-256 at-rest encryption for all data
  • Google and Microsoft OAuth + JWT issuance via Supabase Auth
  • Unique constraint on (user_id, provider, email) prevents account collisions

External Services

External
  • Gmail API — read/send email and calendar (Google OAuth scopes)
  • Microsoft Graph API — read/send email and calendar (Microsoft OAuth scopes)
  • Vercel AI Gateway — proxied LLM calls (no raw provider keys in codebase)
  • exchangerate-api.com — daily FX rates (public, read-only)
  • Google AdSense — ad display for free-tier users only

Significant Data Flows

These are the primary paths data takes through Zeranda.

Email Scan Pipeline

  1. 1.Cron trigger fires on a per-tier schedule (5 min / 2 hr / daily)
  2. 2.Gmail or Microsoft Graph API fetches recent emails via stored OAuth tokens
  3. 3.Email scanner builds a ScanContext (dismissed items, bill history, VIP senders) once per user to avoid N+1 DB queries
  4. 4.Each email is analysed via the Vercel AI Gateway using a Zod-validated schema
  5. 5.Extracted entities (bills, packages, subscriptions, tasks, calendar events, receipts) are inserted into Postgres
  6. 6.Raw email content is never stored — only structured, extracted fields

Authentication

  1. 1.User initiates Google or Microsoft OAuth via Supabase Auth (PKCE flow)
  2. 2.Provider issues an auth code; Supabase exchanges it for tokens
  3. 3.Supabase issues a short-lived JWT stored in an HTTP-only cookie
  4. 4.Every protected route calls requireAuth() → getUser() to verify the JWT server-side
  5. 5.Admin routes additionally call requireAdmin(); editor routes call requireEditor()

Email Provider Token Encryption

  1. 1.After OAuth, the browser sends plaintext tokens to the server immediately after exchange
  2. 2.Server encrypts both access and refresh tokens with AES-256-GCM using TOKEN_ENCRYPTION_KEY before writing to DB
  3. 3.Encrypted blobs are stored in Supabase; decryption happens server-side only (scanner, cron, Server Actions)
  4. 4.New tokens received on refresh are re-encrypted and persisted automatically
  5. 5.Accounts where tokens are revoked are flagged needs_reconnect and skipped by the scanner

Account Deletion

  1. 1.User requests deletion via Settings → Delete Account
  2. 2.Server iterates FK-dependency order and deletes data from 16+ tables (bills, packages, tasks, subscriptions, receipts, contacts, etc.)
  3. 3.If any step fails, deletion halts before the auth user record is touched — no partial deletes
  4. 4.Final step: Supabase auth user record deleted (point of no return)
  5. 5.All connected provider OAuth tokens are revoked

Security Controls

A summary of the controls protecting your data at each layer.

ControlImplementation
AuthenticationGoogle and Microsoft OAuth via Supabase Auth (PKCE flow)
Session managementHTTP-only JWT cookie, verified server-side via getUser() — not getSession()
URI-level authorizationrequireAuth() on every protected route and Server Action
Role-based accessrequireAdmin() and requireEditor() guards on all admin/blog routes
Resource-level authorizationPostgres Row-Level Security on all user-owned tables
Token encryptionAES-256-GCM server-side with TOKEN_ENCRYPTION_KEY (32-byte random key, encrypted at rest)
Service-role isolationAdmin Supabase client used only in server-side scanner/cron paths — never in browser bundle
LLM output safetyAll AI responses validated via Zod schemas before writing to DB
Cron securityAuthorization: Bearer CRON_SECRET on all cron routes
File upload validationMIME type whitelist (JPEG/PNG/WebP/GIF) and 5 MB size cap before Supabase Storage write
Input validationEnum whitelists for tier, status, currency; length caps on all user-supplied strings
Transport securityTLS managed by Vercel with OCSP stapling
At-rest encryptionSupabase Postgres AES-256 encryption at rest
Email contentRaw email content is never stored — only structured entities extracted by AI
SSRF preventionAll outbound URLs are hardcoded — no user-supplied URLs are fetched
Output encodingReact JSX escaping by default — no dangerouslySetInnerHTML on user input
Account deletionCascading delete across 16+ tables in FK order with rollback on error
Multi-provider isolationUnique constraint on (user_id, provider, email) prevents cross-account token collision

Data We Retain

DataEncryptedRetention
Google / Microsoft OAuth tokensYes (AES-256-GCM, server-managed key)Until account deletion
Email metadata (date, urgency score, sender, subject)No — RLS-protectedUntil dismissed / deleted
Raw email contentNever storedNever persisted
Bills (amount, due date, merchant, status)No — RLS-protectedUntil dismissed / deleted
Subscriptions (name, renewal date, amount)No — RLS-protectedUntil dismissed / deleted
Packages (tracking number, carrier, status)No — RLS-protectedUntil dismissed / deleted
Tasks (title, due date, source email ID)No — RLS-protectedUntil dismissed / deleted
Calendar events (title, time, description)No — RLS-protectedUntil dismissed / deleted
Receipts and receipt line itemsNo — RLS-protectedUntil dismissed / deleted
Bank statements and transactionsNo — RLS-protectedUntil dismissed / deleted
Contact records (email, display name, interaction count)No — RLS-protectedUntil manually deleted
Email attachment metadata (filename, size, IDs)No — RLS-protectedUntil dismissed / deleted
Chat messages (question + AI response)No — RLS-protectedUntil account deletion
Exchange ratesNo — public dataRolling daily record

Security Contact

Found a security issue? Please report it responsibly to security@zeranda.app. We aim to respond within 48 hours.