Flootdocs

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:

Terminal
floot requirements check

It 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:

Terminal
floot requirements check || floot requirements install

floot 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".

FlagVariables it makes mandatory
Requires ENABLE_STRIPESTRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET
Requires ENABLE_REVENUECATREVENUECAT_SECRET_API_KEY, REVENUECAT_WEBHOOK_SECRET
Requires ENABLE_SEAT_BASED_BILLINGSTRIPE_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: 500

Auth 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:

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

A 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.localVault secret
ACCOUNT_DELETION_EMAIL_PEPPERaccount_deletion_email_pepper
DATABASE_WEBHOOK_SECRETdatabase_webhook_secret
MFA_RECOVERY_CODE_PEPPERmfa_recovery_code_pepper
SB_STORAGE_URLstorage_url
SB_URLsupabase_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.

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

Terminal
floot requirements check supabase

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

Terminal
supabase stop --no-backup && supabase start

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

.env.local
SUPABASE_URL=http://10.0.2.2:54321

Note

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.

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:

Terminal
curl -sI https://app.myapp.com/.well-known/apple-app-site-association
curl -sI https://app.myapp.com/.well-known/assetlinks.json

A 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:

Terminal
curl -s https://app-site-association.cdn-apple.com/a/v1/app.myapp.com

The 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.com under 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/*.entitlements files, and a build signed with your team.
  • Android: android:host on the autoVerify intent filter in android/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.

Terminal
keytool -list -v -keystore ~/upload-keystore.jks -alias upload | grep SHA256

Android's verification state. Ask the device what it decided:

Terminal
adb shell pm get-app-links com.acme.my_app

app.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:

Terminal
adb shell pm verify-app-links --re-verify com.acme.my_app

If 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:

Terminal
/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.

On this page