Pagamenti con Stripe
Quasi tutti i progetti di Fattibile incassano allo stesso modo: il sito non tocca mai i dati della carta, il prezzo lo decide il server, e solo Stripe può dire "pagato". Cambia solo cosa si sblocca dopo il pagamento (un piano, dei crediti, un ordine): quello è scritto nella frase "Pagamenti con Stripe" di ogni scheda.
Questa guida contiene i pezzi comuni: la funzione che crea il pagamento, il webhook che riceve la conferma da Stripe, il portale per gestire l'abbonamento e come provare tutto senza soldi veri.
Come funziona in 4 passaggi
- L'utente clicca "Abbonati" o "Compra": il sito chiama una Edge Function dicendo solo QUALE pacchetto vuole, mai il prezzo.
- La Edge Function crea una sessione di Stripe Checkout con il prezzo deciso da lei e restituisce il link della pagina di pagamento di Stripe.
- L'utente paga sulla pagina di Stripe: il sito non vede mai la carta.
- Stripe chiama il webhook, che controlla la firma e aggiorna il database: piano Pro, crediti o ordine pagato.
Il passaggio 4 è il più importante: se lo stato "pagato" lo potesse cambiare il browser, chiunque potrebbe sbloccare tutto senza pagare.
La frase da dare all'AI
Pagamenti con Stripe: le regole comuni
Da copiare in Lovable, Bolt o nello strumento che usi, dopo le frasi della scheda.
Aggiungi i pagamenti con Stripe Checkout seguendo queste regole:
1. Crea la Edge Function "crea-pagamento": riceve dal browser solo il nome del pacchetto scelto, prende il prezzo da un listino scritto nel codice della funzione, crea la sessione di Stripe Checkout con client_reference_id uguale all'id dell'utente (o della riga da pagare) e restituisce il link.
2. Crea la Edge Function "stripe-webhook", pubblicata con --no-verify-jwt: verifica la firma di Stripe con STRIPE_WEBHOOK_SECRET e, solo se è valida, aggiorna il database con la service role key.
3. Dal browser non deve essere possibile cambiare piano, crediti o stato del pagamento: le tabelle lo impediscono con le regole di sicurezza.
4. Per gli abbonamenti aggiungi nel profilo il pulsante "Gestisci abbonamento" che apre il portale clienti di Stripe.
5. Chiavi segrete (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, SUPABASE_SERVICE_ROLE_KEY) solo nei segreti delle Edge Function, mai nel codice del sito.La funzione che crea il pagamento
Edge Function crea-pagamento
Il listino sta qui, sul server: il browser manda solo il nome del pacchetto. Gestisce sia i pagamenti singoli (crediti, ordini) sia gli abbonamenti.
// supabase/functions/crea-pagamento/index.ts
import Stripe from "npm:stripe";
import { createClient } from "npm:@supabase/supabase-js";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!, { httpClient: Stripe.createFetchHttpClient() });
const admin = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!);
const cors = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
// Listino deciso dal server (prezzi in centesimi). Per gli abbonamenti si usa la lookup key del prezzo su Stripe.
const LISTINO: Record<string, { nome: string; centesimi?: number; crediti?: number; abbonamento?: string }> = {
crediti_10: { nome: "10 crediti", centesimi: 399, crediti: 10 },
crediti_30: { nome: "30 crediti", centesimi: 899, crediti: 30 },
pro_mensile: { nome: "Piano Pro", abbonamento: "pro_month" },
};
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: cors });
const token = req.headers.get("Authorization")?.replace("Bearer ", "") ?? "";
const { data: { user } } = await admin.auth.getUser(token);
if (!user) return new Response("Accesso richiesto", { status: 401, headers: cors });
const { pacchetto } = await req.json();
const scelta = LISTINO[pacchetto];
if (!scelta) return new Response("Pacchetto sconosciuto", { status: 400, headers: cors });
const sito = Deno.env.get("SITO_URL")!;
const comuni = {
client_reference_id: user.id,
customer_email: user.email,
success_url: sito + "/?pagamento=ok",
cancel_url: sito + "/prezzi",
};
const sessione = scelta.abbonamento
? await stripe.checkout.sessions.create({
...comuni,
mode: "subscription",
line_items: [{ quantity: 1, price: (await stripe.prices.list({ lookup_keys: [scelta.abbonamento] })).data[0].id }],
})
: await stripe.checkout.sessions.create({
...comuni,
mode: "payment",
line_items: [{ quantity: 1, price_data: { currency: "eur", unit_amount: scelta.centesimi!, product_data: { name: scelta.nome } } }],
metadata: { crediti: String(scelta.crediti ?? 0) },
});
return Response.json({ url: sessione.url }, { headers: cors });
});Il webhook: solo Stripe può dire "pagato"
Questo è il webhook nella versione per gli abbonamenti, che gestisce anche la disdetta. Per i crediti e per i pagamenti singoli cambia solo la parte dentro "checkout.session.completed": le due varianti sono nel riquadro successivo.
Webhook di Stripe
Il programmino che Stripe chiama quando un pagamento va a buon fine: controlla che la chiamata venga davvero da Stripe e aggiorna il database. Va pubblicato con l'opzione --no-verify-jwt.
// supabase/functions/stripe-webhook/index.ts
import Stripe from "npm:stripe";
import { createClient } from "npm:@supabase/supabase-js";
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY")!, {
httpClient: Stripe.createFetchHttpClient(),
});
const crittografia = Stripe.createSubtleCryptoProvider(); // necessario su Deno
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, // segreta: solo qui
);
Deno.serve(async (req) => {
const firma = req.headers.get("stripe-signature") ?? "";
const corpo = await req.text();
let evento: Stripe.Event;
try {
// Senza questo controllo chiunque potrebbe fingersi Stripe
evento = await stripe.webhooks.constructEventAsync(
corpo, firma, Deno.env.get("STRIPE_WEBHOOK_SECRET")!, undefined, crittografia,
);
} catch {
return new Response("Firma non valida", { status: 400 });
}
if (evento.type === "checkout.session.completed") {
const sessione = evento.data.object as Stripe.Checkout.Session;
const riferimento = sessione.client_reference_id;
if (!riferimento) return new Response("ok");
// Attiva il piano Pro e ricorda il cliente Stripe
await supabase.from("profili")
.update({ piano: "pro", stripe_customer_id: sessione.customer as string })
.eq("id", riferimento);
}
// Abbonamento disdetto o non rinnovato: si torna al piano gratuito
if (evento.type === "customer.subscription.deleted") {
const abbonamento = evento.data.object as Stripe.Subscription;
await supabase.from("profili")
.update({ piano: "gratis" })
.eq("stripe_customer_id", abbonamento.customer as string);
}
return new Response("ok");
});Le varianti per crediti e pagamenti singoli
Cosa fare quando il pagamento va a buon fine
Sostituisci la parte dentro "checkout.session.completed" del webhook con quella adatta al progetto. La tabella e il campo da aggiornare sono scritti nella frase "Pagamenti con Stripe" di ogni scheda.
// Variante CREDITI: aggiunge i crediti del pacchetto (il numero è nei metadata)
const quanti = Number(sessione.metadata?.crediti ?? 0);
if (quanti > 0) {
await supabase.rpc("aggiungi_crediti", { utente: riferimento, quanti });
}
// Variante PAGAMENTO SINGOLO: segna come pagata la riga indicata in client_reference_id
await supabase.from("ordini") // la tabella indicata nella scheda
.update({ pagato: true }) // il campo e il valore indicati nella scheda
.eq("id", riferimento);
// Variante "IN EVIDENZA PER 30 GIORNI"
await supabase.from("annunci")
.update({ in_evidenza_fino: new Date(Date.now() + 30 * 86_400_000).toISOString() })
.eq("id", riferimento);Il portale per gestire l'abbonamento
Link al portale clienti di Stripe
Il pulsante "Gestisci abbonamento" chiama questa funzione: Stripe mostra la sua pagina per cambiare carta, scaricare le fatture o disdire. Il portale va attivato una volta nella dashboard di Stripe.
// Dentro una Edge Function, dopo aver letto l'utente dal token
const { data: profilo } = await admin.from("profili").select("stripe_customer_id").eq("id", user.id).single();
if (!profilo?.stripe_customer_id) return new Response("Nessun abbonamento", { status: 404, headers: cors });
const portale = await stripe.billingPortal.sessions.create({
customer: profilo.stripe_customer_id,
return_url: Deno.env.get("SITO_URL")! + "/profilo",
});
return Response.json({ url: portale.url }, { headers: cors });Come provarlo senza soldi veri
- Usa le chiavi "di test" di Stripe (iniziano con sk_test_): nessun addebito reale.
- Alla pagina di pagamento usa la carta di prova 4242 4242 4242 4242, una data futura e un CVC qualsiasi.
- Nella dashboard di Stripe, sezione Webhook, aggiungi l'indirizzo della funzione stripe-webhook e scegli gli eventi checkout.session.completed e customer.subscription.deleted. Copia il "segreto di firma" in STRIPE_WEBHOOK_SECRET.
- Fai un pagamento di prova e controlla nel database che piano, crediti o ordine siano cambiati. Poi prova a cambiarli dal browser: deve essere impossibile.
- Solo quando tutto funziona passa alle chiavi live.
- Webhook
- Un indirizzo che un altro servizio (qui Stripe) chiama per avvisarti che è successo qualcosa.
- Stripe Checkout
- La pagina di pagamento già pronta di Stripe: tu non tocchi mai i dati della carta.
- Service role
- La chiave "padrona" di Supabase che ignora le regole di sicurezza: va solo nelle Edge Function, mai nel sito.
- Edge Function
- Un piccolo programma che gira sui server di Supabase, dove si possono usare chiavi segrete.