Reference
Security & Database — Tenancy, Vault, Audit
How DLRS keeps your data isolated, vault-encrypted, and fully auditable. Covers tenancy guards, session security, vault design, and Prisma schema — beginner to advanced.
Security Model Overview
DLRS Security Layers:
1. Tenancy (DB level) → workspace_id on every row, DAL guards
2. Auth (Session) → signed cookie, HttpOnly, SameSite, SESSION_SECRET
3. Vault (Encryption) → AES-256-GCM for OAuth tokens, PAN/DOB fragments
4. Provenance (Audit) → every action logged with source + approved_by
5. Permissions (Agent) → SAFE vs SENSITIVE tool riskBeginner: Why Tenancy Matters
Tenancy = your workspace data never leaks to another workspace. Even if code has bug, DAL check
WHERE workspace_id = ? prevents cross-tenant read. It's iron-clad, not just UI filtering.Workspace Isolation (Tenancy)
// lib/db/tenant.ts — wraps Prisma
export function tenantDb(workspaceId: string) {
return {
task: {
findMany: (args) => prisma.task.findMany({ ...args, where: { ...args.where, workspaceId } }),
create: (args) => prisma.task.create({ data: { ...args.data, workspaceId } }),
// update, delete likewise
}
}
}
// In every route:
const user = await getCurrentUser()
await requireWorkspaceMember(params.workspace, user.id)
const db = tenantDb(params.workspace)
await db.task.findMany({ where: { status: "InProgress" } }) // auto-scoped
Authentication & Sessions
Env: SESSION_SECRET (32-byte hex)
Cookie: session=base64(JSON.stringify({ userId, workspaceId })) + HMAC(SHA256, SESSION_SECRET)
Flags: HttpOnly, Secure (in prod), SameSite=Lax, Path=/, Max-Age=30d
Flow:
1. POST /login { email, password } → verify via bcrypt
2. Create session → Set-Cookie
3. Subsequent requests: getSession() decrypts + verifies HMAC
4. getCurrentUser() → db.user.findUnique
Logout: clear cookie
Middleware: app/middleware.ts protects /w/* → redirect /login if no session🔒 Must Change Default
Generate unique
SESSION_SECRET per deployment. If leaked, all sessions forgeable. Rotate: invalidate all cookies.Vault — Encrypted Secrets
Vault stores sensitive data that would be plaintext in DB if naive: OAuth tokens, bank password hints.
Table: vault_items {
id, workspace_id, kind: "gmail"|"drive"|"pan"|"dob",
encrypted_blob: string (base64 of AES-GCM ciphertext),
created_at, updated_at
}Bank Statement Security
Vault Hints
Store PAN/DOB/phone fragments encrypted. Not full passwords — just hints to try patterns.
Transient Trial
for (pattern of ["DDMMYYYY", "PAN+DD", "phone+..."]) {
password = build(pattern, decryptHints())
try { pdf.unlock(password) } catch {}
if (success) break
}
// Password variable goes out of scope, garbage collected. Never stored.Validation
After parse, validate totals with integer minor units; if mismatch, flag. Finance Agent never trusts LLM for arithmetic.
Database Schema & Prisma
Audit & Provenance
Tables:
activity_log { id, workspace_id, actor, action, target_id, details, created_at }
task_extractions { id, task_id, inbox_item_id, extracted_json, confidence, model }
job_runs { id, job_id, output, error, started_at, finished_at }
Query example:
SELECT * FROM activity_log
WHERE workspace_id = 'my-studio' AND action = 'create_task'
ORDER BY created_at DESC;
UI: Task → Source → shows "Created from Gmail ID abc123, confidence 0.94, by Email Agent"Checklist for Production
🔒 Security Checklist
- Unique strong
SESSION_SECRET,SECRET_ENCRYPTION_KEY,WEBHOOK_SECRET— never default DATABASE_URLwith SSL (?sslmode=requirefor managed DB)NODE_ENV=production→ Secure cookies, no stack traces to clientnpm run check:tenancypasses in CI- Backup vault: export encrypted blobs + keep
SECRET_ENCRYPTION_KEYsafe (like master password) - Rate limit: 100 req/min per IP (in next.config or reverse proxy)
- Rotate OAuth refresh tokens periodically