# Add a backend to an Expo app with one command

> Give an Expo app Postgres, auth, storage and realtime with `npx scribase init`: EXPO_PUBLIC_ variables, lib/supabase.ts added. No service key lands in the app.

Source: https://scribase.com/frameworks/expo

One command gives an Expo app a real backend: database, sign-in, file storage and realtime, with the public keys in .env and a client ready to import. The service role key is never written, because anything in an Expo env file can end up inside the app binary.

## `npx scribase init` output

```
$ npx scribase init
Backend ready: https://acme-my-app-production-c2b0ea0a.scribase.com
  framework   expo
  project     acme/my-app (production)
  env file    .env (EXPO_PUBLIC_SUPABASE_URL, EXPO_PUBLIC_SUPABASE_ANON_KEY, SCRIBASE_API_URL, SCRIBASE_ORG, SCRIBASE_PROJECT, SCRIBASE_ENV)
  client      lib/supabase.ts

Next:
  - import { supabase } from './lib/supabase' and query: await supabase.from('todos').select()
  - create tables with SQL migrations (supabase/migrations/*.sql) and `scribase db push`, or the MCP server
```

## Environment file: .env

- EXPO_PUBLIC_SUPABASE_URL
- EXPO_PUBLIC_SUPABASE_ANON_KEY

## Run init

Run it in the project root. If you are not signed in, it prints a link and a code: approve it in the browser (sign up takes one click plus the $1 card check, credited back), and init carries on. No key is ever pasted. It creates the project, writes the env file, adds the env file to .gitignore, installs @supabase/supabase-js and writes a small client module.

```bash
npx scribase init
```

## What it wrote

Expo inlines EXPO_PUBLIC_ variables at build time. Only the project URL and the publishable key are written; both are meant to ship in the app, and row-level security decides what they can read.

_.env_
```bash
# Scribase (written by `npx scribase init`)
EXPO_PUBLIC_SUPABASE_URL=https://acme-my-app-production-c2b0ea0a.scribase.com
EXPO_PUBLIC_SUPABASE_ANON_KEY=<publishable key>
SCRIBASE_API_URL=https://api.scribase.com
SCRIBASE_ORG=acme
SCRIBASE_PROJECT=my-app
SCRIBASE_ENV=production
```

## Keep users signed in

The generated client works as is. To keep sessions across app restarts, pass AsyncStorage, as the comment in the file suggests.

_lib/supabase.ts_
```ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { createClient } from '@supabase/supabase-js';

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 } },
);
```

## Create your first table

Open your project in the console, go to Editor, then SQL, and run this. Or ask your coding agent: with the Scribase MCP server connected it applies the same SQL as a migration. Row-level security is on, so the public key can only read what the policy allows.

```sql
create table public.todos (
  id bigint generated always as identity primary key,
  title text not null,
  done boolean not null default false,
  created_at timestamptz not null default now()
);

alter table public.todos enable row level security;

create policy "todos are readable" on public.todos
  for select to anon, authenticated using (true);

insert into public.todos (title) values ('Ship the backend'), ('Tell a friend');
```

## Show it on screen

Restart the dev server with `npx expo start --clear` so the new variables are inlined.

_app/index.tsx_
```tsx
import { useEffect, useState } from 'react';
import { FlatList, Text } from 'react-native';
import { supabase } from '../lib/supabase';

type Todo = { id: number; title: string };

export default function Home() {
  const [todos, setTodos] = useState<Todo[]>([]);
  useEffect(() => {
    supabase.from('todos').select('id, title').then(({ data }) => setTodos(data ?? []));
  }, []);
  return <FlatList data={todos} keyExtractor={(t) => String(t.id)} renderItem={({ item }) => <Text>{item.title}</Text>} />;
}
```

## Agent prompt

```
Add a Scribase backend to this Expo app.
1. Run `npx scribase init --yes --json` in the project root. If it returns authorization_pending, show me the link and code, wait for me to approve, then run it again.
2. Create a `todos` table (id, title, done, created_at) with row-level security on and a policy that lets anyone read it; use the Scribase MCP server or a SQL migration, not the service-role key in app code.
3. Show the todos in a FlatList on the first screen, and make the client persist sessions with AsyncStorage.
4. Never print, log or commit the env file; it is already in .gitignore.
```

## FAQ

**Why is there no service role key in .env?**

Expo can bundle any env value into the app. init never writes the service role key for Expo, so it cannot leak into a store build.

**Does it work with EAS Build?**

Yes. Set the two EXPO_PUBLIC_ variables as EAS environment variables (or keep .env in the build) and they are inlined at build time.

**Can I sign users in with Apple or Google?**

Yes. Auth speaks the supabase-js API, including signInWithIdToken for native Apple and Google sign-in.