code/tamagui.dev/doc/stripe-setup.md
This document explains how to properly test Stripe subscriptions in the local development environment for Tamagui, including webhook configuration, database caveats, and reset operations.
stripe listenGo to your Stripe Dashboard and toggle "Test mode" in the top-right corner.
.envFrom the Developers > API keys section in the dashboard, copy the following:
pk_test_...sk_test_...Update your .env as follows:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxxxxxxxxxxxxxx
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxx
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
NEXT_PUBLIC_SUPABASE_URL=http://localhost:54321
The Tamagui site handles Stripe subscriptions in the following way:
create-subscription+api.tsupgrade-subscription+api.tsThese endpoints initiate the Stripe checkout session.
webhook+api.tsThis endpoint receives Stripe events and persists subscription data into Supabase.
Stripe events like checkout.session.completed, customer.subscription.created, and invoice.paid are handled here.
To capture webhook events locally while running your dev server, use the following command:
stripe listen --load-from-webhooks-api --forward-to http://localhost:8081/api/stripe/webhook
This forwards all webhook events configured in your Stripe dashboard to your local server.
๐ Ref: Stripe Webhook Docs
Since you're using Stripe in test mode and Supabase in production mode, a mismatch can occur:
customers table in Supabase uses a foreign key to auth.user.idUse the primary key (auth.user.id) to find and delete the corresponding record from the customers table manually or via Supabase UI/SQL.
Use the provided script to remove active subscriptions for a user:
cd code/tamagui.dev
node ./scripts/cancel-subscription.mjs <USER_ID>
This script will:
subscriptions table in Supabase with status: canceled๐ Script file:
code/tamagui.dev/scripts/cancel-subscription.mjs
A user is considered to be on the "Pro" plan if they have an active or trialing subscription named "Tamagui Pro".
This status is determined by checking the subscriptions table in Supabase.
๐ Logic from:
code/tamagui.dev/features/user/subscription/eligibility.ts
const hasProductAccess = (subscriptions, productName) =>
subscriptions?.some(
(sub) =>
(sub.status === 'active' || sub.status === 'trialing') &&
sub.subscription_items?.some((item) => item.price?.product?.name === productName)
)
This logic is used to check access to specific features like Pro, Chat, or Support tiers.
.env with Stripe test keyscustomers tablestripe listen in parallel with the dev serverCauses:
payment_method.idSolutions:
sk_test_... or sk_live_... keypaymentMethods.list to view available methodsProduction geographic pricing only trusts CF-IPCountry when Cloudflare proves
that the request passed through its edge. The proof uses one shared secret:
http_request_late_transform ruleset sets
X-Origin-Secret for tamagui.dev and www.tamagui.dev requests.tamagui service stores the matching value as
CF_ORIGIN_SECRET.features/geo-pricing/trustedCountry.ts compares the values before reading
CF-IPCountry. Direct requests to the Railway origin therefore cannot spoof a
discounted country.Generate a new random secret when configuring or rotating this pair. Never commit the value. Updating only one side disables geographic pricing safely by returning no country and no discount.
After any Cloudflare rule or Railway variable change, wait for the Railway deployment to finish and verify the public route:
curl -sS https://tamagui.dev/api/parity-discount
The response must contain a real two-letter country. A response with
"country": null means the Cloudflare rule and Railway secret do not match or
one side is missing.