Appearance
🔄 Synced from
castyou-backend/README.md— edit it there, not here.
castyou-backend
Node.js + TypeScript + Apollo GraphQL API for the CastYou talent marketplace.
Tech Stack
- Runtime: Node.js 20+, TypeScript (strict)
- API: Apollo Server 4 + Express
- DB: PostgreSQL (Prisma) + MongoDB (Mongoose)
- Auth: JWT + Firebase Auth (social login) + TOTP 2FA
- Storage: Cloudflare R2 (S3-compatible)
- AI: OpenAI GPT-4o +
text-embedding-3-small, vector search (Qdrant — self-hosted, same machine) - Cache / nonce store: Redis
- Identity verification: face-comparison sidecar (Python/FastAPI + dlib)
Local Development
Prerequisites
| Tool | Version | Install |
|---|---|---|
| Node.js | 20+ | nodejs.org or nvm install 20 |
| npm | 10+ | bundled with Node |
| PostgreSQL | 15+ | brew install postgresql@15 or Docker |
| MongoDB | 7+ | brew install mongodb-community or Docker |
| Redis | 7+ | brew install redis or Docker |
| Docker | any | docker.com (optional — only for the face-comparison sidecar) |
Step 1 — Clone and install
bash
git clone <repo-url> castyou-backend
cd castyou-backend
npm installStep 2 — Set up environment variables
bash
cp .env.example .envOpen .env and fill in the required values:
env
# PostgreSQL — database must exist before running migrations
DATABASE_URL=postgresql://castyou:castyou@localhost:5432/castyou_db
# MongoDB
MONGODB_URI=mongodb://localhost:27017/castyou
# JWT — generate secure values with: openssl rand -base64 48
JWT_SECRET=<min 32 chars>
REFRESH_TOKEN_SECRET=<min 32 chars>
# Redis
REDIS_URL=redis://localhost:6379
# Identity liveness verification — generate with: openssl rand -base64 48
LIVENESS_SECRET=<min 32 chars>
# Leave FACE_COMPARISON_URL unset locally — confirmLiveness will error but
# everything else (challenge creation, token validation, auth) works fine
# FACE_COMPARISON_URL=http://localhost:8001
# Firebase (needed for social login — Google, Facebook, LinkedIn)
# Get from Firebase console → Project Settings → Service Accounts → Generate new private key
# Then base64-encode the JSON: base64 -i serviceAccount.json | tr -d '\n'
FIREBASE_SERVICE_ACCOUNT_KEY=<base64-encoded service account JSON>Everything else in .env.example is optional for basic local development.
Step 3 — Create the PostgreSQL database
bash
createdb castyou_dbOr with a dedicated user:
bash
psql -U postgres -c "
CREATE DATABASE castyou_db;
CREATE USER castyou WITH PASSWORD 'castyou';
GRANT ALL PRIVILEGES ON DATABASE castyou_db TO castyou;
"Step 4 — Run database migrations
bash
npm run db:migrate
# Applies all pending Prisma migrations and regenerates the Prisma clientStep 5 — Start the dev server
bash
npm run dev
# Hot-reload via tsx watch — restarts on every file saveGraphQL playground: http://localhost:4000/graphql
Step 6 — (Optional) Create an admin user
bash
npm run create-admin
# Follow the prompts to set email + passwordStep 7 — (Optional) Seed test data
Creates one account of every role type plus sample jobs, an application, and a pending report — enough data to exercise every major UI surface without clicking through the app manually.
bash
pnpm seed:test # idempotent: safe to run multiple times
pnpm seed:test:clean # wipes all seed accounts first, then re-seeds from scratchTest credentials (all accounts share the same password):
| Role | Password | |
|---|---|---|
| Admin | admin@seed.castyou.dev | Test1234! |
| Talent | talent@seed.castyou.dev | Test1234! |
| Producer | producer@seed.castyou.dev | Test1234! |
| Agency | agency@seed.castyou.dev | Test1234! |
| Pet Owner | petowner@seed.castyou.dev | Test1234! |
What else gets seeded:
- Talent profile — Alex Rivera, Actor, Los Angeles, SAG-AFTRA, ~80% complete
- Producer profile — Horizon Films, with 3 open jobs and 1 closed job
- Application — Talent applied to the first open job (status:
PENDING) - Report — one pending report from Talent against Producer (visible in
/admin/reports)
All seed accounts use the email suffix @seed.castyou.dev. The --clean flag finds and deletes only accounts with that suffix — real users are never touched.
Step 8 — (Optional) Run the face-comparison sidecar
Only needed to test the full identity verification flow end-to-end.
bash
cd ../face-comparison
docker build -t castyou-face-comparison .
# First build takes 10–20 min (dlib compiles from source)
docker run -d --name face-comparison -p 8001:8001 castyou-face-comparisonThen add to your .env:
env
FACE_COMPARISON_URL=http://localhost:8001Scripts
| Command | Description |
|---|---|
npm run dev | Dev server with hot reload (tsx watch) |
npm run build | Compile TypeScript to dist/ |
npm start | Run compiled server (production) |
npm run typecheck | TypeScript type check (no emit) |
npm test | Run all tests with Vitest (watch mode) |
npm test -- --run | Run tests once (no watch) |
npm run db:migrate | Apply pending Prisma migrations (dev — interactive) |
npm run db:deploy | Apply migrations for production (no prompt, safe for CI) |
npm run db:generate | Regenerate Prisma client after a schema change |
npm run db:studio | Open Prisma Studio at localhost:5555 |
npm run db:seed | Seed the database with fixture data |
pnpm seed:test | Seed one account per role + sample jobs, application, and report |
pnpm seed:test:clean | Wipe existing seed accounts then re-seed from scratch |
npm run create-admin | Create (or update) a named admin account |
Environment Variables Reference
| Variable | Required | Default | Description |
|---|---|---|---|
NODE_ENV | no | development | development / test / production |
PORT | no | 4000 | HTTP server port |
DATABASE_URL | yes | — | PostgreSQL connection string |
MONGODB_URI | no | — | MongoDB connection string |
JWT_SECRET | yes | — | HS256 signing key (min 32 chars) |
JWT_EXPIRES_IN | no | 7d | Access token lifetime |
REFRESH_TOKEN_SECRET | yes | — | Refresh token signing key (min 32 chars) |
REFRESH_TOKEN_EXPIRES_IN | no | 30d | Refresh token lifetime |
REDIS_URL | no | redis://localhost:6379 | Redis connection URL |
LIVENESS_SECRET | yes | dev default | HMAC key for liveness challenge tokens (min 32 chars) |
FACE_COMPARISON_URL | no | http://face-comparison:8001 | URL of the face-comparison sidecar |
FIREBASE_SERVICE_ACCOUNT_KEY | no | — | Base64-encoded Firebase service account JSON |
AWS_REGION | no | us-east-1 | Cloudflare R2 / S3 region (use auto for R2) |
AWS_ACCESS_KEY_ID | no | — | R2 / S3 access key |
AWS_SECRET_ACCESS_KEY | no | — | R2 / S3 secret key |
AWS_S3_BUCKET | no | castyou-media | Bucket name |
OPENAI_API_KEY | no | — | GPT-4o (flier generation); embeddings only when EMBEDDINGS_PROVIDER=openai |
EMBEDDINGS_PROVIDER | no | fastembed | fastembed (local ONNX, no key) | openai | none — see docs/INFRASTRUCTURE.md |
QDRANT_URL | no | — | Qdrant vector store (semantic search, AI matching, suggestions); unset = keyword-only |
CORS_ORIGINS | no | http://localhost:3000 | Comma-separated allowed origins |
Production / Staging on Coolify (Hetzner)
Production runs on a single Hetzner CX22 VPS (2 vCPU / 4 GB RAM) managed by Coolify (self-hosted PaaS). All services run as Docker containers on the same host and communicate over an internal Docker network — nothing is exposed to the internet except through Traefik.
Infrastructure overview
| Service | Coolify type | Internal hostname |
|---|---|---|
| Express backend | Application (Dockerfile) | backend |
| PostgreSQL 15 | Database | postgres |
| MongoDB 7 | Database | mongodb |
| Redis 7 | Database | redis |
| Qdrant | Application (Docker image) | qdrant |
| Face-comparison sidecar | Application (Dockerfile) | face-comparison |
| Traefik (reverse proxy + SSL) | auto-managed by Coolify | — |
Step 1 — Install Coolify on the VPS
bash
# SSH into the Hetzner server
ssh root@<server-ip>
# One-command install (~5 min)
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bashOpen the Coolify dashboard at http://<server-ip>:8000 and complete the setup wizard.
Step 2 — Provision the databases
In Coolify: Resources → New → Database, create three resources:
- PostgreSQL 15 — note the connection string (internal hostname:
postgres) - MongoDB 7 — note the connection string (internal hostname:
mongodb) - Redis 7 — note the URL (e.g.
redis://redis:6379)
Enable scheduled backups for PostgreSQL and MongoDB under each resource's Backups tab (target: Cloudflare R2).
Step 3 — Deploy the backend
- Resources → New → Application → connect your GitHub repo, select the
castyou-backenddirectory - Set Build pack to
Dockerfile - Set the public Domain to
api.castyou.com— Coolify provisions a Let's Encrypt certificate automatically - Under Environment Variables, add all required values (use internal Docker hostnames for DB URLs):
env
NODE_ENV=production
DATABASE_URL=postgresql://castyou:<password>@postgres:5432/castyou_db
MONGODB_URI=mongodb://castyou:<password>@mongodb:27017/castyou
REDIS_URL=redis://redis:6379
JWT_SECRET=<openssl rand -base64 48>
REFRESH_TOKEN_SECRET=<openssl rand -base64 48>
LIVENESS_SECRET=<openssl rand -base64 48>
FACE_COMPARISON_URL=http://face-comparison:8001
FIREBASE_SERVICE_ACCOUNT_KEY=<base64 JSON>
AWS_REGION=auto
AWS_ACCESS_KEY_ID=<Cloudflare R2 access key>
AWS_SECRET_ACCESS_KEY=<Cloudflare R2 secret>
AWS_S3_BUCKET=castyou-media
OPENAI_API_KEY=<key>
QDRANT_URL=http://qdrant:6333
CORS_ORIGINS=https://app.castyou.com,https://castyou.com- Click Deploy.
Step 4 — Run migrations on first deploy
Migrations run automatically via npm run db:deploy during container startup. To run them manually from the Coolify terminal:
bash
npm run db:deploy
# prisma migrate deploy — applies pending migrations, no interactive promptsStep 5 — Deploy the face-comparison sidecar
- Resources → New → Application → same GitHub repo, select the
face-comparisondirectory - Build pack:
Dockerfile - No public domain — set only the internal hostname:
face-comparison - Add environment variable:
env
FACE_DISTANCE_THRESHOLD=0.5- Deploy. First build takes 10–20 minutes — dlib compiles from source inside the container.
RAM note: Qdrant uses ~200 MB idle, the face-comparison sidecar ~300 MB. Estimated total on CX22 is ~2.3–2.6 GB of 4 GB. Upgrade to CX32 (~€13/mo) when usage consistently exceeds 3 GB.
Step 6 — Create the first admin user
From the Coolify terminal for the backend container:
bash
npm run create-adminSubsequent deployments
Coolify redeploys automatically on every push to the configured branch (main). The sequence is:
- Pulls the latest commit and builds a new Docker image
prisma migrate deployruns inside the new container before traffic switches- Container swap — typically ~5–10 s of downtime
To trigger a manual redeploy: Coolify dashboard → your app → Redeploy.
Project Structure
src/
├── config/ # Env validation, DB clients (Prisma, Redis), GraphQL context
├── graphql/ # Schema SDL + resolvers (one file per domain)
├── models/ # Mongoose schemas (MongoDB)
├── services/ # Business logic (auth, talent, producer, job, media, AI)
├── middleware/ # Auth guards, rate limiter
└── utils/ # Logger, helpers
prisma/
├── schema.prisma # PostgreSQL schema
└── migrations/ # Migration history (committed — never edit manually)
../face-comparison/ # Python/FastAPI face-comparison sidecar (sibling directory)Documentation
- Architecture — system design, tech decisions, request lifecycle
- Patterns — resolver pattern, service layer, error handling, conventions
- Infrastructure — Hetzner + Coolify full setup details