Going to production
What goes live when you ship a Floot app, the order it ships in, the rules that hold on every deploy, and a launch checklist.
Everything you have run so far lived on your machine: a local Supabase stack,
Mailpit catching the mail, and an app reading .env.local. Going live replaces
each of those with a hosted counterpart, and this section walks through every
one. Start here for the shape of it, then follow the page for your backend.
What goes live
A Floot app ships as three parts, and each has its own production copy:
| Part | Locally | In production |
|---|---|---|
| Database | supabase start, rebuilt by db reset | A hosted Postgres that only ever moves forward, one migration at a time |
| Edge Functions | supabase functions serve, supabase/.env.local | Deployed functions reading their own secrets, set on the project |
| Clients | flutter run, reading .env.local | A web build on a static host and store builds, each reading a bundled .env |
Around those sit the settings that never live in a migration: the Vault rows your SQL reads, the Auth configuration (Site URL, redirect URLs, providers, the send-email hook), and the outside services the kit talks to, your SMTP provider and, if you use them, Stripe, RevenueCat and the OAuth providers.
Supabase Cloud is the default home for the backend, and Supabase Cloud takes a project from nothing to a working deploy. To run the backend on your own server instead, Self-hosted starts from Supabase's official Docker setup. Whichever you choose, Web builds and hosts the web app, and Mobile gets the iOS and Android apps ready for the stores.
The deploy order
Every release ships in the same order, backend first:
The order follows from who reads what. New functions may call a column the migration adds, so the schema lands first. The clients go last because they call both, and because a store build takes days to reach every phone: for that whole time, old app versions keep calling your newest backend.
The rules that hold on every deploy
Four rules apply whichever backend you choose. Each one exists because breaking it fails silently, often long after the deploy looked fine.
--skip-vault on every db push
supabase/config.toml carries a [db.vault] block so that
supabase start can seed five secrets into your local Vault. The same block
runs on db push: any push that applies at least one migration first writes
every Vault value it can resolve, from your shell, supabase/.env.local or
supabase/.env, into the remote Vault. That write is committed before the
migrations run, so even a push whose migration then fails leaves production
holding your laptop's values.
Pointing the CLI at a production env file does not prevent it:
supabase/.env.local still loads and wins over .env.production. So the rule
is absolute: pass --skip-vault on every push, and set the Vault by SQL, as
Vault by SQL shows.
Expand and contract
Every migration must work with the functions and the app of the release before it, because the previous functions keep running against the new schema for the seconds a deploy takes, and the previous app for as long as people keep it installed.
So a breaking change ships in two releases. The first expands: it adds the new column, table or function alongside the old one, and the code moves to the new shape. A later release contracts: once nothing reads the old shape, it drops it. A rename, for example, is an add and a backfill in one release and a drop in another.
Migrations only move forward. Once one has run in production, you never edit it; a fix is a new migration.
Never copy local env values
.env.local and supabase/.env.local hold values made for your local stack:
generated peppers and webhook secrets, the CLI's fixed local keys, URLs such as
http://kong:8000 that mean nothing outside Docker. Production gets its own
fresh set, generated for it and stored in a password manager or secret store,
never in the repository.
The danger is less a wrong value than a shared one. A local secret has been in shell history, in screenshots and on every machine you cloned to; reusing it in production publishes it there.
The app's .env is public
A release build reads .env, bundled into the app as an asset (see
Building for release). Anything bundled
into a client is readable by anyone who has the client: a store binary unzips,
and a web build serves the file to any browser at /assets/.env.
That holds today because the file carries only client-safe values: your
Supabase URL and publishable key, the ENABLE_* flags and OAuth client ids.
Keep it that way. The secret key, webhook secrets, SMTP credentials and
peppers belong in your function secrets and Vault, never in any file the app
bundles.
Both env files ship
pubspec.yaml still lists .env.local after you add .env, so a release
build bundles both, and a web build serves both. Never put anything in either
file that you would not publish.
Launch checklist
Work through it once before the first real user signs up. Each item links to the place that explains it.
Backend
- The project is created and linked, and the database password is kept outside the repository: Create and link.
- Function secrets are set from a production env file, and every function is deployed: Function secrets and deploy.
- Migrations are applied with
--skip-vault, after a dry run: Migrations. - The five Vault rows exist, with values made for this project: Vault by SQL.
- Reference data and the
assetsbucket objects are in place: Reference data and storage. - Database Webhooks are enabled: Database Webhooks.
-
appandpgmq_publicare exposed: Exposed schemas. - Auth points at your real URLs, the Send Email hook is on, and the security notifications are switched on: Auth settings.
- Self-hosting instead? The same items, the self-hosted way: only the API
paths are public (Exposure), every
config.tomlsetting is mirrored in the environment (Fromconfig.tomlto environment variables), private Storage is never cached (Storage behind a CDN), and backups run and restore (Backups).
Feature secrets
- Mail goes through a real provider, from a sender on its verified domain: Moving to a real provider and Mail.
- If you use Stripe: a live webhook endpoint points at your deployed
stripe-webhook, and its signing secret is a function secret: Stripe webhooks. - If you use RevenueCat: the webhook points at your deployed
revenuecat-webhook, with its authorization secret set on both sides: RevenueCat webhooks. - If you offer Sign in with Apple: the client secret is generated, and a reminder is set to regenerate it before it expires, every 6 months: Generate Secret Key.
Content
- The email footer carries your address, not the placeholder: The footer translates itself.
Clients
- Each release build bundles a
.envholding your production URL and publishable key, and nothing secret: Building for release. - The web build serves CanvasKit from your origin, and its host
revalidates every file, falls back to
index.htmlfor app routes only and sends a report-only CSP: Web. - The Site URL is your web origin, and your web app's paths
(
https://app.myapp.com/**) and your app callback are on the Auth redirect list: Auth redirect URLs. - The web app serves both association files as
application/json, filled with your Apple team ID and Android signing fingerprints, and iOS has Associated Domains on your web domain: Deep-link domains. - Your bundle id and package name are your own, not
com.example, and the iOS team is the one that publishes: Your identifiers are permanent and iOS: team and bundle id. - Android release builds are signed with your own key, and Google Sign-In knows Play's app signing key: Android: replace the debug signing.
- The smoke check passes against the live project: Smoke check.