feat(website): Stripe Checkout for paid pricing tiers (PR B-Stripe)#508
Merged
Conversation
Two parallel sub-PRs that close the @ngaf/chat relicense loop after PRs
A + B landed.
- PR B-Stripe: Stripe-hosted Checkout for Indie / Developer Seat /
App Deployment tiers; idempotent products/prices sync script;
/thanks page. One-time 12-month payments. Token minting handled by
the existing minting service webhook.
- PR C: deploy the existing apps/minting-service to Vercel
(threadplane-minting-service project already exists with the prod
private key); wire CACHEPLANE_LICENSE_PUBLIC_KEY into publish.yml so
published @ngaf/chat ships with the prod public key; fix the
runLicenseCheck idempotency bug; document provideChat({license}) and
exercise the verify path via one example. Enforcement stays
advisory-only; origin allowlist deferred.
Both specs include an end-to-end smoke-test runbook that uses
examples/chat/angular/ as the verification surface and is explicit
about reverting all temporary injections before commit, so the demo
stays clean in main.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9 tasks: tiers.config source of truth, generated price-IDs stub, Stripe client wrapper with sk_ guard, /api/checkout/session route, /thanks page, PricingGrid form swap for paid tiers, idempotent products/prices sync script, analytics events + env example, final verification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five-tier definition consumed by both the website's PricingGrid and the Stripe products/prices sync script. Stripe products are matched by metadata.ngaf_tier_slug, never by display name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Populated by scripts/stripe/sync-products.ts. The /api/checkout/session route detects an empty map and 503s with "checkout not yet configured." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
getStripe() refuses to load if STRIPE_SECRET_KEY is missing or doesn't begin with sk_. The key encodes test vs live mode, so we don't need explicit mode detection elsewhere. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST with { tier } returns a 303 redirect to Stripe-hosted Checkout for
the three buyable tiers (indie / developer_seat / app_deployment).
Community and Enterprise return 400. Quantity is clamped to [1, 100]
and adjustable_quantity is enabled only for Developer Seat. Returns
503 when STRIPE_PRICE_IDS is empty (sync-products hasn't run yet).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tells buyers their @ngaf/chat license token will be emailed within a few minutes, links to installation docs and support. Static server component; reads no query params at runtime — Stripe handles the session_id reconciliation via webhook in the minting service. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Tier data now sourced from pricing/tiers.config.ts (single source of truth shared with the Stripe sync script). - Paid tiers (Indie / Developer Seat / App Deployment) submit a POST form to /api/checkout/session which 303-redirects to Stripe-hosted Checkout. - Community keeps the npm link; Enterprise keeps /contact. - Tracking events unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mports apps/website (PricingGrid + /api/checkout/session route) and scripts/stripe (sync-products) both import the repo-root pricing/ config files. They live outside any Nx project on purpose — they're the source of truth shared between display and Stripe sync. The @nx/enforce-module-boundaries rule needs an allow entry to permit this; the alternative is making pricing/ an Nx library, which is overkill for two config files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reads pricing/tiers.config.ts and ensures each stripeBuyable tier has a Stripe product (matched by metadata.ngaf_tier_slug) and exactly one active one-time USD price. Writes pricing/tiers.generated.ts with the resulting price IDs. Re-runnable; stale prices are archived when unit_amount changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
6 tasks: runLicenseCheck idempotency bug fix + regression test;
inject CACHEPLANE_LICENSE_PUBLIC_KEY into publish.yml; add
minting-deploy job to ci.yml mirroring existing Vercel deploy
patterns; document provideChat({license}) in libs/chat/README;
wire examples/chat/angular to read optional license token; final
verification + operational checklist for PR description.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adding stripe to apps/website/package.json without regenerating package-lock.json broke npm ci in CI. The root workspace already ships stripe ^22.0.2, so the import resolves fine without the explicit declaration. (Memory: regenerating package-lock.json on macOS drops Linux @next/swc-* bindings and breaks CI.) Local build + tests still pass; this is a pure lockfile-hygiene fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI installed stripe@22.0.x (LatestApiVersion = '2026-03-25.dahlia')
while local pnpm had 22.1.x ('2026-04-22.dahlia'). The hardcoded
literal failed the website build's TypeScript narrowing.
Omitting apiVersion lets the Stripe lib use whichever default ships
with the installed version. Account-level API version pinning still
applies; we just don't override it from code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the
/contact?source=pricing_tier_<slug>placeholder CTAs on/pricingwith real Stripe-hosted Checkout. Three buyable tiers (Indie, Developer Seat, App Deployment) now POST to/api/checkout/session, which 303-redirects tocheckout.stripe.com. Community keeps the npm link; Enterprise keeps/contact.One-time 12-month payments (no subscriptions per locked brainstorm). License token minting is handled by the existing
apps/minting-service/webhook handler — out of scope here; PR C deploys it.What's in the PR
pricing/tiers.config.ts— typed source of truth for all 5 tiers (3 stripeBuyable). Consumed byPricingGridand the sync script.pricing/tiers.generated.ts— empty Stripe-IDs map; populated by the sync script. Committed empty so the route 503s gracefully until products are created.scripts/stripe/sync-products.ts— idempotent products/prices sync. Identifies products bymetadata.ngaf_tier_slug(never by name). Re-runnable; stale prices are archived whenunit_amountchanges. 3 unit tests against aStripestub.apps/website/src/lib/stripe.ts— Stripe client wrapper that refuses to load ifSTRIPE_SECRET_KEYis missing or doesn't begin withsk_. 3 unit tests.apps/website/src/app/api/checkout/session/route.ts— Next.js Route Handler. Accepts both JSON and form POST. 7 unit tests covering invalid tiers (400), community/enterprise (400), happy path (303),adjustable_quantityon Developer Seat, quantity clamping[1, 100], Stripe-returned-no-URL (502).apps/website/src/app/thanks/page.tsx— server component shown after Checkout success. Tells buyer their license token is on its way. 3 unit tests.apps/website/src/components/pricing/PricingGrid.tsx— refactored to read frompricing/tiers.config.ts; paid tiers submit a POST form to/api/checkout/session. Community + Enterprise hrefs unchanged.apps/website/src/lib/analytics/events.ts— addedmarketing:checkout_startedandmarketing:checkout_succeededevents.eslint.config.mjs— added one allow pattern forpricing/tiers.{config,generated}since they're shared betweenapps/websiteandscripts/stripeand live outside any Nx project on purpose.Test plan
cd apps/website && npx vitest run→ 17 files, 72 tests passing (was 14/59 before this PR; +3 stripe.spec + 7 route.spec + 3 thanks.spec).npx vitest run scripts/stripe/sync-products.spec.ts→ 3 passed.npx nx run website:lint→ success.npx nx build website→ success (build does not require Stripe env vars or populatedSTRIPE_PRICE_IDS).apps/website/,pricing/,scripts/stripe/,docs/superpowers/,eslint.config.mjs, and two.env.examplefiles touched.Operational steps for go-live (after merge)
STRIPE_SECRET_KEY(test mode) in the Vercelthreadplanewebsite project.STRIPE_SECRET_KEY=sk_test_... pnpm tsx scripts/stripe/sync-products.ts— populatespricing/tiers.generated.tswith real price IDs; commit the result./pricing, click "Buy indie license", complete with4242 4242 4242 4242. Confirm redirect to/thanksand Stripe Dashboard shows the session withmetadata.ngaf_tier_slug=indie.Out of scope (deliberately)
checkout.session.completed; PR C deploys it to production.STRIPE_SECRET_KEYvalue and re-running the sync script withsk_live_….Risks
/thankscopy says "within a few minutes" to leave room for manual fulfillment if needed.../../../../../../../pricing/...) from the route handler is ugly but correct; the alternative (apathsalias) didn't prove necessary. ESLint's nx-module-boundaries rule now explicitly allows it via a single regex entry.🤖 Generated with Claude Code