stripe-payments — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited stripe-payments (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
@stripe/stripe-react-nativeNever put the Stripe secret key in frontend or mobile code. Only the publishable key goes client-side.
# Frontend
cd frontend && npm install @stripe/stripe-js @stripe/react-stripe-js
# Mobile
cd mobile && npx expo install @stripe/stripe-react-native# 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.
Create products in the Stripe dashboard or via CLI. Products represent what you sell; prices represent how much and how often.
price_...) — you'll reference this in codestripe products create --name="Pro Plan" --description="Full access"
stripe prices create \
--product=prod_xxx \
--unit-amount=999 \
--currency=usd \
--recurring[interval]=month// 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.
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# 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.localhttps://YOUR_PROJECT.supabase.co/functions/v1/stripe-webhookcheckout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failedwhsec_...) → add to Supabase secrets:supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_...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
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);| 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.
sk_test_ to sk_live_ keys in Supabase secretspk_live_ in frontend/mobile envWebhook 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.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.