Skip to content

Infrastructure Reference

Diátaxis: Reference + How-to

Sources: CLAUDE.md § Infrastructure, docs/infra-comparison.md


ComponentDetails
ServerHetzner CAX11 ARM64, ~€5/mo
IP178.104.154.74
PlatformDokploy (self-hosted PaaS)
OSCloud-init via Pulumi TypeScript IaC
App containerideony-api (Docker, --network host, port 3001)
DatabasePostgreSQL 18 + PostGIS, localhost:5432, DB ideony
CacheRedis 8.6, localhost:6379
StorageCloudflare R2 (S3-compatible, @aws-sdk/client-s3)
SSHssh -i ~/.ssh/id_ed25519_ideony-deploy root@178.104.154.74
ServiceImage
PostgreSQL + PostGISimresamu/postgis:17-3.5
Redisredis:8.6-alpine
NestJS APICustom Docker build
Expo Web (static)nginx served

ContextURL
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 FEhttp://178.104.154.74
Direct BEhttp://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


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 via gh 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"; done

Local gate scripts/verify-local.sh additionally 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 via SKIP_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-verify remains prohibited. Hard rules (auto-enforced since 2026-04-20): workflow/Dockerfile/compose/deploy-script changes require docs/infrastructure.md; apps/mobile/lib/colors*/fonts*/design-tokens changes require docs/design-system.md; prisma/schema.prisma requires docs/architecture.md. Test harness: scripts/check-docs-updated.test.sh (26 cases).

  1. Lint (Biome)
  2. Typecheck
  3. Build
  4. Unit tests (with Postgres + Redis GitHub Actions services)
  5. SAST + audit
  1. SSH into prod server
  2. Docker build on server
  3. Prisma migrate
  4. Blue-green swap
  5. Health check
  6. Rollback on failure
  7. Trivy scan
  8. 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.
  9. Auto-tag release

DEPLOY_SSH_KEY, PROD_DATABASE_URL, PROD_ENV_VARS (20 env vars)

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.


Terminal window
# Build (from repo root)
docker build -t ideony-api -f apps/api/Dockerfile .
# Run
docker run --network host --env-file .env ideony-api
Terminal window
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-test

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:

Terminal window
docker image prune -f
docker builder prune -f --filter until=72h

Terminal window
pnpm docker:up # Postgres (5433), Redis (6379), MinIO (9000/9001), Mailpit (1025/8025)
ServiceLocal port
PostgreSQL5433
Redis6379
MinIO (S3)9000 / 9001
Mailpit (SMTP)1025 / 8025

Local: MinIO (S3-compatible). Production: Cloudflare R2.

Terminal window
pnpm dev # all apps, dev mode
pnpm build # build all
pnpm test # all tests
pnpm lint # Biome lint + format check
pnpm lint:fix # auto-fix lint + format
cd apps/api && pnpm prisma migrate dev # run migrations
cd apps/api && pnpm prisma generate # regenerate Prisma client
pnpm -w run generate:sdk # build API → OpenAPI spec → typed SDK

autossh tunnel: local 15432 → prod 5432

Terminal window
# Tunnel is managed by launchd (auto-restart on crash)
# Connect via: 127.0.0.1:15432

Prisma schema at prisma/ (repo root, referenced from apps/api/prisma.config.ts). Run migrations from apps/api/:

Terminal window
cd apps/api && pnpm prisma migrate dev --name <migration-name>

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).


EnvironmentMechanism
ProductionDokploy 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).


StageServerEst. Cost/mo
MVP / launchCAX11 (2 vCPU, 4 GB)~€5
10K usersCAX21 (4 vCPU, 8 GB)~€11
25K usersCAX31 (8 vCPU, 16 GB)~€21
50K usersCAX41 + 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.


ProviderReason
RailwayNo EU region
RenderNo PostGIS support
DigitalOceanNo PostGIS + cost
Porter$225+/mo minimum

ServiceRole
ClerkAuth (JWT, webhooks via svix)
Stripe Connect ExpressPayments, refunds, professional onboarding
Novu Cloud (EU)Notification orchestration (eu.api.novu.co)
ResendTransactional email (via Novu)
TwilioSMS (via Novu)
Expo PushMobile push (via Novu)
MapboxGeocoding, Directions, Matrix, Places, static tiles
Cloudflare R2Object storage
SentryError monitoring (@sentry/nestjs + @sentry/react-native)
LangSmith EULangGraph tracing

  • ClerkGuard verifies JWT via @clerk/backend verifyToken(); attaches user to request.user
  • @CurrentUser() extracts AuthenticatedUsernever from request body
  • Clerk webhooks verified via svix signature using req.rawBody (NOT JSON.stringify(body))
  • Stripe webhooks verified via req.rawBody in StripeWebhookController
  • RolesGuard checks @Roles() metadata against request.user.role

SMS restriction detail: archive/2026-Q2/docs/clerk-sms-restriction.md — Italian phone SMS carrier restriction (dashboard-only toggle).