Skip to content

feat: add stripe service emulator#4

Merged
ctate merged 4 commits intovercel-labs:mainfrom
mvanhorn:feat/stripe-emulator
Mar 31, 2026
Merged

feat: add stripe service emulator#4
ctate merged 4 commits intovercel-labs:mainfrom
mvanhorn:feat/stripe-emulator

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

@mvanhorn mvanhorn commented Mar 22, 2026

Summary

Adds a Stripe service emulator with stateful persistence, cursor-based pagination, expand[] support, webhook dispatch on every state change, and a checkout UI. Covers the core payment flow: customers, payment intents, charges, products, prices, and checkout sessions.

Why this matters

Stripe's official stripe-mock (1,600 stars) is stateless - data sent via POST is validated but not persisted. Users describe it as "kind of useless for black box testing without some degree of persistence" (24 comments, open since inception). It has no webhook support, and Stripe's own rate limits docs say "do not recommend load testing using the Stripe API in testing environments."

emulate's Store gives us stateful persistence and WebhookDispatcher gives us webhook delivery - the two features stripe-mock is missing.

Changes

Plugin structure - packages/@internal/stripe/ with 6 entity types, 5 route files, seed config, checkout UI

Stateful payment flow:

  • Customers: full CRUD with email filtering
  • PaymentIntents: create, update, confirm (state machine), cancel
  • Charges: auto-created on confirm, filterable by customer/payment_intent
  • Products + Prices: create, list with active filter
  • Checkout Sessions: create, expire, complete via UI

Cursor-based pagination - All list endpoints support starting_after, ending_before, limit, created[gte], created[lte] via shared stripeList() helper. Returns { object: "list", has_more, data }.

expand[] support - Charges expand customer and payment_intent. Payment intents expand customer. Prices expand product. Uses applyExpand() helper.

Stripe-format errors - Returns { error: { type, code, message } } instead of generic error responses. Codes include resource_missing, payment_intent_unexpected_state.

11 webhook events - Dispatched on every state change: customer.created/updated/deleted, payment_intent.created/succeeded/canceled, charge.succeeded, product.created, price.created, checkout.session.completed/expired.

Cascading customer deletion - Deleting a customer nullifies customer_id on related payment intents, charges, and checkout sessions.

Form-urlencoded support - parseStripeBody() handles both JSON and application/x-www-form-urlencoded with bracket notation (metadata[key]=value).

Checkout UI

Uses the shared renderCardPage system from @internal/core:

Stripe checkout page

API flow

Payment flow

Testing

17 vitest tests covering: CRUD, form-urlencoded, Stripe error format, cursor pagination, email/status filtering, expand[] on payment intents and prices, cascading customer delete, payment flow, checkout sessions, and seed config. All 82 tests across all packages pass.

Scope

Covers the core payment flow with quality matching the existing GitHub plugin (pagination, filtering, webhooks on state changes, cascading operations, expand[]). Subscriptions, invoices, disputes, Connect, and refunds are left for follow-up PRs.

This contribution was developed with AI assistance (Claude Code).

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 22, 2026

@mvanhorn is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Collaborator

@ctate ctate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this excellent contribution! The Stripe emulator is a strong addition — stateful persistence and webhook delivery address real gaps that stripe-mock leaves open, and the code quality (pagination, expand[], cascading deletes) matches the existing plugins well.

Before merging, there are two things to address:

1. Bug: Checkout UI crashes when session has line items

