Stripe
Set up Stripe to enable secure payment processing in your Floot app.
Requirements
To set up Stripe for your Floot app, you will need:
- A Stripe account.
- The Stripe CLI installed on your machine (required only if you plan to test payments locally.)
Setup
Turn Stripe on
Stripe ships disabled. Nothing in the backend talks to Stripe until you say so, so a project that monetizes purely through RevenueCat, or not at all, can leave this alone.
Requires ENABLE_STRIPE
Stripe is off in a new project. Set `ENABLE_STRIPE=true` in `supabase/.env.local` to use this section.
ENABLE_STRIPE=trueTurning it on makes three variables mandatory. The Edge Functions validate their environment at startup and refuse to boot if any of them is missing, which is deliberate: a half-configured payment integration should fail loudly at deploy time rather than quietly at checkout.
| Variable | What it is |
|---|---|
STRIPE_SECRET_KEY | Your account's secret key |
STRIPE_WEBHOOK_SECRET | Signing secret for the webhook endpoint (see Webhooks) |
APP_URL_SCHEME | Your app's deep-link scheme, used to return the user after checkout |
Secret key
Find it in your Stripe dashboard and copy the Secret Key.
STRIPE_SECRET_KEY="sk_test_..."Nothing reads it from the database
The Edge Functions read the key from the environment, so
supabase/.env.local is the only place it belongs. It is never read from
Supabase Vault.
App URL scheme
The app URL scheme is how a user gets back into your app after paying in an external browser. It must match the deep linking scheme configured in your Flutter application.
# Your app URL scheme (e.g. "com.acme.myapp")
APP_URL_SCHEME="com.acme.myapp"Webhooks
Stripe is the source of truth for products, prices, customers and subscriptions; your database is a replica of it. Webhook events are what keeps the replica current, so the integration does not work at all without them: products never appear in the paywall, and a completed checkout never grants access.
Thirteen event types are handled:
| Group | Events |
|---|---|
| Checkout | checkout.session.completed |
| Customers | customer.created, customer.deleted |
| Invoices | invoice.paid |
| Prices | price.created, price.updated, price.deleted |
| Products | product.created, product.updated, product.deleted |
| Subscriptions | customer.subscription.created, customer.subscription.updated, customer.subscription.deleted |
Anything else is ignored, not rejected, so a wider endpoint selection is harmless, just noisier.
Events Redirection
Stripe webhooks events should be redirected to your Supabase instance, and this is how to do it.
Here are the steps to receive Stripe events locally using the Stripe CLI (required).
Login to your Stripe account using the CLI.
stripe loginYou'll be prompted to open a browser window to complete the login process.
Redirect Stripe events to your Supabase local instance.
stripe listen --forward-to localhost:54321/functions/v1/stripe-webhookYou should see a message confirming that the webhook is set up and listening for events.
Ready! Your webhook signing secret is whsec_...You can now copy the whsec_... value and add it to your Supabase local environment file.
STRIPE_WEBHOOK_SECRET="whsec_..."stripe listen mints a new signing secret every time you run it.
If webhooks start returning signature errors after a restart, this is
why: copy the new value across and restart supabase functions serve.
Serve the Supabase functions locally to handle Stripe webhooks.
supabase functions serve --env-file .env.local --import-map ./functions/deno.json --no-verify-jwtBilling portal configurations
Stripe's hosted billing portal is driven by a Billing Portal Configuration. Left to its own devices your app uses the account's default one, and that default decides things your app should be deciding, most importantly whether a customer can edit their subscription quantity straight from Stripe's UI.
Two provisioning scripts ship with your project. Both are re-runnable and both clone your account's current default configuration, changing exactly one field.
Run them once per Stripe account and per mode (test and live keys have
separate configurations), then paste the printed ids into
supabase/.env.local.
The restricted configuration, which removes quantity from the updates a
customer may make.
deno run --allow-net --allow-env --env-file=supabase/.env.local \
supabase/scripts/create_restricted_portal_config.tsSTRIPE_PORTAL_CONFIGURATION_ID="bpc_..."Seat-based billing only
This section applies only to projects generated with `--seat-based-billing`.
Requires ENABLE_SEAT_BASED_BILLING
Seat-based billing is off in a new project. Set `ENABLE_SEAT_BASED_BILLING=true` in both `.env.local` and `supabase/.env.local` to use this section.
The confirm configuration, which adds quantity back, for one flow only.
deno run --allow-net --allow-env --env-file=supabase/.env.local \
supabase/scripts/create_confirm_portal_config.tsSTRIPE_PORTAL_CONFIRM_CONFIGURATION_ID="bpc_..."What this pair of configurations protects is explained on Seat-Based Billing.
Why two configurations rather than one
The app's own plan-switch flow sends a subscription_update_confirm session
whose quantity is fixed server-side, not chosen by the customer. Stripe
validates default_allowed_updates against that flow all the same, so the
restricted configuration makes it fail with a 400. create-stripe-portal
therefore uses the restricted configuration for customer-facing flows and the
confirm configuration only for plan switches. One configuration cannot do
both.
Products & prices
Important
Do not create customers from your Stripe dashboard, let the app take care of it, because it attaches useful metadata to them in the creation process.
Once you've completed the steps above, connect to your Stripe account and create your products and prices. The product.* and price.* webhook events sync them into your Supabase database, where the paywall reads them.
Here is a guide on how to manage products and prices in Stripe.
Handling business logic
When integrating Stripe with your Floot app, you may want to customize how your backend responds to payment events. Floot provides sensible defaults for handling subscriptions and one-time payments, but you can extend or override this logic to fit your business needs. The following sections explain where and how to implement custom business logic for different payment scenarios.
Subscriptions
Subscription access is managed automatically based on the duration you configure when creating your offering. If you need to implement additional logic, such as awarding credits, coins, or other custom actions for subscribers, handle it in the following file: supabase/functions/stripe-webhook/_events/subscription_events.ts.
One time payments
To handle one-time payments, edit the file supabase/functions/stripe-webhook/_events/checkout_events.ts and follow the instructions marked with comments starting with // ! ....
Customer cleanup
When a user deletes their account, the matching Stripe customer has to be retired too, otherwise a cancelled account leaves a card on file and a billable object at Stripe with nothing in your database pointing at it.
This happens asynchronously: deleting a public.customers row queues a
cleanup that is drained through the Stripe REST API. A 404 counts as
success, since the customer being absent is the desired state. If that first
attempt is lost, the stripe-cleanup-drain-sweep cron job collects the queue
at 43 minutes past every hour, so nothing is dropped, it is only ever late.
The sweep runs locally too
On a developer machine it will happily retire Stripe test-mode customers for rows you deleted while testing. To stop that in a local database:
SELECT cron.unschedule('stripe-cleanup-drain-sweep');This is billing hygiene, not erasure. Stripe treats DELETE /v1/customers as
object lifecycle management: it leaves a tombstone still retrievable by GET,
carrying name, email and metadata. Erasing that data needs Stripe's
Redaction API, which is
asynchronous and holds records for a 90-day floor.
Free trials
Note
Free trials can only be offered for subscriptions; they are not supported for one-time payments.
With Stripe, a trial is a property of a price, set through its metadata.
Important
Ensure that webhook events are correctly forwarded to your Supabase instance. Learn more in the Events Redirection section.
This option is recommended because it allows you to keep track of the free trials you are currently offering via your Stripe Dashboard.
Stripe Dashboard → Products section and create a new product or select an existing one.
Under the Pricing section, create a new price or edit an existing one.
In the Metadata section, add a key named trial_days and set its value to the number of free trial days you want to offer.
For example, to provide a 7-day free trial, enter 7 as the value. This will trigger a price.updated webhook event,
ensuring the trial period is synced with your Supabase database.
Everything after that (eligibility, the paywall badge, the trial-ending notification schedule, and how to let a user trial more than once) is provider-agnostic and lives on the Free Trials page.
Stripe on Mobile
By default the paywall uses Stripe on web, Windows and Linux, and hands off to RevenueCat wherever the platform has a real in-app-purchase store. Two flags decide that split:
| Platform | Paywall provider |
|---|---|
| Web, Windows, Linux | Stripe |
| iOS, Android | RevenueCat, unless Requires ENABLE_STRIPE_MOBILE |
| macOS | Stripe, unless Requires ENABLE_IAP_MACOS |
The predicate is supportsIAP in
lib/core/presentation/utils/extensions/build_context_x.dart. Note that
ENABLE_STRIPE_MOBILE does not affect macOS, and ENABLE_IAP_MACOS does
not affect phones; they gate different platforms.
This section covers the first exception: putting Stripe back on iOS and Android.
Requires ENABLE_STRIPE_MOBILE
Stripe on mobile is off in a new project. Set `ENABLE_STRIPE_MOBILE=true` in `.env.local` to use this section.
To activate Stripe on mobile (iOS and Android), enable it in your Flutter environment file. Note that this one lives in the app's .env.local, not Supabase's.
ENABLE_STRIPE_MOBILE=trueSince payments on mobile are processed through an external webview, ensure your app's URL scheme is properly configured to redirect users back to the app after payment. Refer to the app URL scheme setup in this section.