Stripe payments
Skill Ampli-Group/agentic-mobile-blueprint/.agents/skills/stripe-payments
The production-ready foundation for shipping mobile and web apps. Auth, deployment, monitoring, and CI/CD — already wired together.
npx -y skills add Ampli-Group/agentic-mobile-blueprint --skill stripe-paymentsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Integrate Stripe payments into the app — products, prices, checkout sessions, webhooks, customer portal, and subscription management. Use when setting up payments from scratch, adding a new product/plan, configuring webhooks, or debugging payment flows.
SKILL.md
8.7 KB, as published. Nobody here has run it
Stripe Payments
Stack
- Stripe SDK (server-side): Supabase Edge Functions handle all Stripe API calls
- Stripe.js (client-side): Frontend and mobile use Stripe's hosted checkout or
@stripe/stripe-react-native - Webhooks: Stripe → Edge Function → Supabase DB (source of truth for subscription state)
Never put the Stripe secret key in frontend or mobile code. Only the publishable key goes client-side.
Setup
Install dependencies
# Frontend
cd frontend && npm install @stripe/stripe-js @stripe/react-stripe-js
# Mobile
cd mobile && npx expo install @stripe/stripe-react-native
Environment variables
# supabase/functions/.env.local
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# frontend/.env.local
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# mobile/.env.local
EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
Get keys from dashboard.stripe.com/test/apikeys.
Products and Prices
Create products in the Stripe dashboard or via CLI. Products represent what you sell; prices represent how much and how often.
Stripe Dashboard (manual, recommended for first setup)
- dashboard.stripe.com/products → Add product
- Fill in name, description, image
- Add pricing:
- One-time: flat amount
- Recurring: monthly/annual, amount
- Copy the Price ID (
price_...) — you'll reference this in code
Via Stripe CLI (scriptable)
stripe products create --name="Pro Plan" --description="Full access"
stripe prices create \
--product=prod_xxx \
--unit-amount=999 \
--currency=usd \
--recurring[interval]=month
Edge Function: Create Checkout Session
// supabase/functions/create-checkout/index.ts
import Stripe from "npm:stripe@14";
import { createClient } from "npm:@supabase/supabase-js@2";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!);
Deno.serve(async (req) => {
const { priceId, userId, returnUrl } = await req.json();
const session = await stripe.checkout.sessions.create({
mode: "subscription", // or "payment" for one-time
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${returnUrl}?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: returnUrl,
metadata: { userId }, // passed back in webhook
allow_promotion_codes: true,
billing_address_collection: "auto",
});
return new Response(JSON.stringify({ url: session.url }));
});
Redirect the user to session.url — Stripe handles the entire payment flow.
Edge Function: Webhooks
Webhooks are how Stripe tells your app about payment events. This is the source of truth — never trust client-side "payment successful" redirects.
// supabase/functions/stripe-webhook/index.ts
import Stripe from "npm:stripe@14";
import { createClient } from "npm:@supabase/supabase-js@2";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!);
const webhookSecret = Deno.env.get("STRIPE_WEBHOOK_SECRET")!;
Deno.serve(async (req) => {
const signature = req.headers.get("stripe-signature")!;
const body = await req.text();
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
} catch {
return new Response("Invalid signature", { status: 400 });
}
const supabase = createClient(
Deno.env.get("PUBLIC_SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.CheckoutSession;
const userId = session.metadata?.userId;
await supabase.from("subscriptions").upsert({
user_id: userId,
stripe_customer_id: session.customer as string,
stripe_subscription_id: session.subscription as string,
status: "active",
});
break;
}
case "customer.subscription.updated":
case "customer.subscription.deleted": {
const sub = event.data.object as Stripe.Subscription;
await supabase.from("subscriptions")
.update({ status: sub.status })
.eq("stripe_subscription_id", sub.id);
break;
}
}
return new Response(JSON.stringify({ received: true }));
});
Key events to handle:
checkout.session.completed— user paid, activate subscriptioncustomer.subscription.updated— plan changed, update statuscustomer.subscription.deleted— cancelled, downgrade accessinvoice.payment_failed— payment failed, notify user
Webhook Setup
Local development
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Forward webhooks to local edge function
stripe listen --forward-to http://localhost:54321/functions/v1/stripe-webhook
# Copy the webhook signing secret printed by the CLI → STRIPE_WEBHOOK_SECRET in .env.local
Production
- dashboard.stripe.com/webhooks → Add endpoint
- Endpoint URL:
https://YOUR_PROJECT.supabase.co/functions/v1/stripe-webhook - Events to send:
checkout.session.completed,customer.subscription.updated,customer.subscription.deleted,invoice.payment_failed - Copy Signing secret (
whsec_...) → add to Supabase secrets:
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_...
Customer Portal
Let users manage their subscription (cancel, update payment method, change plan):
// supabase/functions/create-portal-session/index.ts
const session = await stripe.billingPortal.sessions.create({
customer: stripeCustomerId, // from your subscriptions table
return_url: returnUrl,
});
return new Response(JSON.stringify({ url: session.url }));
Enable and configure the portal: dashboard.stripe.com/settings/billing/portal
- Set which plans users can switch to
- Enable/disable cancellation
- Configure cancellation flow (survey, retention offers)
Database Schema
create table subscriptions (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users not null,
stripe_customer_id text unique,
stripe_subscription_id text unique,
stripe_price_id text,
status text not null default 'inactive', -- active | past_due | canceled | trialing
current_period_end timestamptz,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- RLS: users can read their own subscription
alter table subscriptions enable row level security;
create policy "users read own" on subscriptions for select using (auth.uid() = user_id);
Test Cards
| Card | Scenario |
|---|---|
4242 4242 4242 4242 | Successful payment |
4000 0025 0000 3155 | Requires 3D Secure |
4000 0000 0000 9995 | Declined |
4000 0000 0000 0341 | Attaches successfully but payment always fails |
Any future expiry, any CVC, any postal code.
Go Live Checklist
- Switch from
sk_test_tosk_live_keys in Supabase secrets - Switch publishable key to
pk_live_in frontend/mobile env - Create new webhook endpoint pointing to production (test and live webhooks are separate)
- Verify products/prices exist in live mode (test mode data doesn't carry over)
- Test a real payment with a real card
- Enable Radar fraud rules: dashboard.stripe.com/radar/rules
Gotchas
Webhook signature verification fails — The webhook secret for local CLI (whsec_... from stripe listen) is different from the production dashboard secret. Use the right one per environment.
Missing SUPABASE_SERVICE_ROLE_KEY — Webhook handler must use the service role key to bypass RLS when writing subscription data. The publishable key won't work.
Duplicate webhook events — Stripe can deliver the same event more than once. Use upsert instead of insert, or check for existing records before writing.
Subscription not activating — Webhook is not reaching the function. Check Stripe dashboard → Webhooks → Recent deliveries for errors.
Apple/Google take 30% cut on mobile — If your app sells digital goods or subscriptions through the mobile app, Apple and Google require you to use their in-app purchase system (not Stripe). Stripe is fine for web checkout. This is the "App Store tax" — plan around it.