Quick path
Create a Supabase project → define tables matching the AI-generated mock data shape → install @supabase/supabase-js → swap mocks for supabase.from('table').select() → add RLS policies → add auth via Clerk or Supabase Auth. 1–2 hours end to end.
Three backend shapes
- BaaS (Backend-as-a-Service): Supabase, Firebase. Auth, database, storage, realtime in one product. Simplest choice for most mobile apps.
- API routes: Expo Router API routes on EAS Hosting, or Next.js routes on Vercel. Good when you need custom server logic or to integrate multiple third-parties.
- Custom server: Node, Go, Rails, Python on a VPS or container host. Most flexibility, most maintenance. Pick if you have existing infrastructure.
For a first-time AI-generated app, BaaS is the correct default. Specifically Supabase unless you have a reason otherwise.
Step 1: inspect what the AI generated
Before wiring, understand what you have. Open the generated project and look for:
- Files named
mockData.ts,seed.ts, or hardcoded arrays in components. - Imports like
@supabase/supabase-js— ShipNative ships a wired Supabase layer if you asked for it. - Auth files —
useAuth,ClerkProvider, or similar. - An
.env.examplelisting the environment variables you need.
Step 2: create the Supabase project
- Go to supabase.com → New project. Pick region closest to users.
- Copy the Project URL and anon key. Set as
EXPO_PUBLIC_SUPABASE_URLandEXPO_PUBLIC_SUPABASE_ANON_KEYin your.env. - Open the SQL editor. Create tables matching your mock data shape.
- Enable row-level security on every table:
alter table X enable row level security; - Add policies for who can read/write each row (scoped by
auth.uid()or workspace_id).
Step 3: wire the Supabase client
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
import AsyncStorage from
'@react-native-async-storage/async-storage';
export const supabase = createClient(
process.env.EXPO_PUBLIC_SUPABASE_URL!,
process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!,
{
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
}
);Step 4: swap mocks for real queries
Go screen by screen. Before:
const tasks = [
{ id: '1', title: 'Buy milk', done: false },
{ id: '2', title: 'Call mom', done: false },
];After:
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/lib/supabase';
const { data: tasks } = useQuery({
queryKey: ['tasks'],
queryFn: async () => {
const { data } = await supabase
.from('tasks')
.select()
.order('created_at', { ascending: false });
return data;
},
});Step 5: add authentication
Two patterns:
- Supabase Auth — simplest when you only need email, OAuth, or magic links. One less vendor.
- Clerk + Supabase — Clerk handles user identity and UI; Supabase handles data. Pair them via Clerk’s JWT template pointed at Supabase. More expensive, much better UX.
Full comparison in React Native Authentication 2026.
Step 6: row-level security (RLS)
RLS is what keeps users from seeing each other’s data. Without it, your anon key in the client lets anyone read everything. Example policy:
alter table tasks enable row level security; create policy "users see their own tasks" on tasks for select using (auth.uid() = user_id); create policy "users insert their own tasks" on tasks for insert with check (auth.uid() = user_id); create policy "users update their own tasks" on tasks for update using (auth.uid() = user_id); create policy "users delete their own tasks" on tasks for delete using (auth.uid() = user_id);
Apply this pattern to every user-scoped table. Test by logging in as two different users and confirming each only sees their own data.
Step 7: testing the integration
- Create two test accounts. Verify they cannot see each other’s data.
- Try to write to a table the policy shouldn’t allow — should fail.
- Check the Supabase logs for failed requests — useful for debugging RLS.
- Test offline by toggling airplane mode. Confirm errors fail gracefully.
- Verify environment variables load in both dev and production builds.
Common pitfalls
- Forgetting to enable RLS. By default, Supabase tables are locked down — but if you disabled it during dev and never re-enabled, you have a data leak.
- Putting the service_role key in the client. Never. Only use it from Edge Functions or server.
- Mixing Clerk user IDs and Supabase auth.uid() without a JWT template — policies silently fail.
- Not testing as a different user. Your own account works; a second user reveals the bugs.
- Syncing every action individually. Batch writes when possible; it saves cost and latency.
Skip the wiring entirely
Apps generated via ShipNative with “Supabase backend + RLS” in the prompt arrive with the client wired, tables named, and baseline policies generated. You still set up your Supabase project and run the SQL, but the JavaScript plumbing is done. See Generate App from Description for how the pipeline works.