Skip to content
Chapter 64Lesson 8

Quiz - Stripe billing and plan entitlements

Quiz progress

0 / 0

A teammate gates the Pro analytics panel by calling stripe.subscriptions.retrieve(...) at the top of the Server Component, reasoning “Stripe is the source of truth, so this is always correct.” The code is correct. Why is it still the wrong design?

It puts a network round-trip to Stripe on the hot path of every render — adding latency, burning rate limits under load, and making Stripe’s uptime your app’s uptime. Read the local plan_entitlements projection instead; call Stripe only off the request path.

stripe.subscriptions.retrieve returns a stale cached object, so the gate can show the wrong plan; you need the local row because it’s fresher than Stripe.

Server Components aren’t allowed to make outbound network calls during render, so the call throws at runtime; the local read is the only thing that works there.

You stamp metadata: { organization_id: org.id } onto the Stripe Customer (and Subscription) at creation. What problem does this specifically solve?

When a webhook event later arrives, the metadata rides along on the event payload, so your handler reads organization_id straight off it and knows which org’s row to update — no lookup table mapping cus_… back to an org.

It lets the app store org-specific billing data on Stripe instead of in its own database, keeping the local schema smaller.

It lets Stripe enforce one Customer per organization, rejecting a second Customer create for the same organization_id.

Right after Checkout, the user lands on /billing/success?session_id=.... It would be easy to read the session id, confirm it’s paid, and write the Pro entitlement from the success page. Why does the chapter forbid this?

It makes the success page a second writer racing the webhook for the same row — the dual-writer hazard the webhook design exists to remove. The page must only read-and-poll; the webhook is the single writer that provisions the entitlement.

The session_id in the URL is forgeable, so the success page can never tell whether the user actually paid and must wait for an email confirmation instead.

Writing from the success page is slower than the webhook, so the user would see “Finalizing…” for longer than necessary.

After a customer returns from the Customer Portal to your return_url, a developer wants the billing page to “sync” by calling refreshEntitlementFromStripe(org.id) on render. What’s wrong with treating the return as a signal to update state?

The return_url fires no matter what the customer did — changed plan, browsed, or nothing — so it proves only that they came back. Acting on it either double-writes a row the webhook already owns, or reads a projection the webhook hasn’t updated yet. Do nothing stateful on return; read and poll.

The return URL omits the subscription id, so refreshEntitlementFromStripe can’t know which subscription changed and updates the wrong row.

Stripe rate-limits the return redirect, so calling Stripe again on render risks a 429 that breaks the billing page.

Given the five statuses your row stores, for which one does hasActiveAccess return false even though, mid-cycle, the customer might still be inside a period they’ve paid for?

canceled — and the “paid through the end of the month after cancelling” case never reaches it, because Stripe keeps that subscription active (with cancelAtPeriodEnd: true) until the period actually ends. By the time the status reads canceled, access is genuinely over.

canceled — so the gate must special-case it, granting access while currentPeriodEnd is still in the future and only denying once that date passes.

past_due — a failed renewal means the paid period has lapsed, so access is denied while Stripe retries the card.

A Pro-only export runs inside a Server Action. When requirePlan('pro') throws a BillingError for an org that’s canceled, how should the action respond, and what Result code does it use?

Catch the BillingError, return err('forbidden', error.message), and re-throw anything that isn’t a BillingError. There’s no payment_required code — a tier refusal maps to forbidden, and the billing-specific reason already lives on BillingError.code.

Let the BillingError propagate so the segment’s error.tsx boundary catches it and renders the upgrade screen — the same as a Server Component.

Catch it and add a new payment_required member to the Result error union so the form can distinguish a paywall from an ordinary forbidden.

Run the three-question wrapper test on Cloudflare R2’s storage SDK: the call is verbose (PutObjectCommand({ Bucket, Key, Body, ContentType })), R2/S3/Backblaze are interchangeable, and it’s used in exactly two places with no cross-cutting rule. What’s the verdict?

A helper. The shape is read-hostile and the swap cost is real (two yeses), but there’s no discipline that must be centralized and the call sites don’t multiply — so a presignedPut(key) helper wraps the verbosity while the SDK stays importable. An interface needs three-of-three.

An interface in /lib. Verbose shape plus a real swap cost is enough to forbid the SDK outside one directory, the same as billing.

A direct call. With only two call sites there’s nothing worth wrapping, so app code should construct PutObjectCommand inline at each site.

Quiz complete

Score by topic