🚀 Join the waitlist now! waitlist.floot.dev
LogoFlootdocs
Payments

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.

supabase/.env.local
ENABLE_STRIPE=true

Turning 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.

VariableWhat it is
STRIPE_SECRET_KEYYour account's secret key
STRIPE_WEBHOOK_SECRETSigning secret for the webhook endpoint (see Webhooks)
APP_URL_SCHEMEYour 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.

supabase/.env.local
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.

supabase/.env.local
# 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:

GroupEvents
Checkoutcheckout.session.completed
Customerscustomer.created, customer.deleted
Invoicesinvoice.paid
Pricesprice.created, price.updated, price.deleted
Productsproduct.created, product.updated, product.deleted
Subscriptionscustomer.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.

Terminal
stripe login

You'll be prompted to open a browser window to complete the login process.

Redirect Stripe events to your Supabase local instance.

Terminal
stripe listen --forward-to localhost:54321/functions/v1/stripe-webhook

You 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.

supabase/.env.local
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.

Terminal
supabase functions serve --env-file .env.local --import-map ./functions/deno.json --no-verify-jwt

Billing 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.

Terminal
deno run --allow-net --allow-env --env-file=supabase/.env.local \
  supabase/scripts/create_restricted_portal_config.ts
supabase/.env.local
STRIPE_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.

Terminal
deno run --allow-net --allow-env --env-file=supabase/.env.local \
  supabase/scripts/create_confirm_portal_config.ts
supabase/.env.local
STRIPE_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:

PlatformPaywall provider
Web, Windows, LinuxStripe
iOS, AndroidRevenueCat, unless Requires ENABLE_STRIPE_MOBILE
macOSStripe, 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.

.env.local
ENABLE_STRIPE_MOBILE=true

Since 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.

On this page