Sign-in, tables and row-level security for a Flutter app — from one prompt, and what we learned about doing it safely
Most AI app builders stop at the screens. You get a nice login page and a list view, and then the hard part starts: create a backend project, copy keys, write tables, figure out row-level security, fix redirect URLs, and hope nothing leaks. That gap is where "it looked great in the demo" apps go to die.
I'm building FlutterGo.AI, an AI app builder that outputs real Flutter code for iOS, Android and web (so I'm biased). This week we closed that gap for Supabase. Here's how it works, what it does on its own, what it deliberately won't do, and the patterns you can copy even if you never use our tool.
The test: one prompt, a working backend
I created a brand-new project and typed:
Build a small one-screen notes app with Supabase: email sign up / sign in / sign out, and a notes list where each user only sees their own notes. Use a new Supabase project.
In one turn, the agent:
- Created (or linked) a Supabase project and saved the project URL + publishable key into the app — never the service-role key.
- Turned on email + password sign-in, disabled confirmation emails for testing, and added the app's deep link (
com.yourapp://login-callback) as an allowed redirect. - Wrote a migration file for a
notestable with row-level security, applied it, and ran Supabase's security advisors. - Rebuilt the preview — which opened on a real sign-in screen, not a "connect your backend" placeholder.
No dashboard tabs, no copy-pasting keys into chat.
Pattern 1: the client only ever gets the publishable key
Everything in a Flutter app ships to the user's device. So the only Supabase values that belong in the app are the project URL and the publishable (anon) key. Row-level security is what actually protects the data.
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: '.env');
await Supabase.initialize(
url: dotenv.env['SUPABASE_URL']!,
anonKey: dotenv.env['SUPABASE_ANON_KEY']!,
);
runApp(const NotesApp());
}
Server-only secrets (service-role key, Stripe secret key, database URL) live in a separate file that is never bundled, and are only used by server code like an Edge Function.
Pattern 2: RLS policies for signed-in users, with a sub-select
This is the migration the agent wrote. Two details matter more than they look:
create table if not exists public.notes (
id uuid primary key default gen_random_uuid(),
user_id uuid not null default auth.uid() references auth.users on delete cascade,
content text not null,
created_at timestamptz not null default now()
);
alter table public.notes enable row level security;
drop policy if exists "Users can read their own notes" on public.notes;
create policy "Users can read their own notes" on public.notes
for select to authenticated
using ((select auth.uid()) = user_id);
drop policy if exists "Users can insert their own notes" on public.notes;
create policy "Users can insert their own notes" on public.notes
for insert to authenticated
with check ((select auth.uid()) = user_id);
-- update: using + with check; delete: using — same expression
create index if not exists notes_user_id_idx on public.notes (user_id);
-
to authenticated— the policy only applies to signed-in users. Anonymous requests never even get evaluated against it. -
(select auth.uid())instead ofauth.uid()— Postgres evaluates it once per query instead of once per row. On big tables that's a real performance difference, and it's what Supabase recommends. -
default auth.uid()onuser_idmeans the app doesn't have to send it on insert, so the client can't lie about who owns a row. -
drop policy if exists+createmakes the migration safe to run twice.
The file is saved to supabase/migrations/ in the project, so it's reviewable and reproducible — not a one-off click in a dashboard.
Pattern 3: auth settings that work on a phone
Every new Supabase project starts with a site URL of http://localhost:3000. That's fine for a web app on your laptop, and silently wrong for a mobile app: confirmation and password-reset emails point at a page that doesn't exist on the user's phone.
So the setup step:
- adds
<your app id>://login-callbackto the redirect allow-list (merged, never replacing what's there), - points the site URL at that deep link until the app has a real domain,
- turns email confirmation off while testing, so new accounts can sign in immediately — and makes it one flag to turn back on before launch.
What the agent deliberately won't do
Automation is only useful if you can trust it. A few hard lines we drew:
- It never sees an API key. It works through the owner's Supabase connection with scoped tools; SQL that tries to read tokens, sessions, password hashes or vault secrets is refused.
- It won't silently create projects. A new Supabase project can count against your plan, so by default the agent offers a one-click "Create Supabase project" button. Owners can opt in to let it create projects on its own.
- It won't move a live app to another database. Switching projects is a deliberate step in the settings panel, not something an agent decides mid-conversation.
- It won't pretend. Leaked-password protection is a Supabase Pro feature; on the free plan the agent says so instead of reporting a fake "fixed".
What still needs a human
Honest list: you should still test sign-up and sign-in yourself (we give you a checklist in the preview), decide when to turn email confirmation back on, and review the migration before production. The agent gets you to a correct, secure starting point in minutes — it doesn't replace owning your data model.
Try the pattern
Even if you wire Supabase by hand, steal these three things: publishable key only in the client, to authenticated + (select auth.uid()) policies saved as migrations, and deep-link redirects instead of localhost.
And if you'd rather describe the app and have all of this done for you, that's exactly what we're building at FlutterGo.AI. I'd love feedback from Flutter and Supabase folks — what would you want an agent to never do with your backend?
— Ahmad Mukhtiar, founder of FlutterGo.AI · Flutter & Node.js developer
Top comments (0)