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

bash
curl -X POST http://localhost:3001/auth/register \
  -H "Content-Type: application/json" \
  -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" \
  -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 is required for BullMQ job queues (package sync, status notifications) and runtime features (rate limiting, OTP storage, session caching).

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 features (package sync, status notifications) will fail silently. Tests that require Redis skip gracefully when the connection is unavailable.

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, Stripe 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