Files
civitai__civitai/apps/orchestrator-gateway
Zachary Lowden 5cc442582d feat(orchestrator-gateway): P0 skeleton — Fastify+tRPC service (health/metrics/authed ping) (#2853)
* feat(orchestrator-api): P0 skeleton — Fastify+tRPC service (health/metrics/authed ping)

Phase 0 of the orchestrator generation-API spin-out (strangler-fig). Stands up a
standalone Fastify + tRPC-over-HTTP Node service in the monorepo — the future home of
the generation slice of the orchestrator subdomain — with NO generation code moved yet.

What this ships:
- apps/orchestrator-api/ — @civitai/orchestrator-api, Fastify on :3000, tsup build,
  Dockerfile mirroring apps/auth/Dockerfile (multi-stage node:22-alpine, corepack pnpm,
  filtered frozen install, pnpm deploy --prod runtime).
- GET /health  — no-auth liveness+readiness (zero external deps → can't flap the pod).
- GET /metrics — prom-client scrape, private-by-x-forwarded-for guard (mirrors the hub).
- tRPC at /api/trpc: orchestrator.health (public) + orchestrator.ping (PROTECTED —
  verifies __Secure-civ-token / Bearer LOCALLY via @civitai/auth ES256/JWKS + injected
  sysRedis revocation, returns { userId }).
- Wired-but-unused clients (import + connect-config, no features): @civitai/db (Prisma
  via shared pooler, lazy dynamic import), @civitai/redis (cache + sysRedis),
  @civitai/client (external orchestrator SDK).
- vitest: /health 200, /metrics XFF-guarded, ping 401 unauthenticated + userId for a
  valid token (mocked verifier).

Deliberately deferred (later phases): moving workflows.ts / orchestration-new.service.ts
/ ecosystems (P1/P2); the @civitai/prompt-audit + @civitai/generation-graph carve-outs
(next P0 increment, separate PR); live traffic routing (monolith still serves
/api/trpc/orchestrator — the same-origin PathPrefix cutover is P2).

Plan: datapacket-talos:claudedocs/plan-orchestrator-api-spinout-2026-06-30.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(orchestrator-gateway): rename service orchestrator-api → orchestrator-gateway

Mechanical rename of the P0 scaffolding service. No behavior change.

- apps/orchestrator-api → apps/orchestrator-gateway
- package @civitai/orchestrator-api → @civitai/orchestrator-gateway (+ imports)
- image ghcr.io/civitai/civitai-orchestrator-api → civitai-orchestrator-gateway
- tag prefix orchestrator-api-v → orchestrator-gateway-v
- prom metrics orchestrator_api_* → orchestrator_gateway_*
- service string literals + Dockerfile/tsup/vitest/README refs

tRPC router key stays `orchestrator.*` (client-facing, unchanged).

Verified: pnpm --filter @civitai/orchestrator-gateway build + test both pass
(6/6). typecheck shows only the 5 pre-existing @civitai/db-schema/@civitai/db
Prisma-not-generated-on-NixOS errors — ZERO in apps/orchestrator-gateway/src.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(orchestrator-gateway): point pnpm-lock importer at renamed app dir

The service rename (orchestrator-api → orchestrator-gateway) updated the app
dir + package name but left the pnpm-lock.yaml importer key as
apps/orchestrator-api, so CI's frozen-lockfile install failed with
ERR_PNPM_OUTDATED_LOCKFILE (15 deps 'added'). Importer key now matches the
app dir; all resolved deps were already correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 08:27:34 -05:00
..

@civitai/orchestrator-gateway

Status: P0 skeleton. A standalone Fastify + tRPC-over-HTTP Node service — the target for the generation slice of the orchestrator subdomain, spun out of the Next.js monolith (strangler-fig).

See the full plan: datapacket-talos:claudedocs/plan-orchestrator-gateway-spinout-2026-06-30.md.

Why this exists

The civitai-dp-prod-api pool is pinned at HPA min 80 as a reliability buffer because bursty orchestrator parks (img2img whatIf/generate source-download hangs) head-of-line-block the shared api pool at the connection layer. Moving orchestrator.* generation onto its own pod pool contains a park burst to this service, so the api pool's tail becomes predictable, its floor can be cut, and a civitai-web node can be drained (5→4). Success is isolation + node-drain, not raw CPU reduction.

What P0 ships (this skeleton)

  • Fastify HTTP server on :3000.
  • GET /health — no-auth liveness+readiness (zero external deps, so a DB/redis blip can't flap the pod).
  • GET /metrics — Prometheus scrape (prom-client), private-by-x-forwarded-for guard (mirrors the auth hub).
  • tRPC mounted at /api/trpc (fetch adapter) with:
    • orchestrator.health — public liveness echo.
    • orchestrator.pingprotected; verifies the incoming __Secure-civ-token / Bearer token LOCALLY via @civitai/auth (ES256/JWKS + injected sysRedis revocation, fail-open) and returns { userId }.
  • Wired-but-unused clients (construct + connect-config, no features on them yet): @civitai/db (Prisma via the shared pooler), @civitai/redis (cache + sysRedis), @civitai/client (external orchestrator SDK).

What P0 deliberately does NOT do

  • No generation code moved (workflows.ts, orchestration-new.service.ts, ecosystems) — that's P1/P2.
  • No @civitai/prompt-audit / @civitai/generation-graph carve-outs — next P0 increment, separate PR.
  • No live traffic routing — the monolith still serves /api/trpc/orchestrator. The same-origin PathPrefix(/api/trpc/orchestrator) Traefik cutover is P2. In prod this is ClusterIP + ServiceMonitor only, reachable via port-forward.

Layout

src/
  server.ts              entrypoint (listen)
  app.ts                 buildServer() factory — /health, /metrics, tRPC mount (testable, no listen)
  trpc/
    context.ts           per-request ctx: local token verify → { claims, userId }
    trpc.ts              initTRPC, publicProcedure, protectedProcedure (auth gate)
    router.ts            appRouter — orchestrator.health (public) + orchestrator.ping (protected)
  lib/server/
    metrics.ts           prom-client registry + counters/histograms
    auth/
      verifier.ts        createAuthVerifier (spoke: JWKS + injected revocation)
      registry.ts        createSessionRegistry (sysRedis revocation read, fail-open no-op fallback)
    clients/
      redis.ts           getRedis / getSysRedis (lazy, memoized)
      db.ts              getDb (lazy dynamic import behind DATABASE_URL guard)
      orchestrator.ts    createOrchestratorClient / getInternalOrchestratorClient
  __tests__/
    health.test.ts       /health 200, /metrics served + XFF-guarded 404
    ping.test.ts         ping rejects unauthenticated (401), returns userId for a valid token (mocked verifier)

Local dev

cp .env.example .env   # fill secrets for a real run
pnpm --filter @civitai/orchestrator-gateway dev        # tsx watch
pnpm --filter @civitai/orchestrator-gateway typecheck  # tsc --noEmit
pnpm --filter @civitai/orchestrator-gateway test       # vitest
pnpm --filter @civitai/orchestrator-gateway build      # tsup → dist/server.js

Build / release

Built by the shared Tekton tag-webhookbuild-and-push pipeline (auth is its first user). Push a git tag orchestrator-gateway-vX.Y.Z on civitai/civitai → the webhook matches the prefix in APP_CONFIG (datapacket-talos:clusters/production/apps/tekton-builds/tag-webhook.py) → buildkit builds this Dockerfile → pushes ghcr.io/civitai/civitai-orchestrator-gateway:X.Y.Z → Flux ImagePolicy picks it up.