Add example-stripe: subscription checkout with emulate + drizzle + vitest#47
Add example-stripe: subscription checkout with emulate + drizzle + vitest#47remorses wants to merge 8 commits into
Conversation
Add Simplify<T> that applies & {} only for non-scalar types (preserving
null, undefined, string, number, boolean, etc.) and wrap the FetchResult
usage inside ResolveResult so IDE hovers show the resolved data | error
union instead of nested conditional type aliases.
Session: ses_1fd3de59bffeb7u2hfIFs6aETI
Standalone "use server" action files (separate from the app entry) are
safe to use router.href() since they do not feed return types back into
typeof app. This gives full compile-time validation of redirect paths
and params against the route table.
README:
- "Redirecting After Actions" section now shows router.href() as the
preferred pattern for standalone action files, with a concrete example
- Keeps the inline action example using plain string redirect (to avoid
circular type issues when used inside app-entry handlers)
- Clarifies the circular-type details callout: standalone "use server"
files are explicitly called out as safe to use router.href()
example-better-auth/src/actions.ts:
- redirect("/login") -> redirect(router.href("/login"))
- redirect(`/orgs/${id}/dashboard`) -> redirect(router.href("/orgs/:orgId/dashboard", { orgId: id }))
- redirect(`/orgs/${orgId}/dashboard`) -> redirect(router.href("/orgs/:orgId/dashboard", { orgId }))
example-forms/src/actions.ts:
- redirect(`/success?name=...&id=...`) -> redirect(router.href("/success", { name, id }))
(router.href handles URL encoding, no more manual encodeURIComponent)
Session: ses_1f8467f19ffeO06UZBIiU4UN5R
Type-level changes:
- PathHasRequiredQuery: new helper type that detects required keys in
query schemas using the {} extends T pattern
- ValidatedHref: accepts QS parameter, rejects bare string paths for
routes with required query (forces using router.href() instead)
- HrefArgs/HrefArgsInner: require the query arg when schema has
required fields, allow omission when all fields are optional
- QueryForPath: resolves pattern via MatchingPathPattern so resolved
paths like /users/123 still enforce /users/:id query schema
- MergeParamsAndQuery: no longer wraps Q in Partial<>, keeping
required query keys required when merged with path params
- LinkProps/NavigationTo: thread RouterQuerySchemas so Link and
router.push/replace reject bare string paths with required query
Runtime changes:
- Page routes skip query validation (coerce only, no throw) so
missing query params render the page instead of showing 422.
API routes still return 422 for invalid query.
Tests:
- 20+ @ts-expect-error assertions for href, Link, router.push,
router.replace, and createSpiceflowFetch
- Resolved dynamic path enforcement test (e.g. /users/123)
- API route 422 validation test
- e2e test: page with required query renders without error
Session: ses_1f836c98cffeedSRmMWpcf3gcd
…7.0.0.1 The previous approach defaulted server.host to 127.0.0.1, which made Vite print http://127.0.0.1:5173 in the terminal instead of http://localhost:5173. dns.setDefaultResultOrder(verbatim) prevents Node 17+ from reordering DNS results to prefer IPv6, so localhost still resolves to 127.0.0.1 (IPv4). This keeps the same port-collision protection while Vite shows the familiar localhost URL. Session: ses_1f7fce5d1ffex4MEG1SOaSqKMM
…e, vitest
Spiceflow app demonstrating a full Stripe subscription purchase flow
tested end-to-end with the emulate Stripe emulator.
**App routes:**
- GET / — pricing page
- GET /success — post-checkout confirmation
- GET /dashboard/:orgId — shows subscription status (loader + page)
- GET /checkout/:orgId — creates Stripe Checkout Session, redirects to
hosted checkout. Hardcoded price ID (price_pro_monthly) seeded in
the emulator. Redirects to dashboard if already subscribed.
- POST /api/portal — creates Billing Portal session
- POST /api/webhooks/stripe — webhook handler with signature verification
- GET /api/org/:orgId — returns org + subscriptions (used by tests)
**Stripe patterns:**
- Single customer per org via getOrCreateStripeCustomer()
- Lazy Stripe SDK client (reads STRIPE_HOST/PORT/PROTOCOL env vars on
first call so tests can point at the emulator)
- Idempotent subscription upserts keyed on (subscriptionId, variantId)
- Webhook signature verification supporting both Stripe stripe-signature
(production) and X-Hub-Signature-256 (emulate emulator)
**Test setup:**
- app.listen(0) for ephemeral port
- createEmulator({ service: "stripe" }) with seed data for product,
price, and webhook endpoint (including secret for HMAC signing)
- Emulator delivers webhook on checkout complete; test polls until
subscription appears in DB
**Known limitation:** the emulate Stripe emulator dispatches webhooks
with X-Hub-Signature-256 (GitHub format) instead of stripe-signature
(Stripe format). The webhook handler accepts both. Tracked upstream:
vercel-labs/emulate#98
Session: ses_YUiOcLKIjF
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 26194761dd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| server: { | ||
| host: userConfig.server?.host ?? '127.0.0.1', | ||
| }, | ||
| server: {}, |
There was a problem hiding this comment.
Keep pages reachable on 127.0.0.1
When server.host is left unset, Vite falls back to localhost (its documented default), and on hosts where localhost resolves to ::1 first the dev server binds only IPv6; Node then refuses http://127.0.0.1:<port>. I checked integration-tests/playwright.config.ts starts dev with just pnpm dev --port ..., while integration-tests/e2e/basic.test.ts still fetches 127.0.0.1 directly, so those dev e2e checks can fail on common Linux/CI host ordering. Keeping the previous IPv4 default, or updating the tests/server config together, avoids this split.
Useful? React with 👍 / 👎.
| context.query = await runValidation( | ||
| coerceQueryWithSchema(context.query, route?.route?.hooks?.query), | ||
| route?.route?.validateQuery, | ||
| isPageRoute ? undefined : route?.route?.validateQuery, | ||
| ) |
There was a problem hiding this comment.
Preserve page query schema output
Skipping validateQuery for every page means handlers now receive only the raw/coerced URL object instead of the schema's parsed value. For example, a page query like z.object({ page: z.coerce.number().default(1) }) previously gave query.page === 1 when omitted, but now the handler sees undefined despite the TypeScript type; transforms, defaults, enum/refinement rejection, and other Standard Schema output are also bypassed. If the goal is only to avoid 422 pages for missing required params, this needs a narrower fallback so successful/defaulted parses still feed the handler.
Useful? React with 👍 / 👎.
- enforce required query params at compile time across all typed APIs - simpler typed fetch hover types - fix IsBodyRequired for all-optional body schemas - dev server shows localhost instead of 127.0.0.1
- use 127.0.0.1 instead of localhost in webhook URL to avoid IPv6 resolution issues on Linux + Node 18+ - use random emulator port (14000-14999) to avoid conflicts when multiple CI jobs share a runner - increase poll timeout from 5s to 15s for slow CI environments Session: ses_1f76364feffeiL5oKLWrda39Tu
…izeDeps Vite discovered these deps at runtime in the RSC environment, triggering a program reload that raced with the Cloudflare worker module runner. The stale chunk reference caused "file does not exist" errors. Pre-including them avoids the mid-startup discovery and reload. Session: ses_1f76364feffeiL5oKLWrda39Tu
Full Stripe subscription checkout example app using spiceflow, drizzle (SQLite), and the
emulateStripe emulator for e2e testing.What's in it:
GET /checkout/:orgIdcreates a Checkout Session and redirects to hosted checkoutprice_pro_monthlyseeded in the emulatorEmulator webhook quirk: the
emulatepackage dispatches webhooks withX-Hub-Signature-256(GitHub format) instead ofstripe-signature. The webhook handler accepts both formats as a workaround. Tracked upstream: vercel-labs/emulate#98