> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tryrekkal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Stripe

# Stripe

How `rekkal` integrates Stripe for payments: the server client, the webhook Route Handler, and plan-to-price-ID mapping.

## Environment variables

Stripe reads four variables (all listed in [`.env.example`](../.env.example)):

* `STRIPE_SECRET_KEY` — secret API key. Server-only; never import into a `"use client"` module.
* `STRIPE_WEBHOOK_SECRET` — signing secret used to verify incoming webhook events.
* `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` — publishable key, safe to expose to the browser. Reserved for client-side Stripe.js/Elements; the current server-side redirect integration never reads it.
* `NEXT_PUBLIC_STRIPE_PRICE_PRO` — the Stripe Price ID backing the Pro plan (consumed in `config.ts`).

## The server client — `lib/stripe.ts`

`getStripe()` in [`lib/stripe.ts`](../lib/stripe.ts) returns a memoized `Stripe` instance, constructed lazily on first use from `STRIPE_SECRET_KEY`. Deferring construction (rather than building at module load) lets the app build without the key present — it is only required at request time. The `apiVersion` is pinned so upgrading the Stripe SDK never silently changes API behavior; bump it deliberately.

## Checkout and portal — `app/actions/billing.ts`

Two server actions drive billing from inside the app:

* **`createCheckoutSession(priceId)`** — creates a `mode: "subscription"` Checkout Session for the given price and redirects the user to Stripe's hosted Checkout. It reuses the user's existing Stripe customer when one is on file, and stamps the user id into both session and `subscription_data` metadata so the webhook can attribute events back to the user. Following Stripe best practice, it does **not** pass `payment_method_types` (dynamic payment methods are chosen from the Dashboard). Wired to the pricing CTA via the client `ButtonCheckout`.
* **`createBillingPortalSession()`** — opens the Stripe Billing Portal for the user's customer so they can upgrade, downgrade, cancel, or update payment methods. Wired to `ButtonPortal` on the account page (`/dashboard/settings`), which is also where both Stripe redirects land — see [dashboard.md](./dashboard.md).

## The webhook — `app/api/webhooks/stripe/route.ts`

Stripe calls into the app from outside, so it is handled by a Route Handler, not a Server Action. [`app/api/webhooks/stripe/route.ts`](../app/api/webhooks/stripe/route.ts) exports a `POST` handler that:

1. Reads the **raw** request body with `request.text()` — Stripe signs the exact bytes, so the body must not be parsed or re-serialized before verification.
2. Verifies the `stripe-signature` header against `STRIPE_WEBHOOK_SECRET` via `constructEvent`, returning `400` if verification fails.
3. Dispatches on `event.type` (`checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`) and **upserts the user's `subscriptions` row** via the service-role Supabase client (which bypasses RLS), returning `200` with `{ received: true }`.

Handling is idempotent: every event maps to a single upsert keyed on `user_id`, so re-delivered events converge to the same row. The pure Stripe→row mapping lives in [`lib/subscriptions.ts`](../lib/subscriptions.ts) and is unit-tested. Note that as of API version `2026-06-24.dahlia`, `current_period_end` is a **per-subscription-item** field, so the mapping reads it from `subscription.items.data[0]`. On a successful checkout the handler also sends a best-effort "subscription active" email (see [email.md](./email.md)).

## Plans and price IDs — `config.ts`

Billing plans live in [`config.ts`](../config.ts) under `config.plans` (`free` and `pro`). Each plan carries its `stripePriceId`: the free tier is `null` (not billed through Stripe), and the Pro plan reads `NEXT_PUBLIC_STRIPE_PRICE_PRO` so the ID differs between test and live Stripe accounts. Import plan data from `config.ts` rather than duplicating price IDs across the codebase. Displayed prices are formatted in `config.currency`, which must match the currency the Stripe prices were created in — see its doc comment in [`config.ts`](../config.ts).

## Local webhook testing

Point a Stripe webhook endpoint at `/api/webhooks/stripe`. For production this is a Vercel URL (see [deployment.md](./deployment.md)); locally, forward events with the Stripe CLI (`stripe listen --forward-to localhost:3000/api/webhooks/stripe`), which prints the `STRIPE_WEBHOOK_SECRET` to use.

## Further reading

The Stripe skills (`stripe-docs`, `stripe-best-practices`, `stripe-directory`, `stripe-projects`, `connect-recommend`, `upgrade-stripe`) are pre-bundled (see [agent-skills.md](./agent-skills.md)).
