Architecture
How deplo.ai is built: strict clean architecture on the server, a BullMQ job system for deployment work, a deterministic-first detection engine with a bounded AI fallback, and per-user encrypted provider credentials. This is the contributor-level view — for the gentler tour, start with the Architecture Overview.
If you have not read the Architecture Overview, start there — this page assumes you know the cast of characters (dashboard, API, queue, worker, detection engine, provider adapters) and digs into how each is actually implemented.
The request path#
The frontend (Vercel) and the backend (Render) live on different hosts, but users only ever see one domain. Every API call from the browser is same-origin; a Vercel rewrite defined in the client's next.config.ts proxies /api/* to the Render backend:
- The backend origin is a server-only environment variable (
API_URL, noNEXT_PUBLIC_prefix) — it is never shipped to the browser. - Because requests are same-origin, there are no CORS preflights in the browser; the backend's CORS allowlist is a secondary safeguard for direct access.
- Auth headers (
Authorization: Bearer …) and 3xx redirects pass through the proxy verbatim, so OAuth callbacks flow back through the frontend domain untouched. - Only inbound webhooks (GitHub, Vercel) bypass the proxy — external providers call the backend's own public URL directly.
Clean architecture layers#
The server (server/src/) follows layered clean architecture with strictly enforced dependency rules. Each layer may only depend inward:
| Layer | Contents | May depend on |
|---|---|---|
| domain/ | Pure entities and repository interfaces (User, Deployment, IDeploymentRepository, the Result type, analysis types) | Nothing |
| application/ | Use cases: services (AuthService, DeploymentService, IntegrationService, CliTokenService), the analysis engine, and the DeploymentOrchestrator | domain, shared — no Prisma, no Redis, no Express |
| infrastructure/ | Concrete implementations: Prisma repositories, Redis cache, BullMQ queue and workers, and the GitHub / Vercel / Render / Groq adapters | domain and application interfaces |
| interfaces/ | Delivery: Express controllers, middleware, Swagger | application services — never infrastructure directly |
config/ (Zod-validated environment) and shared/ (logger, AppError) are usable by all layers. Everything is composed once at startup in server/src/index.ts: repositories are constructed over Prisma, services over repositories, controllers over services, and the worker over the orchestrator — plain constructor injection, no DI framework. Those services back the REST API mounted at /api/v1, which the dashboard and the CLI both consume.
The detection engine#
Stack detection (server/src/application/analysis/) is a layered, evidence-scored pipeline. The guiding principle: never trust a single indicator. A framework is identified by a declarative signature — dependencies, config files, content patterns, and scripts, each weighted — not by the presence of one file.
- Weighted signatures. Every supported framework is data in
frameworkSignatures.ts; scores normalize to a 0–1 confidence, and a meta-framework (Next.js) beats its base library (React) via priority. Adding a framework means adding a signature, not new control flow. - Tree-driven. Structure and workspaces come from the full recursive git tree, so Python, Go, and
apps/*monorepo layouts are seen — no filename guessing. - Real commands.
BuildCommandResolverreads the repo's ownpackage.jsonscripts; Python start commands are derived from the detected app module (e.g.uvicorn main:appfromapp = FastAPI()); Go build targets come from themain.go/cmd/*layout. - Centralized provider mapping.
DeploymentStrategyResolverowns the framework → provider rules, so provider knowledge never leaks into the orchestrator. - Env scanning.
EnvVarScannerfinds the variables your code actually reads and classifies them by scope (frontend, backend, shared) and required-ness — this drives the environment variables step.
Layer 3 — the Groq AI fallback#
When deterministic confidence falls below 0.5, the pipeline escalates to Groq (model openai/gpt-oss-120b by default). AI use is strictly bounded: at most two calls per deployment, ever.
- Call 1 — low-confidence analysis.
reasonAboutRepository()sends the folder tree (at most 200 paths), key config files and small top-level source files (at most 25 files, 1.5 KB each), dependency lists, and the deterministic evidence — never the whole repository. The response is constrained by a strict JSON schema with framework and language enums, so the model can only answer in canonical terms. Its corrections merge only when more confident than the deterministic result, and merged evidence is taggedai:groq. - Call 2 — failure diagnosis.
diagnoseDeploymentFailure()fires once when a deployment fails, from the orchestrator's terminal failure path. See Deployment Lifecycle.
aiUsed flag.The queue system#
All deployment work runs on BullMQ over Redis. The API enqueues; a worker (concurrency 5, in the same process today) executes. A deployment is two job phases with distinct, idempotent job IDs:
analyze-<deploymentId> → orchestrator.runAnalysis()
detects stack, scans env vars
→ WAITING_FOR_ENV (parked) or enqueue deploy
deploy-<deploymentId> → orchestrator.runDeploy()
backend → Render, frontend → Vercel, wire URLs- Jobs default to 3 attempts with exponential backoff (5s base) — transient provider hiccups retry themselves.
- Errors that can never succeed — provider 4xx responses, missing configuration — are wrapped in an
UnrecoverableErrorso BullMQ fails them immediately instead of retrying. - Retries are safe because the orchestrator is idempotent: URLs from completed phases are preserved, and existing Render services / Vercel projects are found and reused, never blindly re-created.
A second queue (monitoring) runs a BullMQ job scheduler: every five minutes it probes the live frontend and backend URLs of every project with a completed deployment (15s timeout, HTTP status below 500 counts as up — free-tier cold starts are not false alarms), stores uptime samples, and prunes anything older than seven days. This powers Monitoring & Analytics.
Credentials and data#
Every provider connection is per-user, stored in a single provider-agnostic ProviderConnection table with the credential AES-256-GCM encrypted at rest. Credentials are validated live against the provider before being stored (an invalid Render key is never saved), decrypted only inside the orchestrator at deploy time, and never returned by any API. There are no global platform hosting credentials — a deployment physically cannot land anywhere except your own accounts. Full details in Authentication & Security.
- PostgreSQL (Prisma) — users, repositories, deployments with logs, provider connections, CLI token hashes, uptime samples. All public IDs are
cuid()s. - Redis (ioredis) — BullMQ queues plus short-TTL caches: repository lists (300s), deployment status (30s), user profiles (3600s).
Related pages#
- Deployment Lifecycle — the orchestrator's phases, every status, and failure handling.
- Project Structure — where all of this lives in the codebase.
- API Reference — the REST surface the dashboard and CLI call.