Local-only learning product that mimics a Duck-style virtual card experience: create merchant-locked / single-use / everywhere cards, pause & close them, simulate charges, and see merchant amount + $0.25 platform fee on every successful charge.
No real Stripe, bank links, OAuth, or webhooks. Everything runs against SQLite + in-process mocks.
# From repo root
pnpm install
# Build shared package (types + fee math)
pnpm --filter @duck/shared build
# Create DB + seed demo data
cd apps/web
cp ../../.env.example .env # if .env missing
pnpm db:migrate # applies Prisma migrations (SQLite file)
pnpm db:seed # demo@example.com + 5 cards + transactions
cd ../..
# Dev server
pnpm devOpen http://localhost:3000.
You are always “logged in” as demo@example.com (mock auth — no password).
pnpm --filter @duck/shared build && pnpm --filter @duck/web exec prisma migrate dev --name init && pnpm db:seed && pnpm dev| Command | Description |
|---|---|
pnpm dev |
Next.js app on :3000 |
pnpm test |
Vitest (fee math + authorize rules) |
pnpm smoke |
Acceptance flow against local DB + dev server (dev must be running) |
pnpm db:migrate |
Prisma migrate (interactive name if new) |
pnpm db:seed |
Reseed demo user/cards/txs |
pnpm db:reset |
Drop DB, remigrate, reseed |
pnpm build |
Production build |
duck-cards/
├── apps/web # Next.js 15 App Router + Prisma + UI
│ ├── prisma/ # schema, migrations, seed
│ └── src/
│ ├── app/ # pages: /, /cards, /cards/new, /cards/[id], /pricing
│ ├── components/ # shadcn-style UI + card actions / forms
│ └── lib/
│ ├── auth.ts # mock: always demo user
│ ├── db.ts # Prisma client
│ ├── actions.ts # server actions (CRUD + simulate)
│ ├── validations.ts # zod
│ └── issuing/mock-issuing.ts
└── packages/shared # fee math + shared types
└── src/fees.ts # PLATFORM_FEE_CENTS = 25
- User — demo account
- VirtualCard — label, type, status, limits, fake
last4 - Transaction — merchant amount,
feeCents, status
PLATFORM_FEE_CENTS = 25
totalChargeCents(merchant) = merchant + 25Fee is written only on settled (approved) transactions. Declines store feeCents = 0.
lib/issuing/mock-issuing.ts simulates:
| Method | Behavior |
|---|---|
createCard |
Random last4, status active, local DB only |
pauseCard / unpauseCard / closeCard |
Status transitions |
simulateAuthorization |
Pure rules → approve/decline + write Transaction |
Decline rules (unit-tested):
- Card paused or closed
- Over spend limit
- Wrong merchant on
merchant_locked - Second charge on
single_use(also auto-closes after first success) - Invalid amount
Card UI always shows a clearly fake PAN: 4242 4242 4242 {last4}.
| Concern | Implementation |
|---|---|
| Auth | Hardcoded demo user; no sessions/OAuth |
| Card issuing | Local Prisma writes |
| Authorizations / webhooks | simulateAuthorization form on card detail |
| Bank funding / ACH | Not modeled |
| Stripe / Lithic / Clerk / Supabase | Not used |
- Open dashboard → see 5 seed cards (Netflix, Spotify, iCloud, ChatGPT, YouTube).
- Create a new merchant-locked card.
- Simulate charge with matching merchant → settles, fee $0.25 line item.
- Pause the card → simulate again → declined.
- Unpause → charge works again (if under limit).
- Close (confirm dialog) → permanent; no more approvals.
- Check /pricing for fee disclosure vs mock Privacy-style monthly plans.
Zero external network calls required for core flows.
See .env.example:
DATABASE_URL="file:./dev.db"
DEMO_USER_EMAIL="demo@example.com"
SQLite file lives at apps/web/prisma/dev.db (path relative to Prisma schema).
pnpm testpackages/shared— fee math (totalChargeCents,formatCents, …)apps/web—evaluateAuthorizationdecline/approve matrix
Outlined only — do not implement yet:
- Stripe Issuing (test mode) — real card objects, ephemeral keys for PAN/CVC display under PCI constraints.
- Webhooks —
issuing_authorization.request, transaction updates; replacesimulateAuthorization. - Funding — Connect / treasury / bank link (still sandbox).
- Chrome extension — autofill checkout with card details; share types from
@duck/shared. - Real auth — Clerk/Auth.js; multi-user tenancy.
- Production — hosted Postgres, secrets, KYC, compliance.
- TypeScript monorepo (pnpm)
- Next.js 15 (App Router) + React 19 + Tailwind + Radix/shadcn-style components
- Prisma + SQLite
- Zod validation
- Vitest
- Sonner toasts
MIT — learning project, not a financial product.