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.Cron trigger fires on a per-tier schedule (5 min / 2 hr / daily)
- 2.Gmail or Microsoft Graph API fetches recent emails via stored OAuth tokens
- 3.Email scanner builds a ScanContext (dismissed items, bill history, VIP senders) once per user to avoid N+1 DB queries
- 4.Each email is analysed via the Vercel AI Gateway using a Zod-validated schema
- 5.Extracted entities (bills, packages, subscriptions, tasks, calendar events, receipts) are inserted into Postgres
- 6.Raw email content is never stored — only structured, extracted fields
Authentication
- 1.User initiates Google or Microsoft OAuth via Supabase Auth (PKCE flow)
- 2.Provider issues an auth code; Supabase exchanges it for tokens
- 3.Supabase issues a short-lived JWT stored in an HTTP-only cookie
- 4.Every protected route calls requireAuth() → getUser() to verify the JWT server-side
- 5.Admin routes additionally call requireAdmin(); editor routes call requireEditor()
Email Provider Token Encryption
- 1.After OAuth, the browser sends plaintext tokens to the server immediately after exchange
- 2.Server encrypts both access and refresh tokens with AES-256-GCM using TOKEN_ENCRYPTION_KEY before writing to DB
- 3.Encrypted blobs are stored in Supabase; decryption happens server-side only (scanner, cron, Server Actions)
- 4.New tokens received on refresh are re-encrypted and persisted automatically
- 5.Accounts where tokens are revoked are flagged needs_reconnect and skipped by the scanner
Account Deletion
- 1.User requests deletion via Settings → Delete Account
- 2.Server iterates FK-dependency order and deletes data from 16+ tables (bills, packages, tasks, subscriptions, receipts, contacts, etc.)
- 3.If any step fails, deletion halts before the auth user record is touched — no partial deletes
- 4.Final step: Supabase auth user record deleted (point of no return)
- 5.All connected provider OAuth tokens are revoked
Security Controls
A summary of the controls protecting your data at each layer.
| Control | Implementation |
|---|---|
| Authentication | Google and Microsoft OAuth via Supabase Auth (PKCE flow) |
| Session management | HTTP-only JWT cookie, verified server-side via getUser() — not getSession() |
| URI-level authorization | requireAuth() on every protected route and Server Action |
| Role-based access | requireAdmin() and requireEditor() guards on all admin/blog routes |
| Resource-level authorization | Postgres Row-Level Security on all user-owned tables |
| Token encryption | AES-256-GCM server-side with TOKEN_ENCRYPTION_KEY (32-byte random key, encrypted at rest) |
| Service-role isolation | Admin Supabase client used only in server-side scanner/cron paths — never in browser bundle |
| LLM output safety | All AI responses validated via Zod schemas before writing to DB |
| Cron security | Authorization: Bearer CRON_SECRET on all cron routes |
| File upload validation | MIME type whitelist (JPEG/PNG/WebP/GIF) and 5 MB size cap before Supabase Storage write |
| Input validation | Enum whitelists for tier, status, currency; length caps on all user-supplied strings |
| Transport security | TLS managed by Vercel with OCSP stapling |
| At-rest encryption | Supabase Postgres AES-256 encryption at rest |
| Email content | Raw email content is never stored — only structured entities extracted by AI |
| SSRF prevention | All outbound URLs are hardcoded — no user-supplied URLs are fetched |
| Output encoding | React JSX escaping by default — no dangerouslySetInnerHTML on user input |
| Account deletion | Cascading delete across 16+ tables in FK order with rollback on error |
| Multi-provider isolation | Unique constraint on (user_id, provider, email) prevents cross-account token collision |
Data We Retain
| Data | Encrypted | Retention |
|---|---|---|
| Google / Microsoft OAuth tokens | Yes (AES-256-GCM, server-managed key) | Until account deletion |
| Email metadata (date, urgency score, sender, subject) | No — RLS-protected | Until dismissed / deleted |
| Raw email content | Never stored | Never persisted |
| Bills (amount, due date, merchant, status) | No — RLS-protected | Until dismissed / deleted |
| Subscriptions (name, renewal date, amount) | No — RLS-protected | Until dismissed / deleted |
| Packages (tracking number, carrier, status) | No — RLS-protected | Until dismissed / deleted |
| Tasks (title, due date, source email ID) | No — RLS-protected | Until dismissed / deleted |
| Calendar events (title, time, description) | No — RLS-protected | Until dismissed / deleted |
| Receipts and receipt line items | No — RLS-protected | Until dismissed / deleted |
| Bank statements and transactions | No — RLS-protected | Until dismissed / deleted |
| Contact records (email, display name, interaction count) | No — RLS-protected | Until manually deleted |
| Email attachment metadata (filename, size, IDs) | No — RLS-protected | Until dismissed / deleted |
| Chat messages (question + AI response) | No — RLS-protected | Until account deletion |
| Exchange rates | No — public data | Rolling daily record |
Security Contact
Found a security issue? Please report it responsibly to security@zeranda.app. We aim to respond within 48 hours.