DLRSDLRS
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 risk

Beginner: 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_URL with SSL (?sslmode=require for managed DB)
  • NODE_ENV=production → Secure cookies, no stack traces to client
  • npm run check:tenancy passes in CI
  • Backup vault: export encrypted blobs + keep SECRET_ENCRYPTION_KEY safe (like master password)
  • Rate limit: 100 req/min per IP (in next.config or reverse proxy)
  • Rotate OAuth refresh tokens periodically

On this page