Skip to content

Database ​

Overview ​

We use Drizzle ORM with PostgreSQL (Supabase). All schemas are in packages/database/src/schema/.

Schema Organization ​

FileTablesDomain
auth.tsusers, sessionsAuthentication
catalog.tsoperators, packagesTelecom catalog
activation.tsorders, orderItems, activations, identityVerificationsSIM lifecycle
payment.tspayments, invoicesPayment processing
support.tstickets, ticketCommentsCustomer support
b2b.tscompanies, companyUsers, bulkOrdersB2B portal
audit.tsauditLogs, consentRecordsCompliance (immutable)

PII Encryption ​

Sensitive fields use AES-256-GCM encryption transparently via Drizzle custom types:

ts
// Encrypted fields are automatically encrypted on write and decrypted on read
const user = await db.select().from(users); // phone, email, name, idNumber auto-decrypted

Encrypted columns: phone, email, name, idNumber, contactPhone.

The encryption key is read at call time (not startup) to support key rotation.

Row Level Security (RLS) ​

Each domain has RLS policies documented in schema files and applied through custom SQL migrations:

ts
import { authRlsPolicies } from '@activation-sys/database';
// The executable SQL lives in packages/database/drizzle/custom/0004_apply_rls_policies.sql

After applying migrations, run pnpm db:verify to confirm the live Supabase database has RLS enabled, expected policies installed, custom migration hashes matching, Realtime publication configured, required triggers present, RLS helper owners able to bypass table policies, and no invalid public indexes.

Common Commands ​

bash
# Generate migration from schema changes
pnpm db:generate

# Run pending migrations
pnpm db:migrate

# Verify the live Supabase database state
pnpm db:verify

# Local throwaway schema push only; blocked unless explicitly confirmed
ALLOW_DB_PUSH=1 pnpm db:push:unsafe

Entity Relationships ​

users ──┬── sessions
        ├── orders ──── order_items
        ├── activations ──── identity_verifications
        ├── payments ──── invoices
        ├── tickets ──── ticket_comments
        └── company_users ──── companies

operators ──── packages
companies ──── company_users
                └── bulk_orders

The recursion-breaking users-policy helpers require a function owner that bypasses RLS on users and activations: a superuser/BYPASSRLS role, or membership in both owning roles when FORCE RLS is disabled. Apply migration 0027 through that deployment role. db:verify checks the actual function owners, including ownership drift after deployment.

An interrupted concurrent index build can leave an invalid index whose name causes IF NOT EXISTS to skip rebuilding. If db:verify reports one, inspect pg_index.indisvalid, drop that invalid index with DROP INDEX CONCURRENTLY, and rerun its original concurrent-index SQL. Do not edit applied migration files or their tracking hashes to bypass the check.

Internal documentation - Activation System