Skip to content

Getting Started ​

Prerequisites ​

  • Node.js 22 LTS (enforced by @types/node: ^22.0.0 in API devDependencies)
  • pnpm 10+ (npm install -g pnpm)
  • PostgreSQL 15+ (via Supabase account — no local Postgres needed)
  • Redis 7+ (provided via docker compose; required for BullMQ queues)
  • Flutter 3.41.x (for mobile app development only)
  • Docker (for Redis and any other local infrastructure)

Quick Start ​

bash
# 1. Clone and install
git clone <repo-url> activation-sys
cd activation-sys
pnpm install

# 2. Set up environment variables
cp .env.example .env
# Edit .env with your Supabase credentials, Redis URL, encryption key, etc.

# 3. Start infrastructure (Redis)
docker compose up -d

# 4. Generate, apply, and verify database migrations
pnpm db:generate
pnpm db:migrate
pnpm db:verify

# 5. Start development servers
pnpm dev

Note: Use pnpm db:migrate for proper migration execution, then pnpm db:verify against the live Supabase database. pnpm db:push is intentionally blocked because it bypasses migration files, custom SQL, RLS policies, and Realtime publication setup.

Available Commands ​

CommandDescription
pnpm devStart all apps in dev mode (Turborepo)
pnpm buildBuild all packages and apps
pnpm lintRun Biome linter/formatter
pnpm formatFormat code with Biome
pnpm checkRun TypeScript type checking across workspaces
pnpm db:generateGenerate Drizzle migrations from schema
pnpm db:migrateRun Drizzle + custom SQL migrations
pnpm db:verifyVerify live Supabase RLS, custom migrations, triggers, and Realtime setup
ALLOW_DB_PUSH=1 pnpm db:push:unsafeLocal throwaway schema push only; never use for shared databases

Development Server Details ​

ServicePortURL
API (Hono)3001http://localhost:3001
API Docs (Scalar)3001http://localhost:3001/docs
OpenAPI Spec (JSON)3001http://localhost:3001/openapi.json
Admin/B2B Dashboard (Next.js)3000http://localhost:3000
B2C Web (Next.js)3002http://localhost:3002
Landing Site (Vite)3003http://localhost:3003

The API port defaults to 3001 (configured in apps/api/src/config/env.ts via PORT: z.coerce.number().default(3001)). Admin runs on 3000, B2C web runs on 3002, and the landing site runs on 3003 so pnpm dev can run all Turbo workspaces without frontend port collisions.

First API Call ​

After starting the dev server, verify everything works with these curl examples:

1. Health Check ​

bash
curl http://localhost:3001/health
json
{
  "status": "ok",
  "version": "1.0.0",
  "timestamp": "2026-05-05T08:00:00.000Z",
  "checks": {
    "database": "ok",
    "redis": "ok"
  }
}

If Redis is unavailable, the health check returns 503 with "status": "degraded".

2. Register a User ​

Registration creates a customer account, and customers sign in only from the mobile client, so these calls send X-Client-Type: mobile (see the Client Rule in docs/api/auth.md).

bash
curl -X POST http://localhost:3001/auth/register \
  -H "Content-Type: application/json" \
  -H "X-Client-Type: mobile" \
  -d '{
    "phone": "+966501234567",
    "name": "Test User",
    "consents": [
      {"type": "terms_of_service", "version": "1.0"},
      {"type": "privacy_policy", "version": "1.0"},
      {"type": "data_processing", "version": "1.0"}
    ]
  }'

Response (201):

json
{
  "userId": "a1b2c3d4-...",
  "message": "OTP sent"
}

3. Verify OTP ​

bash
curl -X POST http://localhost:3001/auth/verify-otp \
  -H "Content-Type: application/json" \
  -H "X-Client-Type: mobile" \
  -d '{
    "phone": "+966501234567",
    "otp": "123456"
  }'

In development with SMS_PROVIDER=mock, OTP codes are logged to the console. The response includes a JWT token for subsequent authenticated requests.

4. List Operators ​

bash
curl http://localhost:3001/operators
json
{
  "data": [
    {
      "id": "uuid",
      "slug": "stc",
      "nameEn": "STC",
      "nameAr": "الاتصالات السعودية",
      "logoUrl": null,
      "packageCount": 0,
      "createdAt": "...",
      "updatedAt": "..."
    }
  ]
}

5. List Packages ​

bash
# All packages
curl http://localhost:3001/packages

# Filtered by operator and minimum data
curl "http://localhost:3001/packages?operatorId=uuid&dataMinMb=5000"
json
{
  "data": [
    {
      "id": "uuid",
      "operatorId": "uuid",
      "name": "5GB Package",
      "description": "5GB data, 100 minutes",
      "dataAmountMb": 5120,
      "voiceMinutes": 100,
      "smsCount": 50,
      "validityDays": 30,
      "priceSar": 50,
      "isPromotional": false,
      "createdAt": "...",
      "updatedAt": "..."
    }
  ],
  "total": 1
}

Database Setup Details ​

The project uses two connection URLs for Supabase PostgreSQL:

VariablePortPurpose
DATABASE_URL6543 (pooler)Runtime queries — uses Supabase connection pooler (pgbouncer transaction mode)
DATABASE_URL_DIRECT5432 (direct)Migrations — requires a direct TCP connection (not through the pooler)

Both URLs are defined in the root .env.example:

bash
# Pooler URL (port 6543) — for API runtime queries
DATABASE_URL=postgresql://postgres.[ref]:[password]@aws-0-eu-west-1.pooler.supabase.com:6543/postgres

# Direct URL (port 5432) — for drizzle-kit migrate
DATABASE_URL_DIRECT=postgresql://postgres.[ref]:[password]@aws-0-eu-west-1.pooler.supabase.com:5432/postgres

Why two URLs? drizzle-kit migrate requires a direct Postgres connection to apply DDL statements. The Supabase pooler (port 6543) uses transaction-mode pooling which does not support DDL operations. The migration script (packages/database/src/migrate.ts) falls back to DATABASE_URL if DATABASE_URL_DIRECT is not set, but this will fail on Supabase pooler connections.

Database migration commands:

  • pnpm db:migrate — Runs the full migration pipeline: Drizzle kit migrations + custom SQL migrations from packages/database/drizzle/custom/. This is the required command for real environments.
  • pnpm db:verify — Connects to the live database and verifies RLS flags, expected policies, custom migration hashes, Supabase Realtime publication, required triggers, and PostgreSQL extensions.
  • pnpm db:push — Blocked by design. Use ALLOW_DB_PUSH=1 pnpm db:push:unsafe only for local throwaway prototyping where losing migration history is acceptable.

Redis Setup ​

Redis backs nine queues: activation, payment, store fulfillment, package sync, eSIM product sync, FX refresh, notification email, notification SMS, and notification orchestration. It also supports OTP storage and token revocation checks. HTTP rate-limit counters stay in API memory. Activation Realtime updates remain independent of notification delivery.

Using Docker Compose ​

bash
docker compose up -d

This starts Redis 7.x on port 6379 with persistence enabled (appendonly yes).

Without Redis ​

The API starts and handles requests even if Redis is unavailable. The health check endpoint returns HTTP 503 with "status": "degraded" when Redis is down:

json
{
  "status": "degraded",
  "version": "1.0.0",
  "timestamp": "...",
  "checks": {
    "database": "ok",
    "redis": "error"
  }
}

Queue-dependent work cannot run without Redis. The API starts activation, payment, store, notification email/SMS, and orchestration workers after Redis connects outside test mode. HTTP listening does not wait for those workers. Startup failures are logged, and a partial worker start rolls back workers and notification producers. If the initial Redis connection fails, this path does not automatically start workers when connectivity returns. Restart the API after restoring Redis and verify job processing rather than relying on HTTP health alone.

Notification First Run ​

The three notification queues are notification-email, notification-sms, and notification-orchestration. They replace the former shared stub and API-local status lane. Startup injects the renderer, locale resolver, provider dispatch, and orchestration hooks, then schedules an immediate delivery handoff sweep and a recurring run every minute. No separate notification worker command is required for the normal API host.

Email defaults to the mock until SMTP is selected through the email plugin settings. Notification SMS stays on the mock even if OTP uses another provider. Push is disabled. In-app delivery is persisted directly, and activation Realtime updates do not depend on notification queues.

Use a super-admin account to submit a notification and inspect its batch and delivery counts. A 202 response confirms acceptance, not provider delivery. A finished batch can still have queued deliveries. Persisted deliveries can survive a missed enqueue and be picked up by reconciliation, but the sweep does not recover every unfinished batch or callback. See notification queues for the exact limits and notification testing for targeted checks.

Common Issues ​

Port Already in Use ​

If port 3001 is taken, set the PORT environment variable:

bash
PORT=3002 pnpm dev

Or add PORT=3002 to your .env file. The API reads PORT from env with a default of 3001 (see apps/api/src/config/env.ts).

Environment Validation Failure ​

The app crashes at startup if required environment variables are missing or invalid. The validation is done in apps/api/src/config/env.ts using Zod schemas. Common missing variables:

  • DATABASE_URL — must be a valid URL
  • SUPABASE_URL — must be a valid URL
  • SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY — must be non-empty strings
  • ENCRYPTION_KEY — must be exactly 64 characters (32-byte hex for AES-256)
  • SUPABASE_JWT_SECRET — must be non-empty

Error messages indicate which variable failed validation.

Health Check Returns 503 ​

A 503 status means at least one dependency check failed:

  • "redis": "error" — Redis is not running. Start it with docker compose up -d.
  • "database": "error" — Database connection failed. Verify DATABASE_URL credentials and network access.

Database Migration Fails with Connection Error ​

If pnpm db:migrate or pnpm db:verify fails on Supabase, ensure DATABASE_URL_DIRECT is set with the direct connection URL (port 5432, not the pooler port 6543). The pooler does not support DDL operations required by migrations.

Project Phases ​

PhaseScopeStatus
Phase 1Infrastructure, DB, queues, health checkDone
Phase 2Auth (register, login, OTP, session, logout, profile)Done
Phase 3Operators + Packages catalog (list, search, compare, availability)Done
Phase 4SIM Activation + Identity Verification (ICCID validation, Nafath)Done
Phase 5Payments (card intents, Apple Pay, STC Pay, Moyasar and Paymob webhooks, receipts)Done
Phase 6Profile + Compliance (profile export, account deletion, retention)Done

Frontend workspace commands ​

CommandPurpose
pnpm dev:adminRun only the admin/B2B Next.js app on port 3000
pnpm dev:web-b2cRun only the B2C web Next.js app on port 3002
pnpm dev:siteRun only the Vite landing site on port 3003
pnpm build:web-b2cBuild the B2C web app through its workspace script
pnpm build:siteTypecheck and build the landing site

See Applications for the full app map.

Internal documentation - Activation System