Infrastructure Reference
Infrastructure Reference
Section titled “Infrastructure Reference”Diátaxis: Reference + How-to
Sources: CLAUDE.md § Infrastructure, docs/infra-comparison.md
Production Topology
Section titled “Production Topology”| Component | Details |
|---|---|
| Server | Hetzner CAX11 ARM64, ~€5/mo |
| IP | 178.104.154.74 |
| Platform | Dokploy (self-hosted PaaS) |
| OS | Cloud-init via Pulumi TypeScript IaC |
| App container | ideony-api (Docker, --network host, port 3001) |
| Database | PostgreSQL 18 + PostGIS, localhost:5432, DB ideony |
| Cache | Redis 8.6, localhost:6379 |
| Storage | Cloudflare R2 (S3-compatible, @aws-sdk/client-s3) |
| SSH | ssh -i ~/.ssh/id_ed25519_ideony-deploy root@178.104.154.74 |
Dokploy Services
Section titled “Dokploy Services”| Service | Image |
|---|---|
| PostgreSQL + PostGIS | imresamu/postgis:17-3.5 |
| Redis | redis:8.6-alpine |
| NestJS API | Custom Docker build |
| Expo Web (static) | nginx served |
Domain / Public URLs
Section titled “Domain / Public URLs”| Context | URL |
|---|---|
| FE (canonical, post PR merge) | https://app.ideony.is-a.dev |
| BE (canonical, post PR merge) | https://api.ideony.is-a.dev |
| FE (interim Quick Tunnel) | https://*.trycloudflare.com (rotates on restart) |
| Direct FE | http://178.104.154.74 |
| Direct BE | http://178.104.154.74:3001 |
CF zone: ideony.is-a.dev (account 1490476cf83cc346d25dc22c3de03544; status pending until PR #36614 merges)
Tunnel systemd services on prod: cf-tunnel-fe + cf-tunnel-be (Quick Tunnels) — replace with cloudflared.service named tunnel ideony-prod after zone activates.
GH Actions vars to update after named tunnel goes live: PROD_FE_URL, PROD_BE_URL, EXPO_PUBLIC_API_URL, EXPO_PUBLIC_API_URL_STAGING
CI/CD Pipeline
Section titled “CI/CD Pipeline”Repo: aciDrums7/ideony (GitHub)
⚠️ Status 2026-04-20 — Cloud CI/CD temporarily disabled. GitHub Actions minutes exhausted; all 7 workflows (
CI,CD,docs-check,Nightly E2E,Preview,Update Visual Baselines,Visual Diff) disabled viagh workflow disable. Authoritative gate is now local:scripts/verify-local.sh+scripts/deploy.sh(GHCR build + Dokploy trigger + smoke test). Re-enable when billing resolved:Terminal window for wf in CD CI docs-check "Nightly E2E" Preview "Update Visual Baselines" "Visual Diff"; do gh workflow enable "$wf"; doneLocal gate
scripts/verify-local.shadditionally runs a conditional web-build smoke step (pnpm --filter @ideony/mobile build:web) when mobile sources (apps/mobile/app/,apps/mobile/src/,app.json,metro.config,tailwind.config) change. Skippable viaSKIP_WEB_BUILD=1. Catches Metro bundler errors + bad imports that unit tests miss.Docs-gate still enforced locally via Husky +
scripts/check-docs-updated.sh. Repo policy unchanged:--no-verifyremains prohibited. Hard rules (auto-enforced since 2026-04-20): workflow/Dockerfile/compose/deploy-script changes requiredocs/infrastructure.md;apps/mobile/lib/colors*/fonts*/design-tokens changes requiredocs/design-system.md;prisma/schema.prismarequiresdocs/architecture.md. Test harness:scripts/check-docs-updated.test.sh(26 cases).
CI (all branches)
Section titled “CI (all branches)”- Lint (Biome)
- Typecheck
- Build
- Unit tests (with Postgres + Redis GitHub Actions services)
- SAST + audit
CD (triggered after CI on main)
Section titled “CD (triggered after CI on main)”- SSH into prod server
- Docker build on server
- Prisma migrate
- Blue-green swap
- Health check
- Rollback on failure
- Trivy scan
- Smoke tests —
curl /health+curl /+ Playwright post-CD prod smoke (E2E M2, track A). Non-zero exit aborts deploy with rollback hint. Config:e2e/playwright.prod-smoke.config.ts. Authed smoke expansion deferred to M3+ pending prod Clerk test-user seeding. - Auto-tag release
Configured Secrets
Section titled “Configured Secrets”DEPLOY_SSH_KEY, PROD_DATABASE_URL, PROD_ENV_VARS (20 env vars)
Turborepo Remote Cache
Section titled “Turborepo Remote Cache”Provider: Vercel. Cached tasks: build, typecheck, lint, test. Auto-auth locally via ~/Library/Application Support/turborepo/config.json. CI uses TURBO_TOKEN + TURBO_TEAM GH secrets. Team ID in .turbo/config.json (gitignored). Cache expires after 30 days — purge via Vercel dashboard → Remote Cache → Clear.
Docker
Section titled “Docker”Build + Run (production pattern)
Section titled “Build + Run (production pattern)”# Build (from repo root)docker build -t ideony-api -f apps/api/Dockerfile .
# Rundocker run --network host --env-file .env ideony-apiVerify Locally Before Pushing
Section titled “Verify Locally Before Pushing”docker build -t ideony-api . \ && docker run --env-file apps/api/.env -p 3000:3000 --name ideony-test -d ideony-api \ && docker logs ideony-test \ && curl http://localhost:3000/health \ && docker rm -f ideony-testImage Hygiene
Section titled “Image Hygiene”scripts/deploy.sh prunes superseded ghcr.io/acidrums7/ideony-{api,web}:<sha> tags after each deploy — only the current SHA and :latest survive. Also runs docker image prune -f + docker builder prune --filter until=72h to reclaim dangling layers + old build cache. Run manually any time:
docker image prune -fdocker builder prune -f --filter until=72hLocal Development
Section titled “Local Development”Services
Section titled “Services”pnpm docker:up # Postgres (5433), Redis (6379), MinIO (9000/9001), Mailpit (1025/8025)| Service | Local port |
|---|---|
| PostgreSQL | 5433 |
| Redis | 6379 |
| MinIO (S3) | 9000 / 9001 |
| Mailpit (SMTP) | 1025 / 8025 |
Storage
Section titled “Storage”Local: MinIO (S3-compatible). Production: Cloudflare R2.
Common Commands
Section titled “Common Commands”pnpm dev # all apps, dev modepnpm build # build allpnpm test # all testspnpm lint # Biome lint + format checkpnpm lint:fix # auto-fix lint + formatcd apps/api && pnpm prisma migrate dev # run migrationscd apps/api && pnpm prisma generate # regenerate Prisma clientpnpm -w run generate:sdk # build API → OpenAPI spec → typed SDKDatabase Access
Section titled “Database Access”Production (SSH Tunnel)
Section titled “Production (SSH Tunnel)”autossh tunnel: local 15432 → prod 5432
# Tunnel is managed by launchd (auto-restart on crash)# Connect via: 127.0.0.1:15432Schema / Migrations
Section titled “Schema / Migrations”Prisma schema at prisma/ (repo root, referenced from apps/api/prisma.config.ts). Run migrations from apps/api/:
cd apps/api && pnpm prisma migrate dev --name <migration-name>SDK Generation
Section titled “SDK Generation”Pipeline: apps/api/scripts/generate-openapi.js (runs from dist) → packages/api-client/src/generated/
Toolchain: @nestjs/swagger (OpenAPI spec) → @redocly/cli (validate) → @hey-api/openapi-ts (typed SDK)
Output files: types.gen.ts, sdk.gen.ts, schemas.gen.ts
Swagger UI available at /api-docs in dev only (disabled in production).
Secrets Management
Section titled “Secrets Management”| Environment | Mechanism |
|---|---|
| Production | Dokploy env vars (set via Dokploy UI/API) |
| Local | .env files (gitignored) |
Never commit secrets. All auth/service keys via env vars. Rotate all prod secrets after v0 stable launch (see memory: project_post_v0_secret_rotation.md).
Scaling Playbook
Section titled “Scaling Playbook”| Stage | Server | Est. Cost/mo |
|---|---|---|
| MVP / launch | CAX11 (2 vCPU, 4 GB) | ~€5 |
| 10K users | CAX21 (4 vCPU, 8 GB) | ~€11 |
| 25K users | CAX31 (8 vCPU, 16 GB) | ~€21 |
| 50K users | CAX41 + managed DB | ~€60+ |
Full cost model (including external services) at 10K–50K users: $186–$1,529/mo. See docs/infra-comparison.md for complete breakdown.
Disqualified Providers (Reference)
Section titled “Disqualified Providers (Reference)”| Provider | Reason |
|---|---|
| Railway | No EU region |
| Render | No PostGIS support |
| DigitalOcean | No PostGIS + cost |
| Porter | $225+/mo minimum |
External Services
Section titled “External Services”| Service | Role |
|---|---|
| Clerk | Auth (JWT, webhooks via svix) |
| Stripe Connect Express | Payments, refunds, professional onboarding |
| Novu Cloud (EU) | Notification orchestration (eu.api.novu.co) |
| Resend | Transactional email (via Novu) |
| Twilio | SMS (via Novu) |
| Expo Push | Mobile push (via Novu) |
| Mapbox | Geocoding, Directions, Matrix, Places, static tiles |
| Cloudflare R2 | Object storage |
| Sentry | Error monitoring (@sentry/nestjs + @sentry/react-native) |
| LangSmith EU | LangGraph tracing |
Auth Notes
Section titled “Auth Notes”- ClerkGuard verifies JWT via
@clerk/backendverifyToken(); attaches user torequest.user @CurrentUser()extractsAuthenticatedUser— never from request body- Clerk webhooks verified via svix signature using
req.rawBody(NOTJSON.stringify(body)) - Stripe webhooks verified via
req.rawBodyinStripeWebhookController - RolesGuard checks
@Roles()metadata againstrequest.user.role
SMS restriction detail:
archive/2026-Q2/docs/clerk-sms-restriction.md— Italian phone SMS carrier restriction (dashboard-only toggle).