Skip to main content
Quill is deployed as two independent units, matching its two codebases: the NestJS API on a VPS behind nginx, and the Next.js frontend on Vercel. The two are joined only by the frontend’s BACKEND_API_URL, and both talk to Duro in sandbox mode with sk_test keys.

Topology

Backend: CI/CD to quill-api.useduro.com

The API deploys from the quill-backend branch through GitHub Actions (.github/workflows/deploy-quill-api.yml). The pipeline is CI-gated, then does a tarball release onto the VPS.
  • Runtime: Node 24.3.0, pnpm 10.5.0, --frozen-lockfile (the lockfile is law). The CI job runs typecheck + build and blocks the deploy on failure; lint is non-blocking.
  • Release model: a timestamped release id, versioned releases/{id} directories, and a release.sh that swaps the active symlink and reloads PM2. This gives atomic releases and easy rollback.
  • Process: PM2 runs a single app named quill-api in fork mode (instances: 1), script dist/src/main.js, max_memory_restart: 512M, logs to a shared logs/ dir (ecosystem.config.cjs). The app listens on port 4010 in production; nginx (managed by CloudPanel) terminates TLS and reverse-proxies quill-api.useduro.com to it.
  • Health: the workflow polls http://127.0.0.1:4010/api/v1/health after release.
Because the CI packages node_modules in the tarball (including the platform-built ffmpeg-static used for HLS transcode), the VPS does not reinstall on release; it unpacks and reloads.
Database migrations are managed with drizzle-kit (db:generate / db:migrate / db:push) against DATABASE_URL (drizzle.config.ts). The schema source of truth is src/database/schema/index.ts.

Frontend: Vercel at quill.useduro.com

The Next.js app deploys to Vercel through its Git integration (there is no GitHub Actions workflow in quill-frontend; Vercel builds on push). It runs the App Router in Node runtime.
  • The one required env var is BACKEND_API_URL, the origin the BFF proxy forwards to (defaults to http://localhost:7010/api/v1 in dev; set to the production API in Vercel). The browser never sees this; it only ever calls the same-origin /api/* routes.
  • R2 upload route (app/api/upload/route.ts) needs the R2 credentials in the Vercel environment, since image uploads are written to R2 directly from the edge/server function rather than proxied to Nest.
  • The proxy and upload routes are runtime = "nodejs"; the catch-all proxy is dynamic = "force-dynamic" so responses are never cached.

Configuration surface

Both sides are configured entirely through environment variables. The backend’s config schema is src/config/configuration.ts; the Duro-facing keys are:

Running against Duro’s sandbox

The reference configuration is Quill in live mode with sk_test keys against Duro’s sandbox. This exercises the entire real integration path (HTTP calls, idempotency, signed webhooks, transfers, VAS) with no real funds moved. mock mode remains available as a fully offline dev mode where checkout auto-completes and payouts return paid deterministically, driven where needed by the POST /api/v1/webhooks/duro/simulate endpoint. The net effect is two products, deployed independently on their natural platforms (VPS + Vercel), meeting only at Duro’s public API, which is exactly the shape a real merchant integration takes. Return to Introduction for the overview, or Duro Integration for the client internals.