Skip to content

Return a typed 503 DEMO_MODE_UNSUPPORTED for Postgres and Soroban-backed routes when DEMO_MODE is on #1515

Description

@greatest0fallt1me

Summary

The public deployment runs with DEMO_MODE=true. The README promises that features needing PostgreSQL (metering, billing, invoices, audit persistence, usage exports) "respond with errors in demo mode", and that Stellar/Soroban calls are not made. Nothing enforces either. In demo mode those routes still try to use the Postgres pool (2-second connect timeout, then a 500), the Prisma client, or the Soroban clients, and the response is an opaque INTERNAL_ERROR. This issue adds an explicit demo capability guard: a declared list of routes that are unavailable in demo mode, a stable 503 DEMO_MODE_UNSUPPORTED envelope, and a health field that tells clients which mode they are talking to.

Background and evidence

  • What the README promises. README.md, "Demo mode": "Features needing PostgreSQL (metering, billing, invoices, audit persistence, usage exports) respond with errors in demo mode", and "Stellar/Soroban calls are not made."
  • The only demo branches.
    • src/db/index.ts, initializeDb (lines 31-34), only switches the SQLite bootstrap: if (process.env.DEMO_MODE === 'true') { bootstrapDemoDb(sqlite, migrationDir); return; }.
    • src/app.ts (line 350) only disables OpenAPI response validation: validateResponses: process.env.DEMO_MODE !== "true".
    • No other code reads DEMO_MODE (grep -rn DEMO_MODE src).
  • Postgres still used. src/app.ts (line 180) always sets app.locals.dbPool = pool (src/db.ts, a real pg.Pool with connectionTimeoutMillis). Billing (src/routes/billing/deduct.ts), idempotency, invoices (src/routes/invoices.ts, through Prisma) and forecast all use it in demo mode.
  • Soroban still called. createSorobanBillingService builds a client whenever SOROBAN_BILLING_CONTRACT_ID is set, and the README's Vercel instructions set it, so demo traffic can reach a real RPC.
  • No mode signal. /api/health returns { status: "ok", service: "callora-backend" } with no indication of demo mode.

Scope