The checkout page (checkout-sessions.ts:93) reads li.price_id, but line items arrive from the API with a price field (matching Stripe's real API format). When a session has line items, the price lookup fails and price.currency.toUpperCase() throws:

Cannot read properties of undefined (reading 'replace')

Fix: either map priceprice_id on insert (line 41), or read li.price instead of li.price_id in the render logic.

2. Inline styles break design consistency

The checkout page uses 6 inline style="..." attributes with a custom green accent (#22c55e), while every other emulator (GitHub, Google, Vercel) uses zero inline styles and the shared amber/gold accent (#fbbf24) from the core stylesheet. These should be extracted into CSS classes in the core UI system to stay consistent with the project's design patterns.


Everything else looks great — tests pass (82/82), build is clean, and the plugin structure follows the established patterns well. Once those two items are addressed this is good to go!

@mvanhorn
Copy link
Copy Markdown
Contributor Author

Both fixed in e8c5b0f:

  1. Line items now use price (matching Stripe's API format). Updated the entity type and the checkout render logic - the crash was from reading li.price_id when the field is li.price.

  2. Replaced all 6 inline styles with core CSS classes: .avatar for the $ icon, .user-btn/.user-form for the pay button, .card-footer for the cancel link, .check for the success state. The checkout page now uses the same amber/gold accent as the other emulators.

Also fixed the unawaited webhooks.dispatch() in the customer DELETE handler (VADE caught that one).

Updated checkout:

Checkout v2

@ctate
Copy link
Copy Markdown
Collaborator

ctate commented Mar 23, 2026

Actually sorry @mvanhorn - this branch is behind main. The design system was completely overhauled - from a dark-gray-with-amber look to a black-with-green-terminal aesthetic using pixel fonts

@mvanhorn mvanhorn force-pushed the feat/stripe-emulator branch from e8c5b0f to ff5aab4 Compare March 23, 2026 17:29
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Rebased onto main. The checkout page was already using core CSS classes (.org-row, .avatar, .user-btn, etc.) so no code changes were needed - the new terminal green theme comes through automatically.

Updated checkout:

Checkout v3

@ctate
Copy link
Copy Markdown
Collaborator

ctate commented Mar 23, 2026

Nice! Small nit: the "Cancel" button doesn't match the design system

@mvanhorn
Copy link
Copy Markdown
Contributor Author

Fixed - swapped emu-bar-service for btn-revoke on the cancel link.

Cancel v2

@riderx
Copy link
Copy Markdown

riderx commented Mar 27, 2026

hey @ctate can we have it deployed and reviewed :)

@ctate
Copy link
Copy Markdown
Collaborator

ctate commented Mar 27, 2026

@mvanhorn Do you mind rebasing again using the new @\emulators scope instead of @\internal?

Rebased onto main and migrated from @internal to @emulators scope.
Registered stripe in SERVICE_REGISTRY. All 17 stripe tests + full
suite passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mvanhorn mvanhorn force-pushed the feat/stripe-emulator branch from 5abd8d7 to b48f806 Compare March 27, 2026 03:47
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Rebased onto main in b48f806. Moved everything from @internal/stripe to @emulators/stripe, registered in SERVICE_REGISTRY, matched the package.json and tsup config to the other emulators. All 17 stripe tests + full suite passing.

Copy link
Copy Markdown
Collaborator

@ctate ctate left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates!

Changes requested

Bug: Charge metadata is silently dropped
When a charge is created from a confirmed payment intent, metadata is hardcoded to {} instead of copying from the payment intent. This means any webhook handler reading charge.metadata gets empty data. Should be metadata: updated.metadata.

Input validation: line_items is unvalidated
line_items is cast as any[] with no structural validation. Malformed input (missing price or quantity) is silently accepted and will blow up when rendering the checkout page. Please add basic shape validation.

Foreign key validation is missing
Prices accept non-existent product IDs, payment intents accept non-existent customer IDs, etc. Real Stripe returns 400 for these. An emulator that's more permissive than production gives false confidence — code passes locally then breaks in prod. Please validate foreign keys and return Stripe-style errors.

- Copy payment intent metadata to charge on confirm (was hardcoded {})
- Validate line_items shape: require price (string) and quantity (>0)
- Validate line_items price IDs exist in store
- Validate customer IDs on payment intents and checkout sessions
- Validate product IDs on prices
- Return Stripe-style 400 errors for invalid references
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Fixed in 113c6a4:

  1. Charge metadata - charges now copy updated.metadata from the payment intent instead of {}.

  2. line_items validation - each item is validated for shape (must be object), required price (string, must exist in store), and quantity (positive integer). Returns Stripe-style 400 errors with per-item param paths like line_items[0][price].

  3. Foreign key validation - prices validate product ID exists, payment intents and checkout sessions validate customer ID exists. All return resource_missing errors matching Stripe's format.

@ctate
Copy link
Copy Markdown
Collaborator

ctate commented Mar 31, 2026

Thank you @mvanhorn!

ctate added 2 commits March 31, 2026 23:15
Merge upstream/main into feat/stripe-emulator, combining the new
okta emulator (from vercel-labs#32) with the stripe emulator in the service
registry's SERVICE_NAME_LIST.
Merge upstream/main into feat/stripe-emulator, incorporating the
resend emulator (vercel-labs#7) and microsoft v1 OAuth updates (vercel-labs#30) alongside
the stripe emulator in the service registry and package dependencies.
@ctate ctate merged commit 61732e3 into vercel-labs:main Mar 31, 2026
3 of 4 checks passed
@mvanhorn
Copy link
Copy Markdown
Contributor Author

woohoo

@riderx
Copy link
Copy Markdown

riderx commented Apr 1, 2026

It is released ? i dont see it in the doc and readme

@ctate ctate mentioned this pull request Apr 2, 2026
@mvanhorn
Copy link
Copy Markdown
Contributor Author

mvanhorn commented Apr 5, 2026

Thanks for merging both emulators. Let me know if you want any adjustments to the stripe or resend implementations.

@mvanhorn
Copy link
Copy Markdown
Contributor Author

mvanhorn commented Apr 6, 2026

Appreciate the merge!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants