Getting Started
Prerequisites
- Node.js 22 LTS (enforced by
@types/node: ^22.0.0in 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
# 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 devNote: Use
pnpm db:migratefor proper migration execution, thenpnpm db:verifyagainst the live Supabase database.pnpm db:pushis intentionally blocked because it bypasses migration files, custom SQL, RLS policies, and Realtime publication setup.
Available Commands
| Command | Description |
|---|---|
pnpm dev | Start all apps in dev mode (Turborepo) |
pnpm build | Build all packages and apps |
pnpm lint | Run Biome linter/formatter |
pnpm format | Format code with Biome |
pnpm check | Run TypeScript type checking across workspaces |
pnpm db:generate | Generate Drizzle migrations from schema |
pnpm db:migrate | Run Drizzle + custom SQL migrations |
pnpm db:verify | Verify live Supabase RLS, custom migrations, triggers, and Realtime setup |
ALLOW_DB_PUSH=1 pnpm db:push:unsafe | Local throwaway schema push only; never use for shared databases |
Development Server Details
| Service | Port | URL |
|---|---|---|
| API (Hono) | 3001 | http://localhost:3001 |
| API Docs (Scalar) | 3001 | http://localhost:3001/docs |
| OpenAPI Spec (JSON) | 3001 | http://localhost:3001/openapi.json |
| Admin/B2B Dashboard (Next.js) | 3000 | http://localhost:3000 |
| B2C Web (Next.js) | 3002 | http://localhost:3002 |
| Landing Site (Vite) | 3003 | http://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
curl http://localhost:3001/health{
"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
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):
{
"userId": "a1b2c3d4-...",
"message": "OTP sent"
}3. Verify OTP
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
curl http://localhost:3001/operators{
"data": [
{
"id": "uuid",
"slug": "stc",
"nameEn": "STC",
"nameAr": "الاتصالات السعودية",
"logoUrl": null,
"packageCount": 0,
"createdAt": "...",
"updatedAt": "..."
}
]
}5. List Packages
# All packages
curl http://localhost:3001/packages
# Filtered by operator and minimum data
curl "http://localhost:3001/packages?operatorId=uuid&dataMinMb=5000"{
"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:
| Variable | Port | Purpose |
|---|---|---|
DATABASE_URL | 6543 (pooler) | Runtime queries — uses Supabase connection pooler (pgbouncer transaction mode) |
DATABASE_URL_DIRECT | 5432 (direct) | Migrations — requires a direct TCP connection (not through the pooler) |
Both URLs are defined in the root .env.example:
# 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/postgresWhy 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 frompackages/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. UseALLOW_DB_PUSH=1 pnpm db:push:unsafeonly 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
docker compose up -dThis 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:
{
"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:
PORT=3002 pnpm devOr 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 URLSUPABASE_URL— must be a valid URLSUPABASE_ANON_KEYandSUPABASE_SERVICE_ROLE_KEY— must be non-empty stringsENCRYPTION_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 withdocker compose up -d."database": "error"— Database connection failed. VerifyDATABASE_URLcredentials 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
| Phase | Scope | Status |
|---|---|---|
| Phase 1 | Infrastructure, DB, queues, health check | Done |
| Phase 2 | Auth (register, login, OTP, session, logout, profile) | Done |
| Phase 3 | Operators + Packages catalog (list, search, compare, availability) | Done |
| Phase 4 | SIM Activation + Identity Verification (ICCID validation, Nafath) | Done |
| Phase 5 | Payments (card intents, Apple Pay, STC Pay, Stripe webhooks, receipts) | Done |
| Phase 6 | Profile + Compliance (profile export, account deletion, retention) | Done |
Frontend workspace commands
| Command | Purpose |
|---|---|
pnpm dev:admin | Run only the admin/B2B Next.js app on port 3000 |
pnpm dev:web-b2c | Run only the B2C web Next.js app on port 3002 |
pnpm dev:site | Run only the Vite landing site on port 3003 |
pnpm build:web-b2c | Build the B2C web app through its workspace script |
pnpm build:site | Typecheck and build the landing site |
See Applications for the full app map.