In scope

  1. Config. Promote DEMO_MODE into src/config/env.ts (zod boolean) and config.demoMode, replacing the raw process.env reads.
  2. Capability guard.
    • Add src/middleware/demoGuard.ts with a declared table of route prefixes and methods that require Postgres or Soroban: /api/billing/*, /api/invoices, /api/usage*, /api/exports*, /api/admin/usage/*, /api/admin/db/explain, /api/vault/deposit/prepare, /api/audit writes, /api/credits, /api/refunds.
    • In demo mode, matching requests get 503 with code: "DEMO_MODE_UNSUPPORTED", details: { feature } and Retry-After omitted. Mount it before those routers.
    • Add the code to docs/error-codes.yaml and run npm run error-codes:generate.
  3. No external calls. In demo mode, createSorobanBillingService returns undefined regardless of env, and the Postgres pool is created lazily so it never connects.
  4. Health. /api/health includes mode: "demo" | "standard". Update the OpenAPI schema for /api/health.
  5. Docs. Update README.md (Demo mode section) with the exact list of unavailable routes.

Out of scope

  • Making Postgres features work on SQLite.
  • The Vercel handler and bundle (a separate issue).

Design constraints

  • One table. The guarded route list is a single exported constant, and a test asserts every entry corresponds to a mounted route, so it cannot rot.
  • No effect outside demo. With DEMO_MODE unset, behaviour is byte-for-byte unchanged (snapshot test on /api/health aside from the new field).
  • Envelope. The 503 uses errorEnvelope with requestId.

Test plan

Add src/middleware/demoGuard.test.ts and extend tests/integration/health.test.ts, using createApp with DEMO_MODE=true.

# Scenario Expected
1 POST /api/billing/deduct in demo 503 DEMO_MODE_UNSUPPORTED, quickly (no 2s pool timeout)
2 GET /api/invoices in demo 503
3 GET /api/apis in demo 200 with the seeded catalog
4 GET /api/health in demo mode: "demo"
5 Same billing call with demo off reaches the handler as before
6 Demo with SOROBAN_BILLING_CONTRACT_ID set no Soroban client constructed (spy)
7 Guard table entries every prefix resolves to a mounted router
8 Pool usage in demo pool.connect never called during the suite

Regression proof. Row 1 returns a 500 after the pool timeout on current main.

Acceptance criteria

  • No raw process.env.DEMO_MODE reads remain outside src/config.
  • Every test-plan row passes.
  • npm run lint, npm run typecheck, npm run build (including error-codes:check and validate:openapi) and npm test pass, and CI is green.
  • At least 90% line coverage on demoGuard.ts.

Getting started

git checkout -b feat/demo-mode-capability-guard
npm ci && npx prisma generate
DEMO_MODE=true CORS_ALLOW_MISSING_ORIGIN=true npm run dev
curl -i -X POST localhost:3000/api/billing/deduct

Read first: src/db/index.ts, src/db/demo.ts, src/app.ts (lines 160-360), src/services/createSorobanBillingService.ts, and the README "Demo mode" section.

PR requirements

  • Link this issue (Closes #<n>).
  • Paste the guarded route table and test output with coverage.
  • Example commit: feat(demo): add capability guard with typed 503 for unsupported routes.

Priority rationale

Medium. The work is contained, but it makes the public demo behave as documented, stops it from contacting real infrastructure, and gives clients a machine-readable signal instead of opaque 500s.

Activity

  1. Gabriel-123-diamond commented on Oct 18, 2026

    @Gabriel-123-diamond
    Contributor

    @Gabriel-123-diamond has applied to work on this issue as part of the Stellar Wave Program's 10th wave.

    I'd like to take this issue. My approach is to centralize DEMO_MODE in validated configuration, add one exported method/prefix capability table, and mount the guard before database or Soroban-backed handlers. Unsupported requests will use the existing errorEnvelope with requestId and feature details, without Retry-After.

    I'll prevent Soroban client construction and Postgres connections in demo mode, keep standard-mode behavior intact, and update health/OpenAPI, generated error codes, and the README. Tests will cover all eight scenarios, mounted-route coverage for the guard table, and spies proving no external calls. I'll include coverage (targeting at least 90% for the guard) and lint, typecheck, build, schema validation, and test results in a focused PR. I'll wait for assignment before starting.

    ℹ️ Repo Maintainers: To accept this application, review their application or assign @Gabriel-123-diamond to this issue.

  2. drips-wave commented on Oct 19, 2026

    @drips-wave

    Congratulations, @Gabriel-123-diamond! 🎉 Your application was accepted by the repo's maintainers, and the issue is due on October 22, 2026.

    🧑‍💻 @Gabriel-123-diamond: Please resolve the issue such that the repo's maintainers have enough time to review your contribution before the due date. You'll earn Points for completing the issue on-time, which will make you eligible for a share of the Stellar Wave Program's reward pool.

    Warning

    When opening a PR, please link it to this issue to ensure it gets tracked accurately. Points are awarded when this issue is marked as completed by the maintainer.

    🤠 Repo maintainers: Please keep an eye on the contributor's progress and review their work before the due date. You can manage this issue, including adjusting its complexity and points, here.

    🌊 Happy Wave 🌊

  3. Gabriel-123-diamond commented on Oct 19, 2026

    @Gabriel-123-diamond
    Contributor

    Implementation complete and ready for review: #1526

    Updated on October 24 in response to the maintainer's rebase request: head 002a023 is based on current main 5150bee, retains #1550's restored security/configuration checks and the newer export/vault changes, and has no merge conflicts.

    Validation: 56 focused tests pass; demo guard coverage is 100%; production and Vercel builds and the secret scan pass. Full run: 2,776 tests pass and 17 fail because four PostgreSQL Testcontainers suites lack a working container runtime, reproduced on unchanged main. Existing lint startup failure and the same 254 typecheck diagnostic locations/codes are documented in the PR.

    Remaining CI blocker: OpenAPI compatibility treats the required new DEMO_MODE_UNSUPPORTED response enum member as breaking. This is disclosed in the PR and maintainer reply; CI is not claimed green. Awaiting maintainer review.

  4. added a commit that references this issue on Oct 24, 2026
    002a023
  5. added a commit that references this issue on Oct 24, 2026
    68cab6a
  6. drips-wave commented on Oct 24, 2026

    @drips-wave

    This issue has been completed by @Gabriel-123-diamond as part of the Stellar Wave Program's 10th Wave 🥳

    😎 @Gabriel-123-diamond: You earned 100 Points for completing this issue! Once the Wave's rewards are finalized, you'll be eligible for a percentage of the Wave's reward pool based on the percentage of total points you've earned. Learn more here. You can also Leave a review to share your experience working on this issue.

    🧑‍💻 Repo maintainers: How'd the contributor do? Leave a review to share your experience working with them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Stellar WaveStellar Wave Program issueapiAPI endpoint/contract workbackendBackend service workopspriority:mediumMedium implementation difficulty

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions