<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ahmad Mukhtiar</title>
    <description>The latest articles on DEV Community by Ahmad Mukhtiar (@ahmadmukhtiar).</description>
    <link>https://dev.to/ahmadmukhtiar</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4143669%2F476201fd-7e48-455e-9a0d-09d39264f5a9.jpeg</url>
      <title>DEV Community: Ahmad Mukhtiar</title>
      <link>https://dev.to/ahmadmukhtiar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahmadmukhtiar"/>
    <language>en</language>
    <item>
      <title>I taught our AI app builder to set up Supabase by itself</title>
      <dc:creator>Ahmad Mukhtiar</dc:creator>
      <pubDate>Sat, 26 Sep 2026 01:50:34 +0000</pubDate>
      <link>https://dev.to/ahmadmukhtiar/i-taught-our-ai-app-builder-to-set-up-supabase-by-itself-2ml4</link>
      <guid>https://dev.to/ahmadmukhtiar/i-taught-our-ai-app-builder-to-set-up-supabase-by-itself-2ml4</guid>
      <description>&lt;p&gt;&lt;em&gt;Sign-in, tables and row-level security for a Flutter app — from one prompt, and what we learned about doing it safely&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;em&gt;won't&lt;/em&gt; do, and the patterns you can copy even if you never use our tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test: one prompt, a working backend
&lt;/h2&gt;

&lt;p&gt;I created a brand-new project and typed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In one turn, the agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Created (or linked) a Supabase project and saved the &lt;strong&gt;project URL + publishable key&lt;/strong&gt; into the app — never the service-role key.&lt;/li&gt;
&lt;li&gt;Turned on email + password sign-in, disabled confirmation emails &lt;em&gt;for testing&lt;/em&gt;, and added the app's deep link (&lt;code&gt;com.yourapp://login-callback&lt;/code&gt;) as an allowed redirect.&lt;/li&gt;
&lt;li&gt;Wrote a migration file for a &lt;code&gt;notes&lt;/code&gt; table with row-level security, applied it, and ran Supabase's security advisors.&lt;/li&gt;
&lt;li&gt;Rebuilt the preview — which opened on a real sign-in screen, not a "connect your backend" placeholder.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No dashboard tabs, no copy-pasting keys into chat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: the client only ever gets the publishable key
&lt;/h2&gt;

&lt;p&gt;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 &lt;strong&gt;publishable (anon) key&lt;/strong&gt;. Row-level security is what actually protects the data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_dotenv/flutter_dotenv.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:supabase_flutter/supabase_flutter.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;WidgetsFlutterBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ensureInitialized&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;fileName:&lt;/span&gt; &lt;span class="s"&gt;'.env'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Supabase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;url:&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'SUPABASE_URL'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;anonKey:&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'SUPABASE_ANON_KEY'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;NotesApp&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: RLS policies for signed-in users, with a sub-select
&lt;/h2&gt;

&lt;p&gt;This is the migration the agent wrote. Two details matter more than they look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;references&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;cascade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;alter&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="n"&gt;enable&lt;/span&gt; &lt;span class="k"&gt;row&lt;/span&gt; &lt;span class="k"&gt;level&lt;/span&gt; &lt;span class="k"&gt;security&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;drop&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="nv"&gt;"Users can read their own notes"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can read their own notes"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
  &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;drop&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="nv"&gt;"Users can insert their own notes"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="nv"&gt;"Users can insert their own notes"&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;insert&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt;
  &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="k"&gt;check&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uid&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- update: using + with check; delete: using — same expression&lt;/span&gt;
&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;index&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;exists&lt;/span&gt; &lt;span class="n"&gt;notes_user_id_idx&lt;/span&gt; &lt;span class="k"&gt;on&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;to authenticated&lt;/code&gt;&lt;/strong&gt; — the policy only applies to signed-in users. Anonymous requests never even get evaluated against it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;(select auth.uid())&lt;/code&gt;&lt;/strong&gt; instead of &lt;code&gt;auth.uid()&lt;/code&gt; — 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;default auth.uid()&lt;/code&gt;&lt;/strong&gt; on &lt;code&gt;user_id&lt;/code&gt; means the app doesn't have to send it on insert, so the client can't lie about who owns a row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;drop policy if exists&lt;/code&gt; + &lt;code&gt;create&lt;/code&gt;&lt;/strong&gt; makes the migration safe to run twice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The file is saved to &lt;code&gt;supabase/migrations/&lt;/code&gt; in the project, so it's reviewable and reproducible — not a one-off click in a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: auth settings that work on a phone
&lt;/h2&gt;

&lt;p&gt;Every new Supabase project starts with a site URL of &lt;code&gt;http://localhost:3000&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;So the setup step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;adds &lt;code&gt;&amp;lt;your app id&amp;gt;://login-callback&lt;/code&gt; to the redirect allow-list (merged, never replacing what's there),&lt;/li&gt;
&lt;li&gt;points the site URL at that deep link until the app has a real domain,&lt;/li&gt;
&lt;li&gt;turns email confirmation &lt;strong&gt;off while testing&lt;/strong&gt;, so new accounts can sign in immediately — and makes it one flag to turn back on before launch.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the agent deliberately won't do
&lt;/h2&gt;

&lt;p&gt;Automation is only useful if you can trust it. A few hard lines we drew:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It never sees an API key.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It won't silently create projects.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It won't move a live app to another database.&lt;/strong&gt; Switching projects is a deliberate step in the settings panel, not something an agent decides mid-conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It won't pretend.&lt;/strong&gt; Leaked-password protection is a Supabase Pro feature; on the free plan the agent says so instead of reporting a fake "fixed".&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What still needs a human
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the pattern
&lt;/h2&gt;

&lt;p&gt;Even if you wire Supabase by hand, steal these three things: publishable key only in the client, &lt;code&gt;to authenticated&lt;/code&gt; + &lt;code&gt;(select auth.uid())&lt;/code&gt; policies saved as migrations, and deep-link redirects instead of &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And if you'd rather describe the app and have all of this done for you, that's exactly what we're building at &lt;a href="https://fluttergo.ai" rel="noopener noreferrer"&gt;FlutterGo.AI&lt;/a&gt;. I'd love feedback from Flutter and Supabase folks — what would you want an agent to never do with your backend?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;— Ahmad Mukhtiar, founder of FlutterGo.AI · Flutter &amp;amp; Node.js developer&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>supabase</category>
      <category>ai</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
