Skip to content

Database Package

@activation-sys/database — Drizzle ORM schemas, database connection, and PII encryption.

Installation

Already included in the monorepo workspace.

ts
import { db, users, operators, packages, ... } from '@activation-sys/database';

Exports

ts
// Schema tables
export * from './schema/index.js';

// Database connection
export { createDb, getDb, db } from './utils/connection.js';

// Encryption utilities
export { encrypt, decrypt } from './utils/encryption.js';

Tables

TableFileKey Columns
usersauth.tsid, phone (encrypted), email (encrypted), name (encrypted), idNumber (encrypted), identityType, appRole
sessionsauth.tsid, userId, tokenHash, ipAddress, userAgent
operatorscatalog.tsid, slug (stc/mobily/zain), nameEn, nameAr, serviceType, paymentMode, isNameArFallback
packagescatalog.tsid, operatorId, dataAmountMb, voiceMinutes, priceSar, priceSarB2b, isActive, isActiveCustomized, isNameCustomized, isPriceCustomized, source_*
ordersactivation.tsid, userId, companyId, status, totalAmount, currency
order_itemsactivation.tsid, orderId, packageId, iccid, quantity
activationsactivation.tsid, orderItemId, userId, operatorId, iccid, status, identityStatus
identity_verificationsactivation.tsid, activationId, transId, randomCode, status
paymentspayment.tsid, orderId, userId, amount, currency, method, status, idempotencyKey
invoicespayment.tsid, companyId, orderId, amountSar, zatcaInvoiceId
fx_ratesfx.tscurrency (PK), ratePerUsd, fetchedAt, source — deny-all RLS
ticketssupport.tsid, userId, subject, status, priority
ticket_commentssupport.tsid, ticketId, authorId, content, isInternal
companiesb2b.tsid, crNumber, name, contactName, contactEmail, contactPhone (encrypted)
company_usersb2b.tsid, companyId, userId, role
bulk_ordersb2b.tsid, companyId, orderId, totalSims, status, csvData
audit_logsaudit.tsid, actorId, action, entityType, entityId, oldValues, newValues
consent_recordsaudit.tsid, userId, consentType, version, givenAt, revokedAt

PII Encryption

Uses AES-256-GCM (authenticated encryption) via a custom Drizzle type:

ts
// Encrypted columns are transparent — read/write as plaintext
const [user] = await db.insert(users).values({
  phone: '0512345678',  // Auto-encrypted on write
  email: 'user@example.com',
  name: 'Ahmed',
  identityType: 'citizen',
  appRole: 'end_user',
}).returning();
// user.phone returns '0512345678' (auto-decrypted on read)

Key rotation: ENCRYPTION_KEY is read at call time, not at startup. During a rotation, set ENCRYPTION_KEY_PREVIOUS to the old key — decryption tries the current key first and falls back to the previous one (the GCM auth tag identifies the right key), while new writes always use the current key. Unset it once the old ciphertexts are gone — either by re-encrypting the existing rows, or by waiting for the retention window to drain them. There is no re-encryption command in the repo today, so a rewrite is a one-off task against the affected tables.

RLS Policies

Each domain exports SQL strings for Row Level Security:

ts
import { authRlsPolicies, catalogRlsPolicies, activationRlsPolicies } from '@activation-sys/database';

Internal documentation - Activation System