Marketing website + operations backend: a sales pipeline, a management dashboard, and an authenticated API your Trinity AI (CoAIleague) connects to for automated outreach (email/SMS/phone) and lead orchestration. Hosted on Railway.
Runtime note:
mainis the live Node/Express + Drizzle + Postgres spine. The old branch-note aboutfeature/spine-backendis stale.
public/ (static site + /admin dashboard)
│ contact form → POST /api/v1/ingress/lead
│ careers form → POST /api/v1/ingress/apply (multipart résumé)
▼
server/ (Express + TypeScript)
├─ lib/coerce.ts Zod normalizers (data-coercion checkpoint)
├─ lib/auth.ts bcrypt + signed-cookie sessions
├─ routes/ingress.ts public form intake → Postgres + email notify
├─ routes/admin.ts management dashboard API (login-guarded)
├─ routes/api/v1/trinity.ts Trinity bridge (HMAC-signed; see below)
├─ utils/storage.ts file storage — CoAIleague bridge canonical; local dev-only (prod-forbidden)
└─ db/schema.ts operational tables (Drizzle)
▼
Railway Postgres — SPS operational spine (shifts/time/clients/officers/money DRAFTS)
Authority split (read CLAUDE.md): SPS is the spine (operational source of truth
for shifts, time, clients, officers, and money drafts). CoAIleague is the brain and the
canonical platform authority for money (Stripe), identity/entitlement, and metering — SPS
moves no money; it PREPARES + SIGNALS over the signed bridge. "Single source of truth" applies
per-domain, not globally.
Tables: users · leads (pipeline) · activities (outreach log) ·
applications · incident_logs · schedules.
Sign-in protected console for the team:
- Sales pipeline — leads by stage (new → contacted → qualified → won/lost), inline stage changes.
- Needs-contact queue — leads overdue for follow-up (or never contacted).
- Applications inbox — applicants, status, and résumé download (auth-gated).
- Activity log — per-lead timeline of every email/SMS/call, including what Trinity did.
Admin bootstrap (create-if-absent): on first boot, if no admin exists, one is created
from ADMIN_EMAIL / ADMIN_PASSWORD. After that the password is managed in-app — a
redeploy will not overwrite it (a UI password change is never reverted, and the env var
is not a standing credential). To recover a locked-out single-tenant owner, set the explicit
break-glass ADMIN_PASSWORD_FORCE_SYNC=true, redeploy once, then unset it.
Auth — HMAC-signed bridge. Inbound CoAIleague→SPS calls are verified by the trinityAuth
middleware (HMAC over the exact body via verifyBridge; contract in
server/lib/bridgeSecurity.ts). A bearer Authorization: Bearer <TRINITY_API_KEY> is accepted
in additive mode during rollout, but production should run TRINITY_REQUIRE_SIGNATURE=true
(a.k.a. BRIDGE_REQUIRE_LOCKDOWN) so unsigned calls are refused — and outbound emit
(TRINITY_INGEST_URL set) hard-fails at boot without a signing secret. Do not rely on
bearer-only in production.
GET /api/v1/trinity/sync→{ pipeline[], needsContact[], newApplications[], openSchedules[], knowledgeBase, counts }.knowledgeBasecarries the pricing engine, guard capabilities, and contract rules (server-side constants inserver/constants/) so Trinity can quote consistently. Pipeline stages:new → contacted → ready_to_close → qualified → won/lost; settingready_to_closeemails a hot-lead alert to the team.POST /api/v1/trinity/action→ one of:
log_activity stamps lastContactedAt and advances a new lead to contacted.
npm install
cp .env.example .env # set DATABASE_URL, SESSION_SECRET, ADMIN_PASSWORD, TRINITY_API_KEY
npm run dev # applies idempotent migrations on boot; creates the admin if absentnpm run typecheck · npm run build · npm run db:generate.
| Var | Required | Purpose |
|---|---|---|
DATABASE_URL |
✅ | Railway Postgres (SPS operational spine) |
SESSION_SECRET |
✅ (prod) | signs dashboard login cookies (openssl rand -hex 32) |
ADMIN_EMAIL / ADMIN_PASSWORD |
✅ | bootstrap only — creates the admin if absent; not a standing credential (see Admin bootstrap) |
ADMIN_PASSWORD_FORCE_SYNC |
break-glass | true force-resets the existing admin's password from ADMIN_PASSWORD on one boot; leave unset |
TRINITY_API_KEY |
✅ | bridge key; also the HMAC signing secret unless TRINITY_SIGNING_SECRET is set |
TRINITY_REQUIRE_SIGNATURE |
✅ (prod) | true refuses unsigned bridge calls (production posture) |
SITE_ID |
✅ (bridge) | canonical tenant identity stamped on bridge events; UUID sites must set TRINITY_BILLING_PLAN |
STORAGE_PROVIDER |
✅ (prod) | coaileague (canonical) or s3/R2. local is refused in production — dev only |
RESEND_API_KEY |
optional | email alerts (else logged) |
PORT |
auto | Railway injects it |
- Add a Postgres plugin → provides
DATABASE_URL. - Set
SESSION_SECRET,ADMIN_PASSWORD,TRINITY_API_KEY,SITE_ID,STORAGE_PROVIDER(coaileague/s3), and the production bridge postureTRINITY_REQUIRE_SIGNATURE=true. - Deploy
main— theDockerfilebuilds the Node app, applies idempotent migrations viaensureSchema()on boot, and servespublic/natively (clean URLs). Migration execution is not readiness:/readyzfail-closes (503) until schema + required indexes + bootstrap are actually present, so a partial migration can't serve traffic;/livezis liveness only. - Verify
GET /livez(live) thenGET /readyz(ready), sign into/admin, then connect Trinity to/api/v1/trinity/sync.
- Backup: the SPS operational database (Railway Postgres) must have a verified, restorable
backup before any destructive migration — see
docs/go-live/RETIREMENT_PROOF_PACKETS.md. - Rollback: roll back by redeploying the prior
mainSHA. Migrations are additive + idempotent (Law 6), so a redeploy of an earlier SHA does not require a down-migration; a destructive change is owner-gated (docs/go-live/SPS_OWNER_GATES.md) and needs a restore path.
{ "type":"create_lead", "clientName":"…", "email":"…", "industryType":"hotel|construction|retail|hospital|clinic|data_center|bar_nightclub|apartment|hoa|other", "guardTypeRequired":"…" } // Trinity recommendation { "type":"log_activity", "leadId":"<uuid>", "channel":"call|email|sms|note", "body":"…", "nextFollowupAt":"ISO" } { "type":"set_followup", "leadId":"<uuid>", "nextFollowupAt":"ISO" } { "type":"update_lead_status", "id":"<uuid>", "status":"contacted|qualified|won|lost" } { "type":"update_application_status", "id":"<uuid>", "status":"screening|interview|hired|rejected" } { "type":"create_incident", "location":"…", "description":"…", "severity":"low|medium|high|critical" } { "type":"create_schedule", "postLocation":"…", "startTime":"ISO", "endTime":"ISO" }