Vibecode FlutterFlow
track this build5 phases, 15 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For FlutterFlow, assemble a mobile interface from a schema and export maintainable Flutter code. The hard boundary is visual editor depth, native builds, integrations, collaboration, and code generation, plus connectors, runtime reliability, and governance.
You are building a lean indie version of FlutterFlow. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # FlutterFlow · indie build A Flutter application built from a schema you define once: generated data models, a real authentication flow, list and detail and edit screens per entity, an offline cache that survives a train tunnel, and signed builds distributed to testers on both platforms. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Framework | Flutter with Dart, on the current stable channel | one codebase for both platforms, and the code stays readable, which is the point of leaving a visual builder | | Backend | Supabase, self-hosted or managed | PostgreSQL with authentication and a generated API, which is the closest thing to what the visual builder was providing | | Generation | Dart code generation from a schema file, run as a build step | the repetitive model and form code is generated, and the parts worth writing by hand stay handwritten | | Local cache | SQLite through Drift | typed queries and migrations, and a cache that is queryable rather than a blob of JSON | | State | Riverpod | explicit, testable, and it does not require the widget tree to be the source of truth | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **The Flutter SDK on the stable channel** · free - Why: The framework. The doctor also tells you which platform toolchains are missing before you waste time. - Get it: Install from flutter.dev following the guide for your operating system, then run the doctor and resolve every warning for your targets. - Verify: flutter doctor reports no issues for your intended platforms - [ ] **A physical Android phone** · free if you have one - Why: Emulators hide performance, permissions and offline behaviour, which are three of the five things this build is about. - Get it: Any recent Android device with developer options and USB debugging enabled. - Verify: flutter devices lists it and flutter run installs to it - [ ] **A Mac with Xcode, only if you ship to iOS** (optional) · free if you own a Mac; otherwise this is the largest cost in the project - Why: There is no supported way to build or sign an iOS application without macOS. This decides your platform scope. - Get it: Any Mac able to run a current Xcode. If you do not have one, decide in Phase 1 that this is Android only. - Verify: A Flutter application runs on a physical iPhone - [ ] **Apple Developer Program membership, only if you ship to iOS** (optional) · 99 USD per year - Why: Required to install on devices you do not own and to use the testing distribution channel. - Get it: Enrol at developer.apple.com/programs. Review can take several days, so start early if iOS is in scope. - Verify: A provisioning profile installs a build on a tester's device - [ ] **Google Play developer account** · 25 USD one time, plus identity verification - Why: Needed for the Android testing channel and any public listing. - Get it: Register at the Play Console. There is a one-time fee and an identity verification step that takes time. - Verify: An internal testing release reaches a tester - [ ] **A Supabase project, managed or self-hosted** · free tier available for managed; self-hosting costs a server - Why: Provides the database, authentication and API. Without it Phase 3 has nothing to talk to. - Get it: Create a project at supabase.com, or run the Docker stack yourself. Take the project URL and the anon key. - Verify: A query from the application returns rows ## Quick start ```sh flutter doctor -v flutter create fieldapp ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The visual editor and the collaboration it enables for non-developers. - The prebuilt integration catalogue. - Managed builds and store submission handling. - The design polish that comes from a canvas and a component library. - visual editor depth, native builds, integrations, collaboration, and code generation - hundreds of connectors - pixel-perfect app design - mobile app-store builds - enterprise permissions and scale If one of those is essential to you, that is the reason to keep paying for FlutterFlow, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · FlutterFlow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me the mobile application you were going to assemble visually, written directly in Flutter to replace FlutterFlow. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not build a visual app builder. FlutterFlow is a platform and reproducing it is years of work for an audience of one. Write the single application you wanted, in Flutter, generating the repetitive parts from your schema with code generation rather than with a canvas. Decide the distribution question before Phase 1, because it constrains everything: iOS builds require a Mac and a paid Apple account, and losing an Android signing key means you can never update that application listing again. ### Stack (fixed, do not substitute) - Framework: Flutter with Dart, on the current stable channel · one codebase for both platforms, and the code stays readable, which is the point of leaving a visual builder - Backend: Supabase, self-hosted or managed · PostgreSQL with authentication and a generated API, which is the closest thing to what the visual builder was providing - Generation: Dart code generation from a schema file, run as a build step · the repetitive model and form code is generated, and the parts worth writing by hand stay handwritten - Local cache: SQLite through Drift · typed queries and migrations, and a cache that is queryable rather than a blob of JSON - State: Riverpod · explicit, testable, and it does not require the widget tree to be the source of truth ### Data model (create this before Phase 1) - The worked example is a field inspection application. Replace these with your own entities in Phase 2 and keep the shape. - schema.yaml · your entities, fields, types and relations, the single input to code generation - sites(id, name, address, lat, lng, created_at) - inspections(id, site_id, inspector_id, state, started_at, submitted_at, notes) - findings(id, inspection_id, severity, description, photo_path, recorded_at) - sync_queue(id, entity, entity_id, op, payload_json, attempts, last_error) · the outbox that makes offline work ### Phase 1 · Toolchain, and the distribution decision Build: Install Flutter and confirm the doctor is clean for your targets. Run the starter application on a physical Android device and, if you are shipping to iOS, on a real iPhone. Doing this first surfaces the platform obstacles while they are still cheap. Trap: Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. Done when: flutter doctor reports no issues for your intended targets. The starter application runs on a physical Android device. If iOS is a target, it also runs on a physical iPhone, which proves the signing chain works before any real code exists. Do not build yet: No schema, no backend, no screens. ### Phase 2 · Schema and generated models Build: Define entities, fields, types and relations in a schema file. Generate Dart model classes with serialisation and the Drift table definitions from it. Generate the database migration too, so the local schema cannot drift from the models. Trap: Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. Done when: Adding a field to the schema and regenerating produces an updated model, an updated table definition and a migration, with no manual editing. Generated files are marked as generated and are ignored by review. The application still builds after regeneration. Do not build yet: No backend and no screens yet. ### Phase 3 · Authentication and the data source Build: Connect to the backend, implement sign-in, sign-out and token refresh, and enforce access with row level security on the server rather than with filters in the client. Handle the expired session case explicitly. Trap: Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. Done when: A user signs in, sees only their own records, and is signed out cleanly. Querying the API directly with user A's token cannot retrieve user B's rows, verified with a request tool rather than through the application. An expired token refreshes silently, and a revoked one returns the user to sign-in rather than showing an empty screen. Do not build yet: No offline cache and no screens beyond a list yet. ### Phase 4 · Generated screens Build: Generate a list, a detail and a form screen per entity from the schema, with validation derived from field types and nullability. Generate once into editable files rather than regenerating over your refinements. Trap: Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. Done when: Every entity has working list, detail and edit screens reachable from the application. Validation rejects a null in a non-nullable field before submission. Refining a generated screen and regenerating models does not overwrite the refinement. Do not build yet: No offline behaviour yet. ### Phase 5 · Working offline Build: Read from the local database and treat the network as a synchroniser rather than a source. Queue writes in an outbox with retries. Show sync state honestly, including which records are pending and which failed. Trap: An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. Done when: With the network disabled, records can be created and read, and they survive an application restart. Re-enabling the network drains the queue and each record is submitted exactly once. A permanently failing record shows as failed with its error rather than retrying forever in silence. Do not build yet: Distribution is the last phase. ### Phase 6 · Signing, builds and testers Build: Create signing keys for both platforms, back up the Android keystore somewhere you will still have it in five years, and distribute builds through the platform testing channels. Automate the build so a release is not a memory exercise. Trap: Losing the Android upload keystore means you can never update that listing again under the same identifier. Back it up in at least two places outside the build machine before your first release, and record where. This is not recoverable by support in the general case. Done when: A signed Android build installs on a tester's device through the testing channel. If iOS is a target, a build reaches testers through the platform's testing distribution. The Android keystore exists in two backed-up locations and the locations are recorded. A release runs from one documented command. Do not build yet: Nothing after this. ### Out of scope (and why) - The visual editor, which is the product and is why teams pay for it. - The hundreds of prebuilt integrations, each of which is a connector someone maintains. - Collaborative editing of the application by non-developers, which is the capability you lose by moving to code. - The managed build and deploy pipeline, which now costs you the signing, the certificates and the store submissions. ### README must contain - Which platforms this ships to, and that iOS requires a Mac and a paid account - That the schema file is the input and which directories are generated, never hand-edited - That access control lives in row level security policies, and how that was verified from outside the app - How offline queuing works and what the per-record sync states mean - Where the Android keystore backups are, because losing it ends the ability to update ===== AGENTS.md ===== # Agent instructions · FlutterFlow indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Flutter with Dart, on the current stable channel, Supabase, self-hosted or managed, Dart code generation from a schema file, run as a build step, SQLite through Drift, Riverpod. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. - Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. - Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. - Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. - An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. ===== BUILD_PLAN.md ===== # Build plan · FlutterFlow A Flutter application built from a schema you define once: generated data models, a real authentication flow, list and detail and edit screens per entity, an offline cache that survives a train tunnel, and signed builds distributed to testers on both platforms. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Toolchain, and the distribution decision A Flutter application runs on a real device on each platform you intend to ship to. ### Steps 1. Install Flutter and run the doctor Install the SDK, then resolve every doctor warning for your targets. Ignoring them now means debugging them later with more code in the way. ```sh flutter doctor -v flutter create fieldapp ``` 2. Run on a physical Android device Emulators hide performance and permission behaviour. Use a real device from the first day. ```sh flutter devices flutter run -d <device-id> ``` 3. If iOS is a target, run on a real iPhone This requires Xcode, a signing identity and a provisioning profile. It is the single most likely thing to block you, so do it before writing code. ### Done when - [ ] flutter doctor reports no issues for the intended targets - [ ] The starter application runs on a physical Android device - [ ] If iOS is a target, it runs on a physical iPhone - [ ] The decision about which platforms you ship to is written down ### Watch out - Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. ## Phase 2 · Schema and generated models One schema file produces your data models, and changing it regenerates them. ### Steps 1. Write the schema file Entities with fields, types, nullability and relations. This file is the thing you maintain; everything else follows from it. Files: `schema.yaml` 2. Generate models and tables A build step producing serialisable model classes and Drift table definitions. Run it as part of the build so it cannot be forgotten. Files: `lib/generated/` ```sh dart run build_runner build --delete-conflicting-outputs ``` 3. Generate migrations too A schema change must produce a migration. Without it, the local database and the models diverge and the failure appears as mysterious query errors. Files: `lib/db/migrations/` ### Done when - [ ] Adding a schema field regenerates model, table and migration - [ ] Generated files are marked and never hand-edited - [ ] The application builds after regeneration - [ ] A migration applied to an existing database preserves its rows ### Watch out - Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. ## Phase 3 · Authentication and the data source A real person can sign in, and the data they see is the data they are allowed to see. ### Steps 1. Wire authentication Sign in, sign out, and token refresh. Store the token in secure storage, not in shared preferences. Files: `lib/auth/` 2. Write row level security policies One policy per table, on the server. This is the actual access control and everything else is presentation. Files: `supabase/policies.sql` 3. Verify the boundary from outside the app Use a request tool with a second user's token and confirm the refusal. Testing through your own client proves nothing. ### Done when - [ ] A user sees only their own rows - [ ] A direct API call with another user's token is refused - [ ] An expired token refreshes without user action - [ ] A revoked session returns to sign-in rather than an empty screen ### Watch out - Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. ## Phase 4 · Generated screens Each entity has list, detail and edit screens, generated from the schema and then refined by hand. ### Steps 1. Generate screen scaffolds once A one-off generator writing into the normal source tree, not into the generated directory. Different rules for different kinds of code. Files: `tool/gen_screens.dart` 2. Derive validation from the schema Required, type and length come from the field definitions, so validation cannot drift from the database. Files: `lib/forms/` 3. Refine by hand and prove regeneration is safe Change a generated screen, regenerate models, and confirm the change survives. ### Done when - [ ] Every entity has list, detail and edit screens - [ ] Validation matches the schema's nullability and types - [ ] Regenerating models does not overwrite refined screens - [ ] A form submission failure shows an error rather than losing the input ### Watch out - Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. ## Phase 5 · Working offline The application is usable with no signal, and nothing entered is lost when the network returns. ### Steps 1. Read from the local database always The network fills the cache; the interface never waits on it. This inverts the usual structure and is the whole of offline support. Files: `lib/repository/` 2. Add an outbox with retries and a cap Queue writes with attempts and last error. Exponential backoff, and a cap after which it is marked failed rather than retried forever. Files: `lib/sync/outbox.dart` 3. Show sync state in the interface Per record: synced, pending or failed. Not a global spinner, which tells the user nothing about their record. Files: `lib/widgets/sync_badge.dart` ### Done when - [ ] Records created offline survive an application restart - [ ] Reconnecting submits each queued record exactly once - [ ] A permanently failing record is marked failed with its error - [ ] Pending state is visible per record, not only globally ### Watch out - An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. ## Not in this build - The visual editor and the collaboration it enables for non-developers. - The prebuilt integration catalogue. - Managed builds and store submission handling. - The design polish that comes from a canvas and a component library. ## After v1, if you want it - Push notifications, which need platform credentials and a delivery service - Conflict resolution beyond last write wins, once two people edit the same record - A web build from the same codebase, which Flutter supports and which is nearly free here ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your project settings. Public by design. SUPABASE_URL=https://yourproject.supabase.co # Required. Project API settings. Safe in the client only because row level security is doing the work. SUPABASE_ANON_KEY=eyJhbGciOi... # Optional · secret. Product phase only. Never committed. Back it up in two separate places. ANDROID_KEYSTORE_PATH=./keys/upload.jks # Optional. The alias used when the keystore was created. ANDROID_KEY_ALIAS=upload # Optional · secret. Kept in a password manager, never in the repository. ANDROID_KEY_PASSWORD=replace-with-your-password # Required. How many times a queued write retries before being marked failed and shown to the user. SYNC_MAX_ATTEMPTS=8
You are building a lean indie version of FlutterFlow. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # FlutterFlow · indie build A Flutter application built from a schema you define once: generated data models, a real authentication flow, list and detail and edit screens per entity, an offline cache that survives a train tunnel, and signed builds distributed to testers on both platforms. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Framework | Flutter with Dart, on the current stable channel | one codebase for both platforms, and the code stays readable, which is the point of leaving a visual builder | | Backend | Supabase, self-hosted or managed | PostgreSQL with authentication and a generated API, which is the closest thing to what the visual builder was providing | | Generation | Dart code generation from a schema file, run as a build step | the repetitive model and form code is generated, and the parts worth writing by hand stay handwritten | | Local cache | SQLite through Drift | typed queries and migrations, and a cache that is queryable rather than a blob of JSON | | State | Riverpod | explicit, testable, and it does not require the widget tree to be the source of truth | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **The Flutter SDK on the stable channel** · free - Why: The framework. The doctor also tells you which platform toolchains are missing before you waste time. - Get it: Install from flutter.dev following the guide for your operating system, then run the doctor and resolve every warning for your targets. - Verify: flutter doctor reports no issues for your intended platforms - [ ] **A physical Android phone** · free if you have one - Why: Emulators hide performance, permissions and offline behaviour, which are three of the five things this build is about. - Get it: Any recent Android device with developer options and USB debugging enabled. - Verify: flutter devices lists it and flutter run installs to it - [ ] **A Mac with Xcode, only if you ship to iOS** (optional) · free if you own a Mac; otherwise this is the largest cost in the project - Why: There is no supported way to build or sign an iOS application without macOS. This decides your platform scope. - Get it: Any Mac able to run a current Xcode. If you do not have one, decide in Phase 1 that this is Android only. - Verify: A Flutter application runs on a physical iPhone - [ ] **Apple Developer Program membership, only if you ship to iOS** (optional) · 99 USD per year - Why: Required to install on devices you do not own and to use the testing distribution channel. - Get it: Enrol at developer.apple.com/programs. Review can take several days, so start early if iOS is in scope. - Verify: A provisioning profile installs a build on a tester's device - [ ] **Google Play developer account** · 25 USD one time, plus identity verification - Why: Needed for the Android testing channel and any public listing. - Get it: Register at the Play Console. There is a one-time fee and an identity verification step that takes time. - Verify: An internal testing release reaches a tester - [ ] **A Supabase project, managed or self-hosted** · free tier available for managed; self-hosting costs a server - Why: Provides the database, authentication and API. Without it Phase 3 has nothing to talk to. - Get it: Create a project at supabase.com, or run the Docker stack yourself. Take the project URL and the anon key. - Verify: A query from the application returns rows ## Quick start ```sh flutter doctor -v flutter create fieldapp ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The visual editor and the collaboration it enables for non-developers. - The prebuilt integration catalogue. - Managed builds and store submission handling. - The design polish that comes from a canvas and a component library. - visual editor depth, native builds, integrations, collaboration, and code generation - hundreds of connectors - pixel-perfect app design - mobile app-store builds - enterprise permissions and scale If one of those is essential to you, that is the reason to keep paying for FlutterFlow, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · FlutterFlow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me the mobile application you were going to assemble visually, written directly in Flutter to replace FlutterFlow. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not build a visual app builder. FlutterFlow is a platform and reproducing it is years of work for an audience of one. Write the single application you wanted, in Flutter, generating the repetitive parts from your schema with code generation rather than with a canvas. Decide the distribution question before Phase 1, because it constrains everything: iOS builds require a Mac and a paid Apple account, and losing an Android signing key means you can never update that application listing again. ### Stack (fixed, do not substitute) - Framework: Flutter with Dart, on the current stable channel · one codebase for both platforms, and the code stays readable, which is the point of leaving a visual builder - Backend: Supabase, self-hosted or managed · PostgreSQL with authentication and a generated API, which is the closest thing to what the visual builder was providing - Generation: Dart code generation from a schema file, run as a build step · the repetitive model and form code is generated, and the parts worth writing by hand stay handwritten - Local cache: SQLite through Drift · typed queries and migrations, and a cache that is queryable rather than a blob of JSON - State: Riverpod · explicit, testable, and it does not require the widget tree to be the source of truth ### Data model (create this before Phase 1) - The worked example is a field inspection application. Replace these with your own entities in Phase 2 and keep the shape. - schema.yaml · your entities, fields, types and relations, the single input to code generation - sites(id, name, address, lat, lng, created_at) - inspections(id, site_id, inspector_id, state, started_at, submitted_at, notes) - findings(id, inspection_id, severity, description, photo_path, recorded_at) - sync_queue(id, entity, entity_id, op, payload_json, attempts, last_error) · the outbox that makes offline work ### Phase 1 · Toolchain, and the distribution decision Build: Install Flutter and confirm the doctor is clean for your targets. Run the starter application on a physical Android device and, if you are shipping to iOS, on a real iPhone. Doing this first surfaces the platform obstacles while they are still cheap. Trap: Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. Done when: flutter doctor reports no issues for your intended targets. The starter application runs on a physical Android device. If iOS is a target, it also runs on a physical iPhone, which proves the signing chain works before any real code exists. Do not build yet: No schema, no backend, no screens. ### Phase 2 · Schema and generated models Build: Define entities, fields, types and relations in a schema file. Generate Dart model classes with serialisation and the Drift table definitions from it. Generate the database migration too, so the local schema cannot drift from the models. Trap: Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. Done when: Adding a field to the schema and regenerating produces an updated model, an updated table definition and a migration, with no manual editing. Generated files are marked as generated and are ignored by review. The application still builds after regeneration. Do not build yet: No backend and no screens yet. ### Phase 3 · Authentication and the data source Build: Connect to the backend, implement sign-in, sign-out and token refresh, and enforce access with row level security on the server rather than with filters in the client. Handle the expired session case explicitly. Trap: Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. Done when: A user signs in, sees only their own records, and is signed out cleanly. Querying the API directly with user A's token cannot retrieve user B's rows, verified with a request tool rather than through the application. An expired token refreshes silently, and a revoked one returns the user to sign-in rather than showing an empty screen. Do not build yet: No offline cache and no screens beyond a list yet. ### Phase 4 · Generated screens Build: Generate a list, a detail and a form screen per entity from the schema, with validation derived from field types and nullability. Generate once into editable files rather than regenerating over your refinements. Trap: Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. Done when: Every entity has working list, detail and edit screens reachable from the application. Validation rejects a null in a non-nullable field before submission. Refining a generated screen and regenerating models does not overwrite the refinement. Do not build yet: No offline behaviour yet. ### Phase 5 · Working offline Build: Read from the local database and treat the network as a synchroniser rather than a source. Queue writes in an outbox with retries. Show sync state honestly, including which records are pending and which failed. Trap: An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. Done when: With the network disabled, records can be created and read, and they survive an application restart. Re-enabling the network drains the queue and each record is submitted exactly once. A permanently failing record shows as failed with its error rather than retrying forever in silence. Do not build yet: Distribution is the last phase. ### Phase 6 · Signing, builds and testers Build: Create signing keys for both platforms, back up the Android keystore somewhere you will still have it in five years, and distribute builds through the platform testing channels. Automate the build so a release is not a memory exercise. Trap: Losing the Android upload keystore means you can never update that listing again under the same identifier. Back it up in at least two places outside the build machine before your first release, and record where. This is not recoverable by support in the general case. Done when: A signed Android build installs on a tester's device through the testing channel. If iOS is a target, a build reaches testers through the platform's testing distribution. The Android keystore exists in two backed-up locations and the locations are recorded. A release runs from one documented command. Do not build yet: Nothing after this. ### Out of scope (and why) - The visual editor, which is the product and is why teams pay for it. - The hundreds of prebuilt integrations, each of which is a connector someone maintains. - Collaborative editing of the application by non-developers, which is the capability you lose by moving to code. - The managed build and deploy pipeline, which now costs you the signing, the certificates and the store submissions. ### README must contain - Which platforms this ships to, and that iOS requires a Mac and a paid account - That the schema file is the input and which directories are generated, never hand-edited - That access control lives in row level security policies, and how that was verified from outside the app - How offline queuing works and what the per-record sync states mean - Where the Android keystore backups are, because losing it ends the ability to update ===== AGENTS.md ===== # Agent instructions · FlutterFlow indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Flutter with Dart, on the current stable channel, Supabase, self-hosted or managed, Dart code generation from a schema file, run as a build step, SQLite through Drift, Riverpod. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. - Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. - Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. - Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. - An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. ===== BUILD_PLAN.md ===== # Build plan · FlutterFlow A Flutter application built from a schema you define once: generated data models, a real authentication flow, list and detail and edit screens per entity, an offline cache that survives a train tunnel, and signed builds distributed to testers on both platforms. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Toolchain, and the distribution decision A Flutter application runs on a real device on each platform you intend to ship to. ### Steps 1. Install Flutter and run the doctor Install the SDK, then resolve every doctor warning for your targets. Ignoring them now means debugging them later with more code in the way. ```sh flutter doctor -v flutter create fieldapp ``` 2. Run on a physical Android device Emulators hide performance and permission behaviour. Use a real device from the first day. ```sh flutter devices flutter run -d <device-id> ``` 3. If iOS is a target, run on a real iPhone This requires Xcode, a signing identity and a provisioning profile. It is the single most likely thing to block you, so do it before writing code. ### Done when - [ ] flutter doctor reports no issues for the intended targets - [ ] The starter application runs on a physical Android device - [ ] If iOS is a target, it runs on a physical iPhone - [ ] The decision about which platforms you ship to is written down ### Watch out - Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. ## Phase 2 · Schema and generated models One schema file produces your data models, and changing it regenerates them. ### Steps 1. Write the schema file Entities with fields, types, nullability and relations. This file is the thing you maintain; everything else follows from it. Files: `schema.yaml` 2. Generate models and tables A build step producing serialisable model classes and Drift table definitions. Run it as part of the build so it cannot be forgotten. Files: `lib/generated/` ```sh dart run build_runner build --delete-conflicting-outputs ``` 3. Generate migrations too A schema change must produce a migration. Without it, the local database and the models diverge and the failure appears as mysterious query errors. Files: `lib/db/migrations/` ### Done when - [ ] Adding a schema field regenerates model, table and migration - [ ] Generated files are marked and never hand-edited - [ ] The application builds after regeneration - [ ] A migration applied to an existing database preserves its rows ### Watch out - Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. ## Phase 3 · Authentication and the data source A real person can sign in, and the data they see is the data they are allowed to see. ### Steps 1. Wire authentication Sign in, sign out, and token refresh. Store the token in secure storage, not in shared preferences. Files: `lib/auth/` 2. Write row level security policies One policy per table, on the server. This is the actual access control and everything else is presentation. Files: `supabase/policies.sql` 3. Verify the boundary from outside the app Use a request tool with a second user's token and confirm the refusal. Testing through your own client proves nothing. ### Done when - [ ] A user sees only their own rows - [ ] A direct API call with another user's token is refused - [ ] An expired token refreshes without user action - [ ] A revoked session returns to sign-in rather than an empty screen ### Watch out - Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. ## Phase 4 · Generated screens Each entity has list, detail and edit screens, generated from the schema and then refined by hand. ### Steps 1. Generate screen scaffolds once A one-off generator writing into the normal source tree, not into the generated directory. Different rules for different kinds of code. Files: `tool/gen_screens.dart` 2. Derive validation from the schema Required, type and length come from the field definitions, so validation cannot drift from the database. Files: `lib/forms/` 3. Refine by hand and prove regeneration is safe Change a generated screen, regenerate models, and confirm the change survives. ### Done when - [ ] Every entity has list, detail and edit screens - [ ] Validation matches the schema's nullability and types - [ ] Regenerating models does not overwrite refined screens - [ ] A form submission failure shows an error rather than losing the input ### Watch out - Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. ## Phase 5 · Working offline The application is usable with no signal, and nothing entered is lost when the network returns. ### Steps 1. Read from the local database always The network fills the cache; the interface never waits on it. This inverts the usual structure and is the whole of offline support. Files: `lib/repository/` 2. Add an outbox with retries and a cap Queue writes with attempts and last error. Exponential backoff, and a cap after which it is marked failed rather than retried forever. Files: `lib/sync/outbox.dart` 3. Show sync state in the interface Per record: synced, pending or failed. Not a global spinner, which tells the user nothing about their record. Files: `lib/widgets/sync_badge.dart` ### Done when - [ ] Records created offline survive an application restart - [ ] Reconnecting submits each queued record exactly once - [ ] A permanently failing record is marked failed with its error - [ ] Pending state is visible per record, not only globally ### Watch out - An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. ## Not in this build - The visual editor and the collaboration it enables for non-developers. - The prebuilt integration catalogue. - Managed builds and store submission handling. - The design polish that comes from a canvas and a component library. ## After v1, if you want it - Push notifications, which need platform credentials and a delivery service - Conflict resolution beyond last write wins, once two people edit the same record - A web build from the same codebase, which Flutter supports and which is nearly free here ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your project settings. Public by design. SUPABASE_URL=https://yourproject.supabase.co # Required. Project API settings. Safe in the client only because row level security is doing the work. SUPABASE_ANON_KEY=eyJhbGciOi... # Optional · secret. Product phase only. Never committed. Back it up in two separate places. ANDROID_KEYSTORE_PATH=./keys/upload.jks # Optional. The alias used when the keystore was created. ANDROID_KEY_ALIAS=upload # Optional · secret. Kept in a password manager, never in the repository. ANDROID_KEY_PASSWORD=replace-with-your-password # Required. How many times a queued write retries before being marked failed and shown to the user. SYNC_MAX_ATTEMPTS=8
You are building a production product version of FlutterFlow. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # FlutterFlow · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For FlutterFlow, assemble a mobile interface from a schema and export maintainable Flutter code. The hard boundary is visual editor depth, native builds, integrations, collaboration, and code generation, plus connectors, runtime reliability, and governance. ## Product outcome A Flutter application generated from a schema you own, with access control enforced on the server, offline behaviour that never silently loses an entry, and signed builds reaching real testers with the keystore backed up. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - self-hosted deploy target - API credentials for one selected integration ## Explicit non-goals for v1 - The visual editor and the collaboration it enables for non-developers. - The prebuilt integration catalogue. - Managed builds and store submission handling. - The design polish that comes from a canvas and a component library. - visual editor depth, native builds, integrations, collaboration, and code generation - hundreds of connectors - pixel-perfect app design - mobile app-store builds - enterprise permissions and scale ## Success criteria - A direct API call with another user's token is refused - Offline creation and reconnection is verified to submit exactly once - The Android keystore is confirmed present in two backup locations - A signed build installs on a device you do not own - Regenerating models is confirmed not to overwrite refined screens ===== BRIEF.md ===== # Build brief · FlutterFlow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me the mobile application you were going to assemble visually, written directly in Flutter to replace FlutterFlow. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not build a visual app builder. FlutterFlow is a platform and reproducing it is years of work for an audience of one. Write the single application you wanted, in Flutter, generating the repetitive parts from your schema with code generation rather than with a canvas. Decide the distribution question before Phase 1, because it constrains everything: iOS builds require a Mac and a paid Apple account, and losing an Android signing key means you can never update that application listing again. ### Stack (fixed, do not substitute) - Framework: Flutter with Dart, on the current stable channel · one codebase for both platforms, and the code stays readable, which is the point of leaving a visual builder - Backend: Supabase, self-hosted or managed · PostgreSQL with authentication and a generated API, which is the closest thing to what the visual builder was providing - Generation: Dart code generation from a schema file, run as a build step · the repetitive model and form code is generated, and the parts worth writing by hand stay handwritten - Local cache: SQLite through Drift · typed queries and migrations, and a cache that is queryable rather than a blob of JSON - State: Riverpod · explicit, testable, and it does not require the widget tree to be the source of truth ### Data model (create this before Phase 1) - The worked example is a field inspection application. Replace these with your own entities in Phase 2 and keep the shape. - schema.yaml · your entities, fields, types and relations, the single input to code generation - sites(id, name, address, lat, lng, created_at) - inspections(id, site_id, inspector_id, state, started_at, submitted_at, notes) - findings(id, inspection_id, severity, description, photo_path, recorded_at) - sync_queue(id, entity, entity_id, op, payload_json, attempts, last_error) · the outbox that makes offline work ### Phase 1 · Toolchain, and the distribution decision Build: Install Flutter and confirm the doctor is clean for your targets. Run the starter application on a physical Android device and, if you are shipping to iOS, on a real iPhone. Doing this first surfaces the platform obstacles while they are still cheap. Trap: Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. Done when: flutter doctor reports no issues for your intended targets. The starter application runs on a physical Android device. If iOS is a target, it also runs on a physical iPhone, which proves the signing chain works before any real code exists. Do not build yet: No schema, no backend, no screens. ### Phase 2 · Schema and generated models Build: Define entities, fields, types and relations in a schema file. Generate Dart model classes with serialisation and the Drift table definitions from it. Generate the database migration too, so the local schema cannot drift from the models. Trap: Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. Done when: Adding a field to the schema and regenerating produces an updated model, an updated table definition and a migration, with no manual editing. Generated files are marked as generated and are ignored by review. The application still builds after regeneration. Do not build yet: No backend and no screens yet. ### Phase 3 · Authentication and the data source Build: Connect to the backend, implement sign-in, sign-out and token refresh, and enforce access with row level security on the server rather than with filters in the client. Handle the expired session case explicitly. Trap: Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. Done when: A user signs in, sees only their own records, and is signed out cleanly. Querying the API directly with user A's token cannot retrieve user B's rows, verified with a request tool rather than through the application. An expired token refreshes silently, and a revoked one returns the user to sign-in rather than showing an empty screen. Do not build yet: No offline cache and no screens beyond a list yet. ### Phase 4 · Generated screens Build: Generate a list, a detail and a form screen per entity from the schema, with validation derived from field types and nullability. Generate once into editable files rather than regenerating over your refinements. Trap: Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. Done when: Every entity has working list, detail and edit screens reachable from the application. Validation rejects a null in a non-nullable field before submission. Refining a generated screen and regenerating models does not overwrite the refinement. Do not build yet: No offline behaviour yet. ### Phase 5 · Working offline Build: Read from the local database and treat the network as a synchroniser rather than a source. Queue writes in an outbox with retries. Show sync state honestly, including which records are pending and which failed. Trap: An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. Done when: With the network disabled, records can be created and read, and they survive an application restart. Re-enabling the network drains the queue and each record is submitted exactly once. A permanently failing record shows as failed with its error rather than retrying forever in silence. Do not build yet: Distribution is the last phase. ### Phase 6 · Signing, builds and testers Build: Create signing keys for both platforms, back up the Android keystore somewhere you will still have it in five years, and distribute builds through the platform testing channels. Automate the build so a release is not a memory exercise. Trap: Losing the Android upload keystore means you can never update that listing again under the same identifier. Back it up in at least two places outside the build machine before your first release, and record where. This is not recoverable by support in the general case. Done when: A signed Android build installs on a tester's device through the testing channel. If iOS is a target, a build reaches testers through the platform's testing distribution. The Android keystore exists in two backed-up locations and the locations are recorded. A release runs from one documented command. Do not build yet: Nothing after this. ### Out of scope (and why) - The visual editor, which is the product and is why teams pay for it. - The hundreds of prebuilt integrations, each of which is a connector someone maintains. - Collaborative editing of the application by non-developers, which is the capability you lose by moving to code. - The managed build and deploy pipeline, which now costs you the signing, the certificates and the store submissions. ### README must contain - Which platforms this ships to, and that iOS requires a Mac and a paid account - That the schema file is the input and which directories are generated, never hand-edited - That access control lives in row level security policies, and how that was verified from outside the app - How offline queuing works and what the per-record sync states mean - Where the Android keystore backups are, because losing it ends the ability to update ===== ARCHITECTURE.md ===== # Architecture · FlutterFlow ## Stack | Part | Choice | Why | | --- | --- | --- | | Framework | Flutter with Dart, on the current stable channel | one codebase for both platforms, and the code stays readable, which is the point of leaving a visual builder | | Backend | Supabase, self-hosted or managed | PostgreSQL with authentication and a generated API, which is the closest thing to what the visual builder was providing | | Generation | Dart code generation from a schema file, run as a build step | the repetitive model and form code is generated, and the parts worth writing by hand stay handwritten | | Local cache | SQLite through Drift | typed queries and migrations, and a cache that is queryable rather than a blob of JSON | | State | Riverpod | explicit, testable, and it does not require the widget tree to be the source of truth | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Schema | The single definition of entities and fields. | This is the input to everything generated. Replacing the generator is fine; replacing the single-source principle is not. | | Generation | Models, tables and migrations, continuously; screens, once. | The split between continuous and one-off generation is the important decision. Keep judgement out of continuously regenerated files. | | Auth and policies | Identity and row level access control. | A different backend changes the policy syntax, not the principle that the server enforces access. | | Repository | Reading locally and synchronising remotely. | The rule that the interface reads from the local database is what makes offline work. Any swap must preserve it. | | Outbox | Queued writes, retries and per-record state. | Implementation is replaceable; the visible pending and failed states are not. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `SUPABASE_URL` · required · Your project settings. Public by design. - `SUPABASE_ANON_KEY` · required · Project API settings. Safe in the client only because row level security is doing the work. - `ANDROID_KEYSTORE_PATH` · optional, secret · Product phase only. Never committed. Back it up in two separate places. - `ANDROID_KEY_ALIAS` · optional · The alias used when the keystore was created. - `ANDROID_KEY_PASSWORD` · optional, secret · Kept in a password manager, never in the repository. - `SYNC_MAX_ATTEMPTS` · required · How many times a queued write retries before being marked failed and shown to the user. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · FlutterFlow product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Flutter with Dart, on the current stable channel, Supabase, self-hosted or managed, Dart code generation from a schema file, run as a build step, SQLite through Drift, Riverpod. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. - Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. - Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. - Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. - An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. - Losing the Android upload keystore means you can never update that listing again under the same identifier. Back it up in at least two places outside the build machine before your first release, and record where. This is not recoverable by support in the general case. ===== MILESTONES.md ===== # Delivery milestones · FlutterFlow Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Toolchain, and the distribution decision A Flutter application runs on a real device on each platform you intend to ship to. ### Steps 1. Install Flutter and run the doctor Install the SDK, then resolve every doctor warning for your targets. Ignoring them now means debugging them later with more code in the way. ```sh flutter doctor -v flutter create fieldapp ``` 2. Run on a physical Android device Emulators hide performance and permission behaviour. Use a real device from the first day. ```sh flutter devices flutter run -d <device-id> ``` 3. If iOS is a target, run on a real iPhone This requires Xcode, a signing identity and a provisioning profile. It is the single most likely thing to block you, so do it before writing code. ### Done when - [ ] flutter doctor reports no issues for the intended targets - [ ] The starter application runs on a physical Android device - [ ] If iOS is a target, it runs on a physical iPhone - [ ] The decision about which platforms you ship to is written down ### Watch out - Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. ## M2 · Schema and generated models One schema file produces your data models, and changing it regenerates them. ### Steps 1. Write the schema file Entities with fields, types, nullability and relations. This file is the thing you maintain; everything else follows from it. Files: `schema.yaml` 2. Generate models and tables A build step producing serialisable model classes and Drift table definitions. Run it as part of the build so it cannot be forgotten. Files: `lib/generated/` ```sh dart run build_runner build --delete-conflicting-outputs ``` 3. Generate migrations too A schema change must produce a migration. Without it, the local database and the models diverge and the failure appears as mysterious query errors. Files: `lib/db/migrations/` ### Done when - [ ] Adding a schema field regenerates model, table and migration - [ ] Generated files are marked and never hand-edited - [ ] The application builds after regeneration - [ ] A migration applied to an existing database preserves its rows ### Watch out - Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. ## M3 · Authentication and the data source A real person can sign in, and the data they see is the data they are allowed to see. ### Steps 1. Wire authentication Sign in, sign out, and token refresh. Store the token in secure storage, not in shared preferences. Files: `lib/auth/` 2. Write row level security policies One policy per table, on the server. This is the actual access control and everything else is presentation. Files: `supabase/policies.sql` 3. Verify the boundary from outside the app Use a request tool with a second user's token and confirm the refusal. Testing through your own client proves nothing. ### Done when - [ ] A user sees only their own rows - [ ] A direct API call with another user's token is refused - [ ] An expired token refreshes without user action - [ ] A revoked session returns to sign-in rather than an empty screen ### Watch out - Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. ## M4 · Generated screens Each entity has list, detail and edit screens, generated from the schema and then refined by hand. ### Steps 1. Generate screen scaffolds once A one-off generator writing into the normal source tree, not into the generated directory. Different rules for different kinds of code. Files: `tool/gen_screens.dart` 2. Derive validation from the schema Required, type and length come from the field definitions, so validation cannot drift from the database. Files: `lib/forms/` 3. Refine by hand and prove regeneration is safe Change a generated screen, regenerate models, and confirm the change survives. ### Done when - [ ] Every entity has list, detail and edit screens - [ ] Validation matches the schema's nullability and types - [ ] Regenerating models does not overwrite refined screens - [ ] A form submission failure shows an error rather than losing the input ### Watch out - Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. ## M5 · Working offline The application is usable with no signal, and nothing entered is lost when the network returns. ### Steps 1. Read from the local database always The network fills the cache; the interface never waits on it. This inverts the usual structure and is the whole of offline support. Files: `lib/repository/` 2. Add an outbox with retries and a cap Queue writes with attempts and last error. Exponential backoff, and a cap after which it is marked failed rather than retried forever. Files: `lib/sync/outbox.dart` 3. Show sync state in the interface Per record: synced, pending or failed. Not a global spinner, which tells the user nothing about their record. Files: `lib/widgets/sync_badge.dart` ### Done when - [ ] Records created offline survive an application restart - [ ] Reconnecting submits each queued record exactly once - [ ] A permanently failing record is marked failed with its error - [ ] Pending state is visible per record, not only globally ### Watch out - An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. ## M6 · Signing, builds and testers (production only) Real testers install it on their own phones, and you can still ship an update in a year. ### Steps 1. Create and back up signing keys Generate the Android keystore, then copy it to two separate durable locations before you use it. Record where they are. Files: `docs/release.md` ```sh keytool -genkey -v -keystore upload.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload ``` 2. Automate the build One script producing signed artefacts for each target, so a release does not depend on remembering flags. Files: `scripts/release.sh` ```sh flutter build appbundle --release ``` 3. Distribute to real testers Use the platform testing channels. Installing on a device you do not own is the test that signing is genuinely correct. ### Done when - [ ] A signed build installs on a tester's device - [ ] The Android keystore is backed up in two places and the locations are recorded - [ ] A release runs from one documented command - [ ] If iOS is a target, a build reaches testers through its testing channel ### Watch out - Losing the Android upload keystore means you can never update that listing again under the same identifier. Back it up in at least two places outside the build machine before your first release, and record where. This is not recoverable by support in the general case. ===== OPERATIONS.md ===== # Operations · FlutterFlow ## Backup The backend database is the durable data and needs its own nightly dump off the machine. The Android keystore is the other irreplaceable artefact and must exist in two separate durable locations. ## Restore Restore the database, redeploy, and sign in. For the keystore there is no restore other than your backup, which is why it is called out separately from ordinary data. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Backend uptime, and the proportion of records sitting in outboxes across your users if you report it. A rising failed count usually means a policy change broke writes for a subset of users. ## Incident checklist For a bad schema migration, restore the pre-migration dump rather than writing a reverse migration under pressure. For a signing failure at release time, stop and verify the keystore backup before doing anything else. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A direct API call with another user's token is refused - [ ] Offline creation and reconnection is verified to submit exactly once - [ ] The Android keystore is confirmed present in two backup locations - [ ] A signed build installs on a device you do not own - [ ] Regenerating models is confirmed not to overwrite refined screens ## Launch constraint Do not market omitted FlutterFlow capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your project settings. Public by design. SUPABASE_URL=https://yourproject.supabase.co # Required. Project API settings. Safe in the client only because row level security is doing the work. SUPABASE_ANON_KEY=eyJhbGciOi... # Optional · secret. Product phase only. Never committed. Back it up in two separate places. ANDROID_KEYSTORE_PATH=./keys/upload.jks # Optional. The alias used when the keystore was created. ANDROID_KEY_ALIAS=upload # Optional · secret. Kept in a password manager, never in the repository. ANDROID_KEY_PASSWORD=replace-with-your-password # Required. How many times a queued write retries before being marked failed and shown to the user. SYNC_MAX_ATTEMPTS=8
# FlutterFlow · indie build A Flutter application built from a schema you define once: generated data models, a real authentication flow, list and detail and edit screens per entity, an offline cache that survives a train tunnel, and signed builds distributed to testers on both platforms. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Framework | Flutter with Dart, on the current stable channel | one codebase for both platforms, and the code stays readable, which is the point of leaving a visual builder | | Backend | Supabase, self-hosted or managed | PostgreSQL with authentication and a generated API, which is the closest thing to what the visual builder was providing | | Generation | Dart code generation from a schema file, run as a build step | the repetitive model and form code is generated, and the parts worth writing by hand stay handwritten | | Local cache | SQLite through Drift | typed queries and migrations, and a cache that is queryable rather than a blob of JSON | | State | Riverpod | explicit, testable, and it does not require the widget tree to be the source of truth | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **The Flutter SDK on the stable channel** · free - Why: The framework. The doctor also tells you which platform toolchains are missing before you waste time. - Get it: Install from flutter.dev following the guide for your operating system, then run the doctor and resolve every warning for your targets. - Verify: flutter doctor reports no issues for your intended platforms - [ ] **A physical Android phone** · free if you have one - Why: Emulators hide performance, permissions and offline behaviour, which are three of the five things this build is about. - Get it: Any recent Android device with developer options and USB debugging enabled. - Verify: flutter devices lists it and flutter run installs to it - [ ] **A Mac with Xcode, only if you ship to iOS** (optional) · free if you own a Mac; otherwise this is the largest cost in the project - Why: There is no supported way to build or sign an iOS application without macOS. This decides your platform scope. - Get it: Any Mac able to run a current Xcode. If you do not have one, decide in Phase 1 that this is Android only. - Verify: A Flutter application runs on a physical iPhone - [ ] **Apple Developer Program membership, only if you ship to iOS** (optional) · 99 USD per year - Why: Required to install on devices you do not own and to use the testing distribution channel. - Get it: Enrol at developer.apple.com/programs. Review can take several days, so start early if iOS is in scope. - Verify: A provisioning profile installs a build on a tester's device - [ ] **Google Play developer account** · 25 USD one time, plus identity verification - Why: Needed for the Android testing channel and any public listing. - Get it: Register at the Play Console. There is a one-time fee and an identity verification step that takes time. - Verify: An internal testing release reaches a tester - [ ] **A Supabase project, managed or self-hosted** · free tier available for managed; self-hosting costs a server - Why: Provides the database, authentication and API. Without it Phase 3 has nothing to talk to. - Get it: Create a project at supabase.com, or run the Docker stack yourself. Take the project URL and the anon key. - Verify: A query from the application returns rows ## Quick start ```sh flutter doctor -v flutter create fieldapp ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - The visual editor and the collaboration it enables for non-developers. - The prebuilt integration catalogue. - Managed builds and store submission handling. - The design polish that comes from a canvas and a component library. - visual editor depth, native builds, integrations, collaboration, and code generation - hundreds of connectors - pixel-perfect app design - mobile app-store builds - enterprise permissions and scale If one of those is essential to you, that is the reason to keep paying for FlutterFlow, and the README should say so rather than pretend.
# Build brief · FlutterFlow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me the mobile application you were going to assemble visually, written directly in Flutter to replace FlutterFlow. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not build a visual app builder. FlutterFlow is a platform and reproducing it is years of work for an audience of one. Write the single application you wanted, in Flutter, generating the repetitive parts from your schema with code generation rather than with a canvas. Decide the distribution question before Phase 1, because it constrains everything: iOS builds require a Mac and a paid Apple account, and losing an Android signing key means you can never update that application listing again. ### Stack (fixed, do not substitute) - Framework: Flutter with Dart, on the current stable channel · one codebase for both platforms, and the code stays readable, which is the point of leaving a visual builder - Backend: Supabase, self-hosted or managed · PostgreSQL with authentication and a generated API, which is the closest thing to what the visual builder was providing - Generation: Dart code generation from a schema file, run as a build step · the repetitive model and form code is generated, and the parts worth writing by hand stay handwritten - Local cache: SQLite through Drift · typed queries and migrations, and a cache that is queryable rather than a blob of JSON - State: Riverpod · explicit, testable, and it does not require the widget tree to be the source of truth ### Data model (create this before Phase 1) - The worked example is a field inspection application. Replace these with your own entities in Phase 2 and keep the shape. - schema.yaml · your entities, fields, types and relations, the single input to code generation - sites(id, name, address, lat, lng, created_at) - inspections(id, site_id, inspector_id, state, started_at, submitted_at, notes) - findings(id, inspection_id, severity, description, photo_path, recorded_at) - sync_queue(id, entity, entity_id, op, payload_json, attempts, last_error) · the outbox that makes offline work ### Phase 1 · Toolchain, and the distribution decision Build: Install Flutter and confirm the doctor is clean for your targets. Run the starter application on a physical Android device and, if you are shipping to iOS, on a real iPhone. Doing this first surfaces the platform obstacles while they are still cheap. Trap: Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. Done when: flutter doctor reports no issues for your intended targets. The starter application runs on a physical Android device. If iOS is a target, it also runs on a physical iPhone, which proves the signing chain works before any real code exists. Do not build yet: No schema, no backend, no screens. ### Phase 2 · Schema and generated models Build: Define entities, fields, types and relations in a schema file. Generate Dart model classes with serialisation and the Drift table definitions from it. Generate the database migration too, so the local schema cannot drift from the models. Trap: Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. Done when: Adding a field to the schema and regenerating produces an updated model, an updated table definition and a migration, with no manual editing. Generated files are marked as generated and are ignored by review. The application still builds after regeneration. Do not build yet: No backend and no screens yet. ### Phase 3 · Authentication and the data source Build: Connect to the backend, implement sign-in, sign-out and token refresh, and enforce access with row level security on the server rather than with filters in the client. Handle the expired session case explicitly. Trap: Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. Done when: A user signs in, sees only their own records, and is signed out cleanly. Querying the API directly with user A's token cannot retrieve user B's rows, verified with a request tool rather than through the application. An expired token refreshes silently, and a revoked one returns the user to sign-in rather than showing an empty screen. Do not build yet: No offline cache and no screens beyond a list yet. ### Phase 4 · Generated screens Build: Generate a list, a detail and a form screen per entity from the schema, with validation derived from field types and nullability. Generate once into editable files rather than regenerating over your refinements. Trap: Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. Done when: Every entity has working list, detail and edit screens reachable from the application. Validation rejects a null in a non-nullable field before submission. Refining a generated screen and regenerating models does not overwrite the refinement. Do not build yet: No offline behaviour yet. ### Phase 5 · Working offline Build: Read from the local database and treat the network as a synchroniser rather than a source. Queue writes in an outbox with retries. Show sync state honestly, including which records are pending and which failed. Trap: An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. Done when: With the network disabled, records can be created and read, and they survive an application restart. Re-enabling the network drains the queue and each record is submitted exactly once. A permanently failing record shows as failed with its error rather than retrying forever in silence. Do not build yet: Distribution is the last phase. ### Phase 6 · Signing, builds and testers Build: Create signing keys for both platforms, back up the Android keystore somewhere you will still have it in five years, and distribute builds through the platform testing channels. Automate the build so a release is not a memory exercise. Trap: Losing the Android upload keystore means you can never update that listing again under the same identifier. Back it up in at least two places outside the build machine before your first release, and record where. This is not recoverable by support in the general case. Done when: A signed Android build installs on a tester's device through the testing channel. If iOS is a target, a build reaches testers through the platform's testing distribution. The Android keystore exists in two backed-up locations and the locations are recorded. A release runs from one documented command. Do not build yet: Nothing after this. ### Out of scope (and why) - The visual editor, which is the product and is why teams pay for it. - The hundreds of prebuilt integrations, each of which is a connector someone maintains. - Collaborative editing of the application by non-developers, which is the capability you lose by moving to code. - The managed build and deploy pipeline, which now costs you the signing, the certificates and the store submissions. ### README must contain - Which platforms this ships to, and that iOS requires a Mac and a paid account - That the schema file is the input and which directories are generated, never hand-edited - That access control lives in row level security policies, and how that was verified from outside the app - How offline queuing works and what the per-record sync states mean - Where the Android keystore backups are, because losing it ends the ability to update
# Agent instructions · FlutterFlow indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Flutter with Dart, on the current stable channel, Supabase, self-hosted or managed, Dart code generation from a schema file, run as a build step, SQLite through Drift, Riverpod. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. - Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. - Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. - Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. - An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted.
# Build plan · FlutterFlow A Flutter application built from a schema you define once: generated data models, a real authentication flow, list and detail and edit screens per entity, an offline cache that survives a train tunnel, and signed builds distributed to testers on both platforms. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Toolchain, and the distribution decision A Flutter application runs on a real device on each platform you intend to ship to. ### Steps 1. Install Flutter and run the doctor Install the SDK, then resolve every doctor warning for your targets. Ignoring them now means debugging them later with more code in the way. ```sh flutter doctor -v flutter create fieldapp ``` 2. Run on a physical Android device Emulators hide performance and permission behaviour. Use a real device from the first day. ```sh flutter devices flutter run -d <device-id> ``` 3. If iOS is a target, run on a real iPhone This requires Xcode, a signing identity and a provisioning profile. It is the single most likely thing to block you, so do it before writing code. ### Done when - [ ] flutter doctor reports no issues for the intended targets - [ ] The starter application runs on a physical Android device - [ ] If iOS is a target, it runs on a physical iPhone - [ ] The decision about which platforms you ship to is written down ### Watch out - Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. ## Phase 2 · Schema and generated models One schema file produces your data models, and changing it regenerates them. ### Steps 1. Write the schema file Entities with fields, types, nullability and relations. This file is the thing you maintain; everything else follows from it. Files: `schema.yaml` 2. Generate models and tables A build step producing serialisable model classes and Drift table definitions. Run it as part of the build so it cannot be forgotten. Files: `lib/generated/` ```sh dart run build_runner build --delete-conflicting-outputs ``` 3. Generate migrations too A schema change must produce a migration. Without it, the local database and the models diverge and the failure appears as mysterious query errors. Files: `lib/db/migrations/` ### Done when - [ ] Adding a schema field regenerates model, table and migration - [ ] Generated files are marked and never hand-edited - [ ] The application builds after regeneration - [ ] A migration applied to an existing database preserves its rows ### Watch out - Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. ## Phase 3 · Authentication and the data source A real person can sign in, and the data they see is the data they are allowed to see. ### Steps 1. Wire authentication Sign in, sign out, and token refresh. Store the token in secure storage, not in shared preferences. Files: `lib/auth/` 2. Write row level security policies One policy per table, on the server. This is the actual access control and everything else is presentation. Files: `supabase/policies.sql` 3. Verify the boundary from outside the app Use a request tool with a second user's token and confirm the refusal. Testing through your own client proves nothing. ### Done when - [ ] A user sees only their own rows - [ ] A direct API call with another user's token is refused - [ ] An expired token refreshes without user action - [ ] A revoked session returns to sign-in rather than an empty screen ### Watch out - Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. ## Phase 4 · Generated screens Each entity has list, detail and edit screens, generated from the schema and then refined by hand. ### Steps 1. Generate screen scaffolds once A one-off generator writing into the normal source tree, not into the generated directory. Different rules for different kinds of code. Files: `tool/gen_screens.dart` 2. Derive validation from the schema Required, type and length come from the field definitions, so validation cannot drift from the database. Files: `lib/forms/` 3. Refine by hand and prove regeneration is safe Change a generated screen, regenerate models, and confirm the change survives. ### Done when - [ ] Every entity has list, detail and edit screens - [ ] Validation matches the schema's nullability and types - [ ] Regenerating models does not overwrite refined screens - [ ] A form submission failure shows an error rather than losing the input ### Watch out - Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. ## Phase 5 · Working offline The application is usable with no signal, and nothing entered is lost when the network returns. ### Steps 1. Read from the local database always The network fills the cache; the interface never waits on it. This inverts the usual structure and is the whole of offline support. Files: `lib/repository/` 2. Add an outbox with retries and a cap Queue writes with attempts and last error. Exponential backoff, and a cap after which it is marked failed rather than retried forever. Files: `lib/sync/outbox.dart` 3. Show sync state in the interface Per record: synced, pending or failed. Not a global spinner, which tells the user nothing about their record. Files: `lib/widgets/sync_badge.dart` ### Done when - [ ] Records created offline survive an application restart - [ ] Reconnecting submits each queued record exactly once - [ ] A permanently failing record is marked failed with its error - [ ] Pending state is visible per record, not only globally ### Watch out - An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. ## Not in this build - The visual editor and the collaboration it enables for non-developers. - The prebuilt integration catalogue. - Managed builds and store submission handling. - The design polish that comes from a canvas and a component library. ## After v1, if you want it - Push notifications, which need platform credentials and a delivery service - Conflict resolution beyond last write wins, once two people edit the same record - A web build from the same codebase, which Flutter supports and which is nearly free here
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your project settings. Public by design. SUPABASE_URL=https://yourproject.supabase.co # Required. Project API settings. Safe in the client only because row level security is doing the work. SUPABASE_ANON_KEY=eyJhbGciOi... # Optional · secret. Product phase only. Never committed. Back it up in two separate places. ANDROID_KEYSTORE_PATH=./keys/upload.jks # Optional. The alias used when the keystore was created. ANDROID_KEY_ALIAS=upload # Optional · secret. Kept in a password manager, never in the repository. ANDROID_KEY_PASSWORD=replace-with-your-password # Required. How many times a queued write retries before being marked failed and shown to the user. SYNC_MAX_ATTEMPTS=8
# FlutterFlow · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For FlutterFlow, assemble a mobile interface from a schema and export maintainable Flutter code. The hard boundary is visual editor depth, native builds, integrations, collaboration, and code generation, plus connectors, runtime reliability, and governance. ## Product outcome A Flutter application generated from a schema you own, with access control enforced on the server, offline behaviour that never silently loses an entry, and signed builds reaching real testers with the keystore backed up. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Docker - PostgreSQL - self-hosted deploy target - API credentials for one selected integration ## Explicit non-goals for v1 - The visual editor and the collaboration it enables for non-developers. - The prebuilt integration catalogue. - Managed builds and store submission handling. - The design polish that comes from a canvas and a component library. - visual editor depth, native builds, integrations, collaboration, and code generation - hundreds of connectors - pixel-perfect app design - mobile app-store builds - enterprise permissions and scale ## Success criteria - A direct API call with another user's token is refused - Offline creation and reconnection is verified to submit exactly once - The Android keystore is confirmed present in two backup locations - A signed build installs on a device you do not own - Regenerating models is confirmed not to overwrite refined screens
# Build brief · FlutterFlow The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me the mobile application you were going to assemble visually, written directly in Flutter to replace FlutterFlow. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Do not build a visual app builder. FlutterFlow is a platform and reproducing it is years of work for an audience of one. Write the single application you wanted, in Flutter, generating the repetitive parts from your schema with code generation rather than with a canvas. Decide the distribution question before Phase 1, because it constrains everything: iOS builds require a Mac and a paid Apple account, and losing an Android signing key means you can never update that application listing again. ### Stack (fixed, do not substitute) - Framework: Flutter with Dart, on the current stable channel · one codebase for both platforms, and the code stays readable, which is the point of leaving a visual builder - Backend: Supabase, self-hosted or managed · PostgreSQL with authentication and a generated API, which is the closest thing to what the visual builder was providing - Generation: Dart code generation from a schema file, run as a build step · the repetitive model and form code is generated, and the parts worth writing by hand stay handwritten - Local cache: SQLite through Drift · typed queries and migrations, and a cache that is queryable rather than a blob of JSON - State: Riverpod · explicit, testable, and it does not require the widget tree to be the source of truth ### Data model (create this before Phase 1) - The worked example is a field inspection application. Replace these with your own entities in Phase 2 and keep the shape. - schema.yaml · your entities, fields, types and relations, the single input to code generation - sites(id, name, address, lat, lng, created_at) - inspections(id, site_id, inspector_id, state, started_at, submitted_at, notes) - findings(id, inspection_id, severity, description, photo_path, recorded_at) - sync_queue(id, entity, entity_id, op, payload_json, attempts, last_error) · the outbox that makes offline work ### Phase 1 · Toolchain, and the distribution decision Build: Install Flutter and confirm the doctor is clean for your targets. Run the starter application on a physical Android device and, if you are shipping to iOS, on a real iPhone. Doing this first surfaces the platform obstacles while they are still cheap. Trap: Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. Done when: flutter doctor reports no issues for your intended targets. The starter application runs on a physical Android device. If iOS is a target, it also runs on a physical iPhone, which proves the signing chain works before any real code exists. Do not build yet: No schema, no backend, no screens. ### Phase 2 · Schema and generated models Build: Define entities, fields, types and relations in a schema file. Generate Dart model classes with serialisation and the Drift table definitions from it. Generate the database migration too, so the local schema cannot drift from the models. Trap: Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. Done when: Adding a field to the schema and regenerating produces an updated model, an updated table definition and a migration, with no manual editing. Generated files are marked as generated and are ignored by review. The application still builds after regeneration. Do not build yet: No backend and no screens yet. ### Phase 3 · Authentication and the data source Build: Connect to the backend, implement sign-in, sign-out and token refresh, and enforce access with row level security on the server rather than with filters in the client. Handle the expired session case explicitly. Trap: Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. Done when: A user signs in, sees only their own records, and is signed out cleanly. Querying the API directly with user A's token cannot retrieve user B's rows, verified with a request tool rather than through the application. An expired token refreshes silently, and a revoked one returns the user to sign-in rather than showing an empty screen. Do not build yet: No offline cache and no screens beyond a list yet. ### Phase 4 · Generated screens Build: Generate a list, a detail and a form screen per entity from the schema, with validation derived from field types and nullability. Generate once into editable files rather than regenerating over your refinements. Trap: Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. Done when: Every entity has working list, detail and edit screens reachable from the application. Validation rejects a null in a non-nullable field before submission. Refining a generated screen and regenerating models does not overwrite the refinement. Do not build yet: No offline behaviour yet. ### Phase 5 · Working offline Build: Read from the local database and treat the network as a synchroniser rather than a source. Queue writes in an outbox with retries. Show sync state honestly, including which records are pending and which failed. Trap: An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. Done when: With the network disabled, records can be created and read, and they survive an application restart. Re-enabling the network drains the queue and each record is submitted exactly once. A permanently failing record shows as failed with its error rather than retrying forever in silence. Do not build yet: Distribution is the last phase. ### Phase 6 · Signing, builds and testers Build: Create signing keys for both platforms, back up the Android keystore somewhere you will still have it in five years, and distribute builds through the platform testing channels. Automate the build so a release is not a memory exercise. Trap: Losing the Android upload keystore means you can never update that listing again under the same identifier. Back it up in at least two places outside the build machine before your first release, and record where. This is not recoverable by support in the general case. Done when: A signed Android build installs on a tester's device through the testing channel. If iOS is a target, a build reaches testers through the platform's testing distribution. The Android keystore exists in two backed-up locations and the locations are recorded. A release runs from one documented command. Do not build yet: Nothing after this. ### Out of scope (and why) - The visual editor, which is the product and is why teams pay for it. - The hundreds of prebuilt integrations, each of which is a connector someone maintains. - Collaborative editing of the application by non-developers, which is the capability you lose by moving to code. - The managed build and deploy pipeline, which now costs you the signing, the certificates and the store submissions. ### README must contain - Which platforms this ships to, and that iOS requires a Mac and a paid account - That the schema file is the input and which directories are generated, never hand-edited - That access control lives in row level security policies, and how that was verified from outside the app - How offline queuing works and what the per-record sync states mean - Where the Android keystore backups are, because losing it ends the ability to update
# Architecture · FlutterFlow ## Stack | Part | Choice | Why | | --- | --- | --- | | Framework | Flutter with Dart, on the current stable channel | one codebase for both platforms, and the code stays readable, which is the point of leaving a visual builder | | Backend | Supabase, self-hosted or managed | PostgreSQL with authentication and a generated API, which is the closest thing to what the visual builder was providing | | Generation | Dart code generation from a schema file, run as a build step | the repetitive model and form code is generated, and the parts worth writing by hand stay handwritten | | Local cache | SQLite through Drift | typed queries and migrations, and a cache that is queryable rather than a blob of JSON | | State | Riverpod | explicit, testable, and it does not require the widget tree to be the source of truth | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Schema | The single definition of entities and fields. | This is the input to everything generated. Replacing the generator is fine; replacing the single-source principle is not. | | Generation | Models, tables and migrations, continuously; screens, once. | The split between continuous and one-off generation is the important decision. Keep judgement out of continuously regenerated files. | | Auth and policies | Identity and row level access control. | A different backend changes the policy syntax, not the principle that the server enforces access. | | Repository | Reading locally and synchronising remotely. | The rule that the interface reads from the local database is what makes offline work. Any swap must preserve it. | | Outbox | Queued writes, retries and per-record state. | Implementation is replaceable; the visible pending and failed states are not. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `SUPABASE_URL` · required · Your project settings. Public by design. - `SUPABASE_ANON_KEY` · required · Project API settings. Safe in the client only because row level security is doing the work. - `ANDROID_KEYSTORE_PATH` · optional, secret · Product phase only. Never committed. Back it up in two separate places. - `ANDROID_KEY_ALIAS` · optional · The alias used when the keystore was created. - `ANDROID_KEY_PASSWORD` · optional, secret · Kept in a password manager, never in the repository. - `SYNC_MAX_ATTEMPTS` · required · How many times a queued write retries before being marked failed and shown to the user. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · FlutterFlow product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Flutter with Dart, on the current stable channel, Supabase, self-hosted or managed, Dart code generation from a schema file, run as a build step, SQLite through Drift, Riverpod. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. - Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. - Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. - Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. - An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. - Losing the Android upload keystore means you can never update that listing again under the same identifier. Back it up in at least two places outside the build machine before your first release, and record where. This is not recoverable by support in the general case.
# Delivery milestones · FlutterFlow Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Toolchain, and the distribution decision A Flutter application runs on a real device on each platform you intend to ship to. ### Steps 1. Install Flutter and run the doctor Install the SDK, then resolve every doctor warning for your targets. Ignoring them now means debugging them later with more code in the way. ```sh flutter doctor -v flutter create fieldapp ``` 2. Run on a physical Android device Emulators hide performance and permission behaviour. Use a real device from the first day. ```sh flutter devices flutter run -d <device-id> ``` 3. If iOS is a target, run on a real iPhone This requires Xcode, a signing identity and a provisioning profile. It is the single most likely thing to block you, so do it before writing code. ### Done when - [ ] flutter doctor reports no issues for the intended targets - [ ] The starter application runs on a physical Android device - [ ] If iOS is a target, it runs on a physical iPhone - [ ] The decision about which platforms you ship to is written down ### Watch out - Building for iOS requires macOS and Xcode, and distributing to testers requires a paid Apple Developer account before you can install on a device you do not own. If you do not have a Mac, decide now whether this is an Android-only application, because discovering it in Phase 6 wastes the work. ## M2 · Schema and generated models One schema file produces your data models, and changing it regenerates them. ### Steps 1. Write the schema file Entities with fields, types, nullability and relations. This file is the thing you maintain; everything else follows from it. Files: `schema.yaml` 2. Generate models and tables A build step producing serialisable model classes and Drift table definitions. Run it as part of the build so it cannot be forgotten. Files: `lib/generated/` ```sh dart run build_runner build --delete-conflicting-outputs ``` 3. Generate migrations too A schema change must produce a migration. Without it, the local database and the models diverge and the failure appears as mysterious query errors. Files: `lib/db/migrations/` ### Done when - [ ] Adding a schema field regenerates model, table and migration - [ ] Generated files are marked and never hand-edited - [ ] The application builds after regeneration - [ ] A migration applied to an existing database preserves its rows ### Watch out - Hand-editing generated files is the habit that destroys the value of generation, because the next run silently discards the edit. Keep generated files clearly marked, never edit them, and put anything custom in a separate file that extends rather than modifies. ## M3 · Authentication and the data source A real person can sign in, and the data they see is the data they are allowed to see. ### Steps 1. Wire authentication Sign in, sign out, and token refresh. Store the token in secure storage, not in shared preferences. Files: `lib/auth/` 2. Write row level security policies One policy per table, on the server. This is the actual access control and everything else is presentation. Files: `supabase/policies.sql` 3. Verify the boundary from outside the app Use a request tool with a second user's token and confirm the refusal. Testing through your own client proves nothing. ### Done when - [ ] A user sees only their own rows - [ ] A direct API call with another user's token is refused - [ ] An expired token refreshes without user action - [ ] A revoked session returns to sign-in rather than an empty screen ### Watch out - Filtering by user in the client is not access control; anyone can call the API directly with the same token. Put the rule in a row level security policy, then verify by querying the API with a second user's token and confirming the refusal. ## M4 · Generated screens Each entity has list, detail and edit screens, generated from the schema and then refined by hand. ### Steps 1. Generate screen scaffolds once A one-off generator writing into the normal source tree, not into the generated directory. Different rules for different kinds of code. Files: `tool/gen_screens.dart` 2. Derive validation from the schema Required, type and length come from the field definitions, so validation cannot drift from the database. Files: `lib/forms/` 3. Refine by hand and prove regeneration is safe Change a generated screen, regenerate models, and confirm the change survives. ### Done when - [ ] Every entity has list, detail and edit screens - [ ] Validation matches the schema's nullability and types - [ ] Regenerating models does not overwrite refined screens - [ ] A form submission failure shows an error rather than losing the input ### Watch out - Screens are where design judgement lives, so continuously regenerating them destroys every refinement you make. Generate screens once as a scaffold you then own, and keep continuous regeneration for models and migrations where there is no judgement to lose. ## M5 · Working offline The application is usable with no signal, and nothing entered is lost when the network returns. ### Steps 1. Read from the local database always The network fills the cache; the interface never waits on it. This inverts the usual structure and is the whole of offline support. Files: `lib/repository/` 2. Add an outbox with retries and a cap Queue writes with attempts and last error. Exponential backoff, and a cap after which it is marked failed rather than retried forever. Files: `lib/sync/outbox.dart` 3. Show sync state in the interface Per record: synced, pending or failed. Not a global spinner, which tells the user nothing about their record. Files: `lib/widgets/sync_badge.dart` ### Done when - [ ] Records created offline survive an application restart - [ ] Reconnecting submits each queued record exactly once - [ ] A permanently failing record is marked failed with its error - [ ] Pending state is visible per record, not only globally ### Watch out - An optimistic write with no visible pending state makes the user believe the data is safe when it exists only on their phone. Show pending and failed states plainly, because the moment that matters is when someone closes the application believing their work was submitted. ## M6 · Signing, builds and testers (production only) Real testers install it on their own phones, and you can still ship an update in a year. ### Steps 1. Create and back up signing keys Generate the Android keystore, then copy it to two separate durable locations before you use it. Record where they are. Files: `docs/release.md` ```sh keytool -genkey -v -keystore upload.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload ``` 2. Automate the build One script producing signed artefacts for each target, so a release does not depend on remembering flags. Files: `scripts/release.sh` ```sh flutter build appbundle --release ``` 3. Distribute to real testers Use the platform testing channels. Installing on a device you do not own is the test that signing is genuinely correct. ### Done when - [ ] A signed build installs on a tester's device - [ ] The Android keystore is backed up in two places and the locations are recorded - [ ] A release runs from one documented command - [ ] If iOS is a target, a build reaches testers through its testing channel ### Watch out - Losing the Android upload keystore means you can never update that listing again under the same identifier. Back it up in at least two places outside the build machine before your first release, and record where. This is not recoverable by support in the general case.
# Operations · FlutterFlow ## Backup The backend database is the durable data and needs its own nightly dump off the machine. The Android keystore is the other irreplaceable artefact and must exist in two separate durable locations. ## Restore Restore the database, redeploy, and sign in. For the keystore there is no restore other than your backup, which is why it is called out separately from ordinary data. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Backend uptime, and the proportion of records sitting in outboxes across your users if you report it. A rising failed count usually means a policy change broke writes for a subset of users. ## Incident checklist For a bad schema migration, restore the pre-migration dump rather than writing a reverse migration under pressure. For a signing failure at release time, stop and verify the keystore backup before doing anything else. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A direct API call with another user's token is refused - [ ] Offline creation and reconnection is verified to submit exactly once - [ ] The Android keystore is confirmed present in two backup locations - [ ] A signed build installs on a device you do not own - [ ] Regenerating models is confirmed not to overwrite refined screens ## Launch constraint Do not market omitted FlutterFlow capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Your project settings. Public by design. SUPABASE_URL=https://yourproject.supabase.co # Required. Project API settings. Safe in the client only because row level security is doing the work. SUPABASE_ANON_KEY=eyJhbGciOi... # Optional · secret. Product phase only. Never committed. Back it up in two separate places. ANDROID_KEYSTORE_PATH=./keys/upload.jks # Optional. The alias used when the keystore was created. ANDROID_KEY_ALIAS=upload # Optional · secret. Kept in a password manager, never in the repository. ANDROID_KEY_PASSWORD=replace-with-your-password # Required. How many times a queued write retries before being marked failed and shown to the user. SYNC_MAX_ATTEMPTS=8
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for FlutterFlow because the builder is only useful when its runtime, connectors, permissions, and deployments remain boring for years. The recurring cost buys schema migration, auth, permissions, connector drift, queues, deployments, backups, and auditability, not just the visible interface.
xvisual editor depth, native builds, integrations, collaboration, and code generation
xhundreds of connectors
xpixel-perfect app design
xmobile app-store builds
xenterprise permissions and scale
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
FlutterFlow pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | 2 projects; 10 Lite credits; 5 AI requests lifetime; 2 API endpoints; 1 editor; 1 branch; 2 subdomains |
| basic | $39/user | — | Unlimited projects; 100 Lite credits/month; 50 AI requests/month; 1 editor; 1 agent; 1 custom domain |
| growth | $80/user | — | First editor seat: 200 Lite credits and 200 AI requests/seat/month; 2 open branches; up to 4 paid collaborators |
| business | $150/user | — | First editor seat: 400 Lite credits and 500 AI requests/seat/month; up to 5 editors; 5 branches; 3 automated tests |
| enterprise | custom | — | Custom seats, deployment, security, support and contract terms |
free tier2 projects; 10 Lite credits; 5 AI requests lifetime; 2 API endpoints; 1 editor; 1 branch; 2 subdomains
billingmonthly + annual (advertised savings up to about 25%); exact annual USD values were not exposed reliably
hidden costsGrowth and Business add editor-seat charges after the first seat, paid collaborators and extra custom domains can cost more, and generated apps still require external backend, hosting and app-store accounts where applicable
verified 2026-08-14 · source ↗
Vibecode FlutterFlow
Kinda. The core of FlutterFlow is buildable in a weekend with the prompt on this page, but there are real gaps: visual editor depth, native builds, integrations, collaboration, and code generation, hundreds of connectors. Read the honest list above before committing.
How much does FlutterFlow cost?
FlutterFlow costs about $39/month (Basic, checked 2026-08-14), which is $468 per year.
What do I lose by replacing FlutterFlow?
Honestly: visual editor depth, native builds, integrations, collaboration, and code generation; hundreds of connectors; pixel-perfect app design; mobile app-store builds; enterprise permissions and scale. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to FlutterFlow?
Yes: Nowa Starter (A visual Flutter builder with editable standard code and unlimited free source downloads; publishing-as-a-service is paid.) The prompt is for when you want it exactly your way.