# Add a backend to Remix or React Router with one command

> Give a Remix or React Router v7 app Postgres, auth, storage and realtime with `npx scribase init`: VITE_ variables written, a client ready for loaders.

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

Remix and React Router v7 build with Vite, so init applies the Vite conventions: VITE_ variables in .env.local and a client at src/lib/supabase.ts. Those variables work in loaders on the server and in components in the browser. Move the file under app/ if you prefer; nothing else changes.

## `npx scribase init` output

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

Next:
  - import { supabase } from './src/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.local

- VITE_SUPABASE_URL
- VITE_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
```

## Move the client next to your routes (optional)

Remix apps keep code under app/. Moving the generated file there keeps imports short.

```bash
mkdir -p app/lib && mv src/lib/supabase.ts app/lib/supabase.ts
```

## 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');
```

## Read it in a loader

The loader runs on the server; the component gets typed data.

_app/routes/_index.tsx_
```tsx
import { useLoaderData } from 'react-router';
import { supabase } from '../lib/supabase';

export async function loader() {
  const { data: todos, error } = await supabase.from('todos').select('id, title');
  if (error) throw error;
  return { todos };
}

export default function Index() {
  const { todos } = useLoaderData<typeof loader>();
  return <ul>{todos.map((t) => <li key={t.id}>{t.title}</li>)}</ul>;
}
```

## Agent prompt

```
Add a Scribase backend to this Remix / React Router 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. Move the client to app/lib/supabase.ts, then load the todos in the index route loader and render them.
4. Never print, log or commit the env file; it is already in .gitignore.
```

## FAQ

**On Remix v2 the import is different.**

Import useLoaderData from @remix-run/react instead of react-router. The rest is identical.

**Where does server-only logic go?**

In loaders and actions. If one needs the service role key, add it to .env.local without the VITE_ prefix and read it with process.env in server code only.