# Central SaaS Phase 1 Review: Postgres Persistence

**Date:** 2026-05-19  
**Status:** COMPLETE  
**VPS:** root@187.77.140.128  
**Project root:** /opt/devxura

---

## What Was Implemented

Replaced the in-memory Maps (`state.developers`, `state.licenses`, `state.runtimeNodes`) in `apps/central-app/src/server.mjs` with real Postgres persistence using the `pg` library. All existing API endpoints maintain the same HTTP contract while data now survives server restarts.

### 1. Postgres-Backed Storage For:
- **developers/accounts** — INSERT into `developers` table on signup
- **licenses** — INSERT into `licenses` table with key hash
- **trials** — INSERT into `trials` table
- **runtime nodes** — INSERT/UPDATE in `runtime_nodes` table
- **license check-ins** — Query licenses and update node timestamps
- **plan entitlements** — Read from `plan_entitlements` table (cached in memory)

### 2. Schema / Migrations
**New migration:** `packages/db/migrations/0005_central_saas_seed.sql`
- Creates `plan_entitlements` table (plan_id, max_customers, max_workspaces, max_ai_employees, max_team_members, max_runtime_nodes, white_label_basic, support_level)
- Seeds 3 plans: solo, small_agency, agency_pro
- Inserts plan records into existing `plans` table from 0001_foundation.sql

**Existing tables used (from 0001_foundation.sql):**
- `developers` (id, email, name, created_at)
- `licenses` (id, developer_id, plan_id, state, payment_state, key_hash, trial_ends_at, grace_ends_at, created_at, updated_at)
- `runtime_nodes` (id, developer_id, license_id, app_url, fingerprint, version, last_check_in_at, created_at)
- `trials` (id, developer_id, starts_at, ends_at)

### 3. Repository / Data-Access Layer
**New module:** `apps/central-app/src/db.mjs`
- Lazy singleton Postgres client using `pg.Client`
- Reads `DATABASE_URL` env var
- `getClient()` — returns connected client
- `status()` — returns `{backend: "postgres", connected: true|false}`

### 4. Validation / Error Handling
- Plan validity check on signup (returns 400 if plan doesn't exist)
- License state validation (allowed states: trialing, active, grace, expired, past_due, cancelled)
- Payment state validation (not_required_for_trial, active, past_due, failed, cancelled)
- Duplicate email constraint enforced by DB UNIQUE index
- UNIQUE constraint on `licenses.key_hash`

---

## Files Changed

| File | Change |
|------|--------|
| `apps/central-app/src/server.mjs` | REWRITTEN — Postgres-backed, 427 lines (was 216 lines) |
| `apps/central-app/src/db.mjs` | NEW — Postgres connection module, 35 lines |
| `packages/db/migrations/0005_central_saas_seed.sql` | NEW — plan_entitlements table + seed data |
| `infra/docker/Dockerfile.central-app` | NEW — Docker build for central-app |
| `infra/compose/docker-compose.prod.override.yml` | NEW — replaces mock-central with central-app |
| `infra/env/.env.runtime` | MODIFIED — DEVXURA_CENTRAL_URL changed from mock-central:4100 to central-app:4100 |

**Removed (from server.mjs):**
- In-memory `state` Map objects (developers, licenses, runtimeNodes)
- Static `planEntitlements` object (now loaded from DB)
- Synchronous `createTrialDeveloper`, `updateLicenseState`, etc. (now async)

---

## What Works Now (Verified)

All 9 verification tests passed on VPS:

1. **GET /health** → 200, storage backend: "postgres", connected: true
2. **GET /plans** → 200, 3 plans loaded from DB
3. **POST /signup** → 201, developer + trial + license created in Postgres
4. **POST /runtime-nodes/register** → 201, runtime node registered
5. **POST /runtime-nodes/check-in** (by runtimeNodeId) → 200, accepted
6. **POST /runtime-nodes/check-in** (by licenseKey) → 200, accepted
7. **PATCH /licenses/state** → 200, state transition trialing→active+payment active
8. **POST /runtime-nodes/check-in** (after activation) → 200, accepted
9. **Entitlements** → correct plan data returned (agency_pro: maxCustomers=50, supportLevel=priority)

---

## What Is Still Pending For Phase 2

- **Write permission flags** — central-app now reads entitlements from DB; Phase 2 should add per-developer feature flags
- **Payment integration** — license `payment_state` is updated via PATCH only; Phase 2 needs Stripe webhooks
- **Grace period enforcement** — `grace_ends_at` is stored but not auto-transitioned; Phase 2 needs scheduler
- **Fingerprint deduplication** — currently allows multiple nodes per license; Phase 2 should add maxRuntimeNodes enforcement
- **Email verification** — signup creates account immediately; Phase 2 should add verification flow
- **Billing/plan management UI** — Phase 2 admin panel for plan management
- **Backup/restore** — Phase 2 should add database backup automation
- **Session/auth tokens** — Phase 2 needs JWT-based auth for developer API access

---

## Blockers Found

**None.** Phase 1 is fully functional and passed all tests.
