Stripe template
A real Stripe-style REST API backend — customers, payments, invoices, subscriptions, refunds, and signed webhooks.
The Stripe template (apps/stripe-clone, name stripe) is a faithful, API-only
clone of the Stripe REST API: a NestJS backend on Postgres.
Auth is a Stripe-style secret key (sk_test_…) sent as a bearer token — no
signup/login, no per-user session. Every request is scoped to the account that
owns the key. It's faithful enough at the URL + wire-format level that an
agent built for Stripe can operate it.
hone env spin stripeRouting — read this first
Real Stripe serves business routes at /v1/* (not /api/v1/*), while the clone
health probe is /api/health. So the backend uses no global prefix: business
controllers bind @Controller('v1/...') and a single health controller binds
@Controller('api'). Both /v1/customers and /api/health resolve faithfully.
What's inside
| Piece | Notes |
|---|---|
backend/ | NestJS REST API, Bearer sk_ auth, Postgres via pg. Business at /v1/*, health at /api/health, Swagger at /api/docs. |
supabase/migrations/ | 001_initial_schema.sql + 002_constraints_and_helpers.sql — the schema (17 tables), gen_stripe_id, status checks, run on spin and replayed on reset. |
seeds/ | acct.sql — the bundled deterministic fixture. |
Modes
| Mode | Services | Use it for |
|---|---|---|
api (default) | backend | Agents that talk to the REST API. |
This template is API-only — there is no frontend or full mode.
Seeding
hone env spin stripe --seed acct # deterministic fixture on create
hone env seed stripe-a1b2 --ai # realistic data via the REST APIAI seeding generates customers, products, prices, and invoices (the
template's declared entities) through real /v1/* calls. See
Seeding.
Auth — the seeded secret key
spin stripe seeds one account and one secret key. The plaintext key (only its
sha256 is stored) is:
sk_test_asymmetric000000000000stripeUse it as a bearer token against the REST API — no signup/login step:
curl -s http://127.0.0.1:3003/v1/customers \
-H "authorization: Bearer sk_test_asymmetric000000000000stripe"Real Stripe also accepts the key as the HTTP Basic username, and so does the clone:
curl -s -u sk_test_asymmetric000000000000stripe: http://127.0.0.1:3003/v1/balanceTwo flows worth pointing an agent at
The template is built around two end-to-end money-movement chains (both verified against a live Postgres):
B=http://127.0.0.1:3003
A="authorization: Bearer sk_test_asymmetric000000000000stripe"
# 1. Subscription that actually goes paid: creates items + the first invoice +
# a PaymentIntent, confirms it to a charge + balance transaction, status=active.
curl -s $B/v1/subscriptions -H "$A" \
-d customer=cus_JennyRosen0001 -d 'items[0][price]=price_ProMonthly01'
# 2. Partial then full refund: amount_refunded increments, a full refund sets
# refunded=true, a negative balance transaction posts, GET /v1/balance reflects
# it, and charge.refunded + refund.created webhooks fire.
curl -s $B/v1/refunds -H "$A" -d charge=ch_SeedPaid000001 -d amount=500
curl -s $B/v1/refunds -H "$A" -d charge=ch_SeedPaid000001
curl -s $B/v1/balance -H "$A"Money movement is simulated: confirming a PaymentIntent with a seeded test
card (pm_card_visa / pm_card_mastercard) succeeds synchronously. There is no
real PSP, 3DS/SCA, or async bank webhook.
API shape — read this before pointing an agent at it
Unlike the Slack clone (REST-vs-RPC, capability-level match only), Stripe is
itself a REST API, so the clone matches it on the same literal /v1/… paths
with the same wire format — not just the capability level:
- prefixed object ids (
cus_,prod_,price_,pi_,ch_,re_,in_,sub_, …) — no UUIDs on the wire, - cursor pagination wrapped in
{object:'list', url, has_more, data}withlimit(default 10, max 100) /starting_after/ending_before, - form-encoded request bodies (nested
items[0][price]keys), theIdempotency-Keyreplay header, aRequest-Id: req_…response header, and the{error:{type,code,message,param}}error envelope, - per-resource search at
GET /v1/<resource>/search, wrapped in{object:'search_result', url, has_more, data, next_page}, - per-account rate limiting —
100 req/sfor live keys,25 req/sfor test keys (matching Stripe's published tiers); over the cap returns HTTP 429 with aStripe-Rate-Limited-Reasonheader and a{error:{code:'rate_limit', …}}body, - HMAC-signed outgoing webhooks with a
Stripe-Signature: t=…,v1=…header, and a byte-faithfuleventenvelope (api_version,pending_webhooks,request,data.previous_attributes).
The template ships an API_PARITY.md with the per-endpoint breakdown, re-audited
against Stripe's live docs. The implemented core spans customers, products,
prices, payment methods, payment intents (both automatic and manual capture),
charges, refunds, invoices + invoice items, subscriptions + subscription items, a
read-only balance, an events feed, and webhook endpoints — 72 endpoints:
| Status | Count |
|---|---|
| ✅ exact (path + convention + envelope align) | 72 |
| ❌ missing (sub-actions outside the targeted slice) | 10 |
That's 88% of the targeted core surface, on exact Stripe paths. Whole Stripe
surfaces outside that core (Connect, Checkout, Terminal, Issuing, tax, disputes,
SetupIntents, …) are unimplemented by design — this is an MVP+Beta subset, not the
full API. A handful of sub-actions in scope are also not yet implemented
(payment_intents/:id/increment_authorization, invoices/:id/send,
subscriptions/:id/resume, customer-scoped payment-method reads). Check
apps/stripe-clone/API_PARITY.md for the full list, including the documented
?expand[] allowlist.
Inspect a running clone
hone env query stripe-a1b2 "select stripe_id, email from customers"
hone env db stripe-a1b2 # prints a psql shell command
hone env logs stripe-a1b2 -f # stream the backendThe schema (customers, charges, invoices, subscriptions, …) comes straight from
001_initial_schema.sql — read it to know what's queryable.
Slack template
A real Slack-style backend, frontend, and MCP server — RPC Web API, the {ok} envelope, Slack-format ids, and byte-identical events.
Notion template
A real Notion-style API backend — databases, data sources, pages, blocks, views, comments, file uploads, and search, with Notion's rate limits.