--- name: flet-supabase-framework description: Framework for a Flet + Supabase multi-platform Python app — correct project structure, dependency config, integration patterns, and hard-won lessons from a full build cycle author: POWR-DATA version: 2.4.0 license: MIT
Flet + Supabase App Framework
Purpose
Scaffold a new Python cross-platform app using Flet (Python/Flutter UI framework) and Supabase (auth + database + edge functions backend) with the correct project structure, pyproject.toml configuration, dependency pinning, and integration patterns from the start — avoiding the class of build failures and runtime crashes that only emerge on device if set up incorrectly.
When to use
When a developer wants to build a Python app that targets Android, iOS, and web from a single codebase, backed by Supabase for auth and data. Apply at project creation time, before any platform builds are attempted. The output of this skill feeds directly into flet-multiplatform-build.
Inputs expected
- App name and intended bundle ID (or placeholder if not yet registered with Google Play / App Store)
- Target platforms: Android, iOS, Web, or a subset
- Supabase project URL and anon key (from Supabase dashboard → Project Settings → API)
- Rough list of Supabase tables or Edge Functions planned
- List of app screens/views planned (even just names)
Guiding principles
- pyproject.toml ≠ requirements.txt for Android builds. Flet's Android packager (serious_python) reads
[project] dependenciesinpyproject.toml— your direct app dependencies only — and resolves transitive deps for Android arm64-v8a from Flet's custom wheel index (pypi.flet.dev).requirements.txtserves the host dev environment. Never put transitive deps in pyproject.toml. - Exclude your dev environment from the Android bundle. Always add
exclude = [".venv", "build", ".git", ".github", "__pycache__", "*.pyc"]under[tool.flet.app]. Without this, serious_python may bundle Windows packages from the local venv into the APK instead of cross-compiled arm64-v8a packages. - Use
page.run_thread()not raw threads.page.run_thread(fn)routes the thread through Flet's event loop, ensuringpage.update()calls from background threads reach Flutter. With rawthreading.Thread, calls topage.update()can silently drop on some targets — the update appears to succeed but the UI does not repaint. - Supabase client is a singleton. Create the client once and return the cached instance from a module-level variable. Re-creating it on every request drops the auth session.
- Auth state lives in the Supabase client, not in page state. After sign-in, the session is held by the client object. Navigate by route change; never store user info on
page. - Never block the main thread. All Supabase API calls go in background threads via
page.run_thread(). Callpage.update()at the end of every background function to flush UI changes. did_mountis the entry point for data loading. Callpage.run_thread()fromdid_mount(), not__init__(). The view must be mounted before anypage.update()call is valid.- Pin cryptography and cffi to Android-compatible versions. Only specific versions of these packages have pre-built Android arm64-v8a wheels on
pypi.flet.dev. Usecryptography==43.0.1andcffi==1.17.1. Do not upgrade without first verifying wheel availability on the Flet custom index. - Do not put LLM API keys in the client app. Route LLM calls (Gemini, OpenAI, Anthropic, etc.) through Supabase Edge Functions. The app only holds the Supabase anon key, which is safe to expose — it is protected by Row Level Security, not by secrecy.
.envdoes not exist at runtime on mobile.load_dotenv()reads from disk — on Android and iOS there is no.envfile in the app bundle. Always embed Supabase URL and anon key as code-level defaults so the app works on device, while still allowing.envto override for local dev. Use a lazyget_client()singleton — not a module-levelcreate_client()call — to avoid import-time network activity that can crash on mobile before the runtime is fully ready.- Use a single transparent PNG for all icon placements. A transparent PNG (RGBA mode, alpha=0 in background areas) blends against any background automatically. Creating separate icon variants per background colour requires the background RGB to match exactly — even a 1-point difference shows as a rectangular border.
- Script all infrastructure — never click through the portal. Keep an
infra/setup-azure.sh(or equivalent) in the repo that provisions everything from scratch. Apply the same discipline to Supabase: table creation and RLS policies belong in SQL migration files, not just dashboard clicks. - Never reuse a single
ft.AppBarinstance — use a factory method. Flet cannot reattach the sameft.AppBarobject to a View after it has been detached. Reusingself._appbarcauses'AppBar' object has no attribute 'appbar'on the second toggle. Fix: use a factory method (_make_appbar()) that returns a freshft.AppBarinstance each time it is needed. - Pass a
set_appbar(appbar)callback for dynamic AppBar changes within a view. To show or hide an AppBar dynamically within a screen (e.g. toggling between sign-in and sign-up modes), pass aset_appbarcallback from thenavigateclosure inmain.pyinto the view constructor. Never accesspage.views[0].appbardirectly — indexing intopage.viewsis unreliable on Flet desktop. - Always assign
self._navigate = navigatein__init__if any method in that view calls navigate. Omitting this assignment causesAttributeError: '<ViewName>' object has no attribute '_navigate'at runtime when the handler fires — the error does not surface at construction time.
Flet 0.84 version notes
ft.ImageFitdoes not exist in Flet 0.84.0. Thefitparameter onft.Imagemust be omitted — passing it causesAttributeError: module 'flet' has no attribute 'ImageFit'.ft.ElevatedButtonis deprecated from 0.80.0 — useft.Buttonin new code.ft.padding.symmetric()is deprecated from 0.80.0 — useft.Padding.symmetric().page.launch_url()is async in 0.84.0 — handlers that call it must beasync def.ft.app()is deprecated from 0.80.0 — useft.run().
Process
Generate every project file from the templates in reference.md — it holds the load-bearing code for each step below.
- Create the directory structure — see Project structure in
reference.md. - Write
pyproject.toml— direct dependencies only (no transitive deps); add[tool.flet.app] excludeto keep the dev venv out of the Android bundle. See pyproject.toml. - Write
requirements.txt— the full transitive tree frompip freeze, includingflet-weband the pinnedcryptography/cffiversions. See requirements.txt. - Write
services/supabase_client.py— a lazyget_client()singleton with embedded URL/key defaults so the app works on device without.env. See Supabase client. - Write
services/auth.py— thinsign_in/sign_up/sign_out/get_userwrappers overget_client(); store no state here. See Auth service. - Write
main.py— one persistentft.View, controls swapped via a synchronousnavigate(route)callback (notpage.controls-only, not view-per-route). See main.py and navigation. - Write each screen as an
ft.Columnsubclass — exposeself.appbar; load data indid_mount()viapage.run_thread(), ending every background function withpage.update(). See Screen view. - Write
.env.example(committed) and.env(gitignored) — see Environment files. - Add
.gitignoreentries —.env,.venv/,build/,__pycache__/,*.pyc,*.apk,*.aab,*.ipa. - Create the Supabase
profilestable with RLS policies — see Supabase profiles table. - Verify — run
flet run main.pylocally; sign in, navigate, confirm data loads before attempting any mobile build.
Output format
The skill produces a complete, runnable project. Present the result as:
- Project structure — the directory tree created
- Generated files — each file from the Process in order (
pyproject.toml,requirements.txt,services/,main.py, views,.env.example,.gitignore), drawn from the templates inreference.md - Supabase setup — the
profilestable SQL and RLS policies - Verification result — confirmation that
flet run main.pyruns locally, sign-in works, and navigation and data loading succeed
Testing workflow
Use the fastest tier that answers your question — never push to trigger a CI build just to test a local change:
| Tier | Command | Time | Use for |
|---|---|---|---|
| Desktop | flet run main.py |
Instant | All logic, Supabase, navigation, UI — 90% of dev |
| Web local | flet run --web --port 8550 main.py → http://localhost:8550 |
Seconds | Web layout, before every push |
| Android (CI) | push → bash install-apk.sh |
~12 min | Mobile-specific, official artifact |
| Android (local) | flet build apk + adb install |
~5 min | Frequent mobile testing (requires local Flutter) |
| iOS | macOS only | N/A on Windows | Defer to App Store prep |
Quality checklist
-
pyproject.tomlhas 5-6 direct dependencies only, no transitive deps -
[tool.flet.app] excludeincludes.venvandbuild -
requirements.txthas the full transitive dep tree with annotated groups -
flet-web==<version>inrequirements.txt(same version asflet) -
cryptography==43.0.1andcffi==1.17.1(or explicitly verified newer versions) -
supabase_client.pyhas embedded defaults for URL and anon key — not relying solely on.env - Supabase client is a module-level singleton — one instance, cached in
_client - All Supabase calls are in background functions passed to
page.run_thread()— not rawthreading.Thread -
page.update()is called at the end of every background thread function - Data loading happens in
did_mount(), not__init__() - Navigation uses a single persistent
ft.Viewwith controls swapped vianavigate(route)callback — not view-per-route orpage.controls-only - Screen views are
ft.Columnsubclasses withself.appbarexposed — notft.Viewsubclasses -
navigate(route)is a synchronous callback — notawait page.go(route)from synchronous handlers -
.envis in.gitignore;.env.exampleis committed with placeholder values - No LLM API keys in the client app — routed via Edge Functions
- App icon is a transparent PNG — 1024×1024px, no coloured background
- Splash screen is 2048×2048px — separate file from icon
- Infrastructure setup is scripted in
infra/— not portal-click-only - Supabase schema is in SQL migration files, not just dashboard clicks
- App runs locally with
flet run main.pybefore any mobile build is attempted -
self._navigate = navigateis assigned in__init__for every view that calls navigate in any of its methods - Views that need to change the AppBar dynamically receive a
set_appbarcallback — not direct access topage.views
Avoid
- Putting transitive dependencies in
pyproject.toml— direct deps only; transitive deps here break Android arm64-v8a pip resolution - Upgrading
cryptographyorcffiwithout first verifying the target version has an arm64-v8a wheel onpypi.flet.dev - Using
threading.Thread(target=fn).start()directly —page.update()from a raw thread can silently drop on some targets; usepage.run_thread(fn) - Calling
page.update()from__init__— the view is not yet mounted; defer todid_mount - Storing user session data on the
pageobject — the Supabase auth client holds the session - Putting LLM API keys in the mobile app — always route AI calls server-side through Supabase Edge Functions
- Committing
.env— always gitignore it and ship.env.exampleinstead - Relying on
.envfor runtime config on mobile — the file is not bundled; embed defaults in code - Using a module-level
create_client()call — triggers import-time network activity that can crash on mobile before the runtime is fully ready; use a lazyget_client()singleton - Using view-per-route navigation (multiple entries in
page.views) — causes issues on Android; use a single persistentft.Viewwith controls swapped vianavigate(route)callback - Using only
page.controls(nopage.views) — renders nothing on Android; Flutter's Navigator requires at least oneft.Viewinpage.views - Using
await page.go(route)in synchronous event handlers —page.go()is async in Flet 0.84 but handlers are sync; pass a synchronousnavigate(route)callback to each view instead - Writing screen views as
ft.Viewsubclasses — useft.Columnsubclasses with anappbarinstance attribute sonavigate()in main can assign it to the persistent view's appbar - Creating separate icon files per background colour — use transparent PNG instead
- Clicking through Azure portal or Supabase dashboard for setup steps — script everything in
infra/ - Using
fit=ft.ImageFit.CONTAINin Flet 0.84.0 — the attribute doesn't exist in this version - Using
ft.app()— deprecated since Flet 0.80; useft.run() - Pushing to
mainjust to test a change — use desktop or web local tier first - Reusing the same
ft.AppBarinstance across multiple show/hide cycles — use a factory method that returns a fresh instance each time - Accessing
page.views[0].appbardirectly to change the AppBar dynamically — indexing intopage.viewsis unreliable on Flet desktop; pass aset_appbarcallback from the navigate closure instead - Omitting
self._navigate = navigatein a view's__init__when any method in that class calls navigate — theAttributeErroronly surfaces at runtime when the handler fires, not at construction time
Example usage
"Scaffold a Flet + Supabase app called GardenTrack targeting Android and web. Users log in then see a list of their plants. Supabase tables: plants (id, user_id, name, species, last_watered date, watering_interval_days int). Show me the full project structure and all files."
Source: This skill is sourced from the Matrix Skills library. Learn more at the AI Agent Skills Library.