Skip to main content

Environment variables

The app reads its configuration from a .env file in the project root. Create this file before starting the development server.
.env
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
body.STRIPE_SECRET_KEY
string
required
Your Stripe secret API key. Used server-side in customer.ts and payment.ts to instantiate the Stripe client via new Stripe(STRIPE_SECRET_KEY). In test mode, this value starts with sk_test_. In live mode, it starts with sk_live_.
body.STRIPE_WEBHOOK_SECRET
string
required
The webhook signing secret used in webhook.ts to verify that incoming webhook requests originate from Stripe. Generated when you create a webhook endpoint in the Stripe Dashboard or by the Stripe CLI during local development. Starts with whsec_.
Rotate your secrets immediately if they are ever exposed. In the Stripe Dashboard, roll your secret key under Developers → API keys → Roll key. For webhooks, delete the compromised endpoint and create a new one to obtain a fresh signing secret. Never commit .env to version control.

Getting Stripe API keys

1

Log in to the Stripe Dashboard

Go to dashboard.stripe.com and sign in to your account.
2

Switch to test mode

Toggle the Test mode switch in the top-right corner of the Dashboard. All keys and resources created in test mode are isolated from live-mode data.
3

Open the API keys page

Navigate to Developers → API keys. You will see two keys:
  • Publishable key — starts with pk_test_. Used client-side in subscribe.tsx to initialize Stripe.js.
  • Secret key — starts with sk_test_. Used server-side only. Click Reveal test key to view it.
4

Copy the secret key to your .env file

.env
STRIPE_SECRET_KEY=sk_test_your_key_here
The publishable key is embedded directly in the client-side code (subscribe.tsx) and does not need to be in .env. It is safe to include in browser-facing code.

Webhook configuration

Stripe sends webhook events to your server when subscription-related actions occur (for example, when a checkout session completes or a payment fails). The app verifies each event using the webhook signing secret.

Local development with the Stripe CLI

The Stripe CLI is the fastest way to receive webhook events during local development. It creates a secure tunnel between Stripe and your local server without requiring a public URL.
1

Authenticate the CLI

stripe login
This opens a browser window to authorize the CLI with your Stripe account.
2

Start the webhook listener

stripe listen --forward-to localhost:5173/webhook
The CLI prints output similar to:
> Ready! Your webhook signing secret is whsec_abc123... (^C to quit)
Copy the whsec_ value.
3

Add the signing secret to .env

.env
STRIPE_WEBHOOK_SECRET=whsec_abc123...
Restart your dev server after updating .env.

Production webhook setup

1

Create a webhook endpoint in the Dashboard

Go to Developers → Webhooks → Add endpoint in the Stripe Dashboard.Set the endpoint URL to your production domain:
https://yourdomain.com/webhook
2

Select events to listen for

At a minimum, subscribe to the following events (these are the events the app handles):
  • invoice.paid
  • invoice.payment_failed
  • customer.subscription.deleted
3

Copy the signing secret

After creating the endpoint, click Reveal next to the signing secret and add it to your production environment:
STRIPE_WEBHOOK_SECRET=whsec_your_production_secret
Do not reuse a test-mode webhook signing secret in production. Each webhook endpoint in Stripe generates its own unique signing secret.

Session configuration

The app uses a cookie-based session to store authentication state. The session is configured in sessions.ts using React Router’s createCookieSessionStorage:
sessions.ts
const { getSession, commitSession, destroySession } =
  createCookieSessionStorage<SessionData, SessionFlashData>({
    cookie: {
      name: "__session",
      httpOnly: true,
      maxAge: 60,
      path: "/",
      sameSite: "lax",
      secrets: ["s3cret1"],
      secure: true,
    },
  });
OptionValueDescription
name__sessionThe name of the cookie sent in the browser.
httpOnlytruePrevents JavaScript from reading the cookie, reducing XSS exposure.
maxAge60Session expires after 60 seconds of inactivity. Extend this for longer sessions.
path/The cookie is sent for all routes under the root path.
sameSitelaxProtects against most CSRF attacks while allowing top-level navigations.
secrets["s3cret1"]Used to sign the cookie. Rotate this value in production.
securetrueThe cookie is only sent over HTTPS.
The default secrets value "s3cret1" is a placeholder. Replace it with a strong, randomly generated string before deploying to production. You can provide multiple secrets in the array — React Router will use the first secret to sign new cookies and accept any secret in the array for verification, which allows you to rotate secrets without immediately invalidating existing sessions.
The secure: true flag means the cookie will not be sent over plain HTTP. If you are running locally without HTTPS, you may need to set secure: false in your development environment. Never disable secure in production.

Stripe publishable key

The Stripe publishable key is used client-side in subscribe.tsx to initialize Stripe.js for the payment element. Unlike the secret key, the publishable key can safely appear in browser-side code.
subscribe.tsx
const stripe = loadStripe("pk_test_your_publishable_key_here");
Find your publishable key in the Stripe Dashboard under Developers → API keys. It starts with pk_test_ in test mode and pk_live_ in live mode.
When moving from test to live mode, update both STRIPE_SECRET_KEY in .env and the publishable key in subscribe.tsx to their respective live-mode values.

Build docs developers (and LLMs) love