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, no NEXT_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:

LayerContentsMay 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 DeploymentOrchestratordomain, shared — no Prisma, no Redis, no Express
infrastructure/Concrete implementations: Prisma repositories, Redis cache, BullMQ queue and workers, and the GitHub / Vercel / Render / Groq adaptersdomain and application interfaces
interfaces/Delivery: Express controllers, middleware, Swaggerapplication 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. BuildCommandResolver reads the repo's own package.json scripts; Python start commands are derived from the detected app module (e.g. uvicorn main:app from app = FastAPI()); Go build targets come from the main.go / cmd/* layout.
  • Centralized provider mapping. DeploymentStrategyResolver owns the framework → provider rules, so provider knowledge never leaks into the orchestrator.
  • Env scanning. EnvVarScanner finds 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 tagged ai:groq.
  • Call 2 — failure diagnosis. diagnoseDeploymentFailure() fires once when a deployment fails, from the orchestrator's terminal failure path. See Deployment Lifecycle.
Graceful degradation
With no Groq keys configured the platform runs deterministic-only. If the model errors or times out, the deterministic result is kept — AI failure is never surfaced as a user-facing error. Results that used AI are marked with an 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:

queue: deployments
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 UnrecoverableError so 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).