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 thequill-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 arelease.shthat swaps the active symlink and reloads PM2. This gives atomic releases and easy rollback. - Process: PM2 runs a single app named
quill-apiinforkmode (instances: 1), scriptdist/src/main.js,max_memory_restart: 512M, logs to a sharedlogs/dir (ecosystem.config.cjs). The app listens on port 4010 in production; nginx (managed by CloudPanel) terminates TLS and reverse-proxiesquill-api.useduro.comto it. - Health: the workflow polls
http://127.0.0.1:4010/api/v1/healthafter release.
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 inquill-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 tohttp://localhost:7010/api/v1in 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 isdynamic = "force-dynamic"so responses are never cached.
Configuration surface
Both sides are configured entirely through environment variables. The backend’s config schema issrc/config/configuration.ts; the Duro-facing keys are:
Running against Duro’s sandbox
The reference configuration is Quill inlive 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.