Security & Privacy

Multi-layer security architecture protecting your financial data at every level

🔐 JWT Authentication Active

Supabase Auth with JWT tokens. Every API request is authenticated and verified server-side.

🛡️ Row Level Security Active

PostgreSQL RLS ensures users can only access their own data, enforced at the database level.

⏱️ Rate Limiting Active

100 req/min REST, 30 msg/min WebSocket per user. Prevents abuse and DDoS attempts.

🔒 TLS Encryption Active

All traffic encrypted via HTTPS/WSS. Caddy automatically manages TLS certificates.

✅ Input Validation Active

Zod schema validation on all inputs. No raw SQL, parameterized queries only.

⚠️ Action Confirmation Active

Dangerous commands (delete wallet, close debt) require explicit user confirmation.

1 Authentication Flow

Client Server Supabase Login (email/password) ─────────────────────→ supabase.auth.signIn() ─────────────────────→ Validate credentials ←───────────────────── Return JWT tokens ←───────────────────── Store tokens iOS: Keychain Web: localStorage API Request Authorization: Bearer <jwt> ─────────────────────→ supabase.auth.getUser(token) ─────────────────────→ Verify JWT ←───────────────────── request.authUser = { id, email } Process request... ←───────────────────── Token expired? Auto-refresh via Supabase SDK ─────────────────────→ Issue new tokens ←─────────────────────
💡
Token Storage iOS stores tokens securely in the Keychain via KeychainService. Web uses localStorage with Supabase SDK handling automatic token refresh.

2 Row Level Security (RLS)

Every table has RLS enabled with policies that ensure users can only access their own data. This is enforced at the PostgreSQL level — even if application code has a bug, the database won't expose another user's data.

wallets RLS Enabled
CREATE POLICY "Users can access own wallets" ON wallets FOR ALL USING (auth.uid() = user_id);
transactions RLS Enabled
CREATE POLICY "Users can access own transactions" ON transactions FOR ALL USING (auth.uid() = user_id);
categories RLS Enabled
CREATE POLICY "Users can access own categories" ON categories FOR ALL USING (auth.uid() = user_id);
contacts RLS Enabled
CREATE POLICY "Users can access own contacts" ON contacts FOR ALL USING (auth.uid() = user_id);

All other tables (recurring_templates, ai_notifications, chat_messages, action_history, automation_rules, etc.) follow the same pattern: USING (auth.uid() = user_id).

⚠️
Service Role The server uses SUPABASE_SERVICE_KEY (service role) to bypass RLS for server-side operations like cron jobs and admin tasks. This key is never exposed to clients.

3 Middleware Chain

Every request passes through a security middleware chain before reaching route handlers:

Incoming Request 1. CORS Middleware Validates origin, methods, headers 2. Auth Middleware Extracts JWT token from Authorization header Validates via supabase.auth.getUser(token) → 401 Unauthorized if invalid 3. Rate Limit Middleware Checks per-user rate limit (100 req/min) → 429 Too Many Requests if exceeded 4. Route Handler ✓ Authenticated user available as request.authUser ✓ Rate limit not exceeded ✓ Input validated with Zod schemas

4 Input Validation

All user input is validated using Zod schemas before processing. This prevents injection attacks and ensures data integrity.

// Example: Transaction creation validation const createTransactionSchema = z.object({ wallet_id: z.string().uuid(), // Must be valid UUID category_id: z.string().uuid(), // Must be valid UUID amount: z.number().positive(), // Must be > 0 currency: z.string().length(3) // Must be 3 chars (ISO) .default('RUB'), description: z.string().max(500) // Max 500 characters .optional(), date: z.string().datetime().optional() // Valid ISO 8601 });

Protection Against Common Attacks

Attack Vector Protection Layer
SQL Injection Parameterized Supabase client, no raw SQL with user input Database
XSS (Cross-Site Scripting) React auto-escapes output; SwiftUI is safe by default Client
CSRF JWT in Authorization header (not cookies) Auth
Unauthorized Access RLS at DB level + auth middleware at API level Database + API
Brute Force Rate limiting (100 req/min per user) Middleware
Man-in-the-Middle TLS encryption via Caddy (auto-renewing certificates) Transport
Data Exposure Service keys server-only; public keys for client auth only Config

5 Rate Limiting

Channel Limit Window Algorithm
REST API 100 requests Per minute per user Sliding window
WebSocket 30 messages Per minute per user Sliding window
// Rate limit response headers X-RateLimit-Limit: 100 X-RateLimit-Remaining: 85 Retry-After: 12 // seconds (only when exceeded) // When exceeded: HTTP/1.1 429 Too Many Requests { "error": "RATE_LIMIT_EXCEEDED", "retryAfter": 12 }

6 Dangerous Actions Confirmation

Commands that modify or delete important data require explicit user confirmation. The action is stored as "pending" and executed only after confirmation.

Command Risk Level What It Does
delete_wallet High Deletes wallet and orphans all associated transactions
delete_transaction Medium Permanently removes transaction and adjusts wallet balance
close_debt Medium Archives contact and all debt records
delete_goal Medium Removes savings goal
delete_budget Medium Removes budget limit from category
delete_recurring Medium Cancels recurring payment template
delete_category Medium Removes category (transactions become uncategorized)
User: "Delete my card wallet" AI parses → delete_wallet command Command is DANGEROUS → Store as pending_action Bot: "Are you sure you want to delete wallet 'Card'? This will remove the wallet and orphan 142 transactions." User: "Yes" / clicks Confirm button Execute pending_action → Wallet deleted Action recorded in action_history (undoable)

7 Secret Management

SUPABASE_URL
PostgreSQL database connection URL
Server only
SUPABASE_SERVICE_KEY
Service role key (bypasses RLS). Never exposed to clients.
Server only
OPENAI_API_KEY
AI API key for GPT models and Whisper
Server only
SUPABASE_ANON_KEY
Public key for client-side auth only. Limited by RLS.
Client (limited)
⚠️
Never expose service keys SUPABASE_SERVICE_KEY and OPENAI_API_KEY are stored only in the server's .env file and never included in client bundles, git repositories, or public responses.

8 Client Security

📱 iOS

Token storage: Keychain (encrypted, hardware-backed)
Network: HTTPS only, certificate pinning ready
UI: SwiftUI (auto XSS protection)
Auth refresh: Automatic on app foreground

🌐 Web

Token storage: localStorage (Supabase SDK)
Network: HTTPS only via Caddy
UI: React (auto XSS protection)
Auth refresh: Automatic via Supabase client

🔌 WebSocket

Auth: JWT required as first message
Encryption: WSS (TLS)
Rate limit: 30 msg/min per user
Timeout: Auto-disconnect on inactivity