Troubleshooting
A guide to resolving common development problems.
Start here
Most "this used to work" problems are a missing tool or one that is too old, so before you read any further, ask the CLI:
floot requirements checkIt probes Git, the Dart SDK, FVM, Flutter, Docker, the Supabase CLI, Deno, jq, OpenSSL, the Stripe CLI and your platform toolchains, and reports which of them are absent or below the version this project needs. It exits non-zero when anything is wrong, which makes the repair a one-liner:
floot requirements check || floot requirements installfloot requirements install installs the missing ones through your host's
package manager, asking before it touches anything. See the
CLI reference for the full flag list.
Environment
Every Edge Function fails to boot
ENV VALIDATION FAILED: {
formErrors: [],
fieldErrors: {
SB_SECRET_KEY: [ "Invalid input: expected string, received undefined" ]
}
}The whole environment is validated the moment any Edge Function starts, and
throws if the shape is wrong. Because every function shares that validation,
one missing variable takes them all down at once, which is the point: a
half-configured backend should fail at functions serve rather than at the
first request that needed the variable.
A blank value counts as unset
A value of the empty string is treated as unset before validation runs, so
SMTP_USER= and a completely absent SMTP_USER are the same thing. That is
deliberate, it is what lets the conditionally-required variables sit in the
file as empty placeholders, but it does mean the cure is always to fill a
value in, never just to add the line.
Read the fieldErrors keys: each one names a variable in
supabase/.env.local. If they name every required key below at once, no
variable is wrong: the file was not loaded at all. See
Every auth email fails to send.
These are required in every project, whatever it was generated with:
DATABASE_WEBHOOK_SECRET, DENO_ENV, ENABLE_MULTI_TENANCY, SMTP_FROM,
SMTP_HOST, SMTP_PASSWORD, SMTP_PORT, SMTP_USER, SB_URL,
SB_PUBLISHABLE_KEY, SB_SECRET_KEY, SB_AUTH_EXTERNAL_REDIRECT_URI,
SB_SEND_EMAIL_HOOK_SECRET, SB_STORAGE_URL, WEBSITE_URL.
The SMTP pair is prefilled, not blank
floot create seeds SMTP_USER and SMTP_PASSWORD with a placeholder,
because the local mail catcher that supabase start runs ignores credentials
anyway. If you blank them while wiring up a real provider, the functions stop
booting until you fill both back in; the validator requires them
unconditionally, whatever the host.
The rest are optional until a feature flag turns them mandatory. The message
says so in as many words: STRIPE_SECRET_KEY is required when ENABLE_STRIPE is "true".
| Flag | Variables it makes mandatory |
|---|---|
| Requires ENABLE_STRIPE | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET |
| Requires ENABLE_REVENUECAT | REVENUECAT_SECRET_API_KEY, REVENUECAT_WEBHOOK_SECRET |
| Requires ENABLE_SEAT_BASED_BILLING | STRIPE_PORTAL_CONFIRM_CONFIGURATION_ID |
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.
Seat-based billing also validates its own prerequisites, and reports them against itself rather than against the variable that is missing:
ENABLE_MULTI_TENANCY is required when ENABLE_SEAT_BASED_BILLING is "true".
ENABLE_STRIPE is required when ENABLE_SEAT_BASED_BILLING is "true".
Both of those mean the same thing: you turned seats on without turning on what they are built from. Seat billing's moving parts are on Seat-Based Billing.
Every auth email fails to send
Unexpected status code returned from hook: 500Auth sends every email (sign-up confirmation, sign-in link, password reset,
email change) through the send-email Edge Function, and answers this when the
function fails. In the app, the login page's passwordless button shows "Sorry
an unexpected error occurred. Please try again."
On a local stack, the usual cause is that supabase functions serve is not
running. supabase start runs an edge runtime too, but it does not load
supabase/.env.local, so every function fails validation with every required
key missing (Every Edge Function fails to boot).
Serve the functions from supabase/ and leave the command running:
supabase functions serve --env-file .env.local --import-map ./functions/deno.json --no-verify-jwtA release build throws on startup
Exception: Error loading .env: <the underlying failure>
Release builds read `.env`, which is git-ignored and is not created by
`floot create`. Copy `.env.local` to `.env`, replace the local values with your
production ones, then add `- .env` under `flutter: assets:` in pubspec.yaml:
an asset that is not listed there is not bundled.The message names the remedy, but here is the reasoning behind it.
Debug and profile builds load .env.local; release builds load .env. Every
.env* file is git-ignored, and floot create writes only .env.local, so
until you create .env yourself, the first release build you run throws here,
before a single frame is drawn.
There is a second way to hit this with the file sitting right there on disk:
Flutter reads it as a bundled asset, so .env must also be listed under
flutter: assets: in pubspec.yaml. floot create lists .env.local and not
.env, for the same reason it writes one and not the other.
Fix
Both halves are covered step by step in Building for
release: copy .env.local to .env,
swap in your production values, and add .env to the asset list.
Database webhooks fail after you change a secret
401 INVALID_SIGNATURE. Failed to verify the request.
Five values in supabase/.env.local are also read from the Supabase Vault by
SQL, under lowercase names, because Postgres cannot see your environment file:
supabase/.env.local | Vault secret |
|---|---|
ACCOUNT_DELETION_EMAIL_PEPPER | account_deletion_email_pepper |
DATABASE_WEBHOOK_SECRET | database_webhook_secret |
MFA_RECOVERY_CODE_PEPPER | mfa_recovery_code_pepper |
SB_STORAGE_URL | storage_url |
SB_URL | supabase_url |
The [db.vault] block in supabase/config.toml seeds them locally, but only
when the database volume is created. A plain supabase stop && supabase start restores the saved volume and skips seeding entirely, so the Vault keeps
whatever it was seeded with the first time.
That is what the 401 above is: the database signs the webhook with the stale
database_webhook_secret while the Edge Function verifies it against the fresh
DATABASE_WEBHOOK_SECRET from supabase/.env.local. Every database-driven
function is affected at once.
Fix
Destroy the volume so the next start reseeds the Vault.
supabase stop --no-backup && supabase start--no-backup throws away your local database. Migrations and seeds rebuild
the schema, but any data you typed in by hand is gone.
On a deployed backend
Your deployed backend's Vault holds its own five rows, created by SQL under
those exact lowercase names: see Vault by
SQL on Supabase Cloud, or Vault by
psql self-hosted. Their values
belong to that host, never the local ones: supabase_url is its API URL,
such as https://<ref>.supabase.co, and storage_url the public API URL
followed by /storage/v1/object, because emails show the links built on it.
Every supabase db push passes --skip-vault, so a push never reseeds them from your local
file (see the --skip-vault
rule). The same
401 there means database_webhook_secret and the function secret
DATABASE_WEBHOOK_SECRET differ: update the row with vault.update_secret
and the function secret together.
Supabase
The initial migration refuses to run
ERROR: realtime.messages does not have RLS pre-enabled. This project requires the realtime image shipped with Supabase CLI >= 2.116.0 (older images leave RLS disabled, which would expose private broadcast topics). Upgrade the Supabase CLI and rerun.
This is a deliberate guard in supabase/migrations, not a bug. Since 2.116.0
the realtime image owns realtime.messages and ships it with row-level security
already enabled; postgres may add policies to it but can no longer ALTER it.
On an older image RLS stays off, the subscription-topic policies protect
nothing, and any signed-in user can subscribe to any topic, so the migration
stops rather than build that.
Confirm the version you are on.
floot requirements check supabaseUpgrade the Supabase CLI to 2.116.0 or later.
Start from a clean volume, so the new realtime image is pulled and the migration re-runs from the beginning.
supabase stop --no-backup && supabase startOn a deployed backend
The guard checks the database you push to, not your CLI, so upgrading the CLI
changes nothing there. A new Supabase Cloud project passes it. On a
self-hosted stack the table comes from the Realtime container: push only once
it is healthy and realtime.messages exists, as The first
start shows, and upgrade an older
stack's Realtime image first (see
Upgrades). A table that does not exist yet
slips past the guard and fails later, at CREATE POLICY, with relation "realtime.messages" does not exist.
Android
Emulator is not reaching the backend
Connection refused - errno 101
SocketException: Connection failed (OS Error: Network is unreachable, errno = 101)…
Fix
Turn the Wi-Fi off on emulator.
Connection refused - errno 111
SocketException: Connection refused (OS Error: Connection refused, errno = 111)…
This occurs because the Android emulator does not recognize localhost as the
host machine. Instead, you should use the IP address 10.0.2.2. The port stays
54321, that is the local Supabase API, which is what the app talks to.
SUPABASE_URL=http://10.0.2.2:54321Note
This is the app's .env.local at the project root, not
supabase/.env.local. Change .env the same way if you are pointing a
release build at a local backend. On a physical device neither 127.0.0.1 nor
10.0.2.2 works; use your machine's LAN address, from ipconfig getifaddr en0 on macOS or ipconfig on Windows.
If you would rather keep 127.0.0.1 and avoid a separate Android-only
SUPABASE_URL, forward the port instead; see
Useful Commands.
Deep links
The link opens the browser, not the app
A Stripe return or an invitation lands on the web app even though the app is installed. That is the designed fallback, so nothing errors: the platform did not verify your domain for the app, or does not hand links over at all. First rule out the platforms that never open the app from a link: Windows, Linux, a Mac that is not opted in, and any Mac whose default browser is Chrome, Arc or Edge (see When the link misses the app).
Then check the chain in order; each step depends on the one before. The
examples use app.myapp.com, the bundle id com.acme.myApp and the package
com.acme.my_app.
The served file. Each file answers 200, as application/json, with no
redirect on the way:
curl -sI https://app.myapp.com/.well-known/apple-app-site-association
curl -sI https://app.myapp.com/.well-known/assetlinks.jsonA 301 or 302, a text/html body (the SPA fallback answering for a file
that is not there) or a text/plain type all fail verification: see
The association files, as JSON.
iOS and macOS read the Apple file from Apple's CDN, not from your server, so
check that copy too:
curl -s https://app-site-association.cdn-apple.com/a/v1/app.myapp.comThe CDN can trail your server after a fix; reinstall the app once it serves the new file.
The values in the files. The Apple file's appIDs entry is your Team ID,
a dot, and your bundle id, ABCDE12345.com.acme.myApp, not the
APPLE_TEAM_ID placeholder. assetlinks.json names your package and the
fingerprint of the certificate that signed the build you installed, not the
ANDROID_SIGNING_CERT_SHA256 placeholder. The claimed paths are the three in
The claimed paths.
The domain in the app. The build names the same host the files are served from:
- iOS:
applinks:app.myapp.comunder Associated Domains in Xcode (ios/Runner/Runner.entitlements), and the capability on in the provisioning profile the build was signed with. - macOS: the same entry in both
macos/Runner/*.entitlementsfiles, and a build signed with your team. - Android:
android:hoston theautoVerifyintent filter inandroid/app/src/main/AndroidManifest.xml.
No scheme, path or www the server does not answer on.
The fingerprint (Android). Compare the fingerprint in assetlinks.json
with the one that signed the installed app. A build from the store is signed
with Play's app signing key, a release build you installed yourself with your
upload key, and a debug build with your machine's debug key; each needs its
own entry.
keytool -list -v -keystore ~/upload-keystore.jks -alias upload | grep SHA256Android's verification state. Ask the device what it decided:
adb shell pm get-app-links com.acme.my_appapp.myapp.com: verified means the link opens the app. Anything else means
Android's last check failed. After fixing the cause, ask it to check again,
then read the state once more:
adb shell pm verify-app-links --re-verify com.acme.my_appIf the domain reads verified but the link still opens the browser, the user
may have turned off Open supported links in the app's settings, or link
handling may be off for the app:
adb shell pm set-app-links-allowed --user 0 --package com.acme.my_app true
turns it back on.
On a developer Mac
Older builds of the same bundle id, a Debug build in another folder or one in the Trash, stay registered with macOS and can take the link instead of the build you are testing. Remove them, or unregister each one before you test:
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -u <path to the old .app>Flutter
Widget Testing
Pending timers
When running widget tests, you might encounter the following error:
The following assertion was thrown running a test: A Timer is still pending even after the widget tree was disposed.
…'!timersPending’
This typically occurs when an animation or asynchronous operation hasn't completed. Below are two approaches to resolve this using tester.runAsync.
Run the pump and settle in that function.
await tester.runAsync(() async => tester.pumpAndSettle());Apple build failures (Swift Package Manager)
Floot builds all iOS/macOS plugins with Swift Package Manager; there is no
CocoaPods integration, and your project has no Podfile. If you see an error
mentioning pod install or CocoaPods's specs repository, it is coming from a
stale build directory rather than from Floot.
Note
If you are encountering this issue for your Mac app, replace every occurrence
of the ios path with macos in the steps below.
Fix
Run flutter clean to remove build artifacts.
Delete ios/Flutter/ephemeral so the generated Swift package is
rebuilt from scratch.
Run flutter pub get to fetch Dart dependencies.
Run flutter run to build and launch the app.