
Supabase and React Admin Integration Guide
TL;DR: A hands-on guide for integrating Supabase (for database/auth) and React Admin (for UI) to build admin panels and customer portals. This combination provides direct database control, custom business logic, and security via Row Level Security, significantly speeding up development of full-featured admin interfaces.
Building admin panels from scratch is tedious. Building them with a CMS often means fighting against the abstractions when you need custom logic. For one client's member management platform, the combination of Supabase and React Admin proved ideal, full control over data and auth, with enough structure to move fast.
Here's what actually matters when putting these two together.
When This Stack Makes Sense
Before getting into the technical setup, it's worth understanding when Supabase + React Admin is the right choice.
The use case: You're building an internal admin tool, a customer portal, or any app where you need direct database control, custom business logic, and role-based access, but you don't want to build everything from scratch.
Why this over a traditional CMS:
Custom data models - Define your exact schema and relationships without CMS constraints
Relational logic - Handle complex relationships naturally in PostgreSQL
Auth tied to data - Row Level Security (RLS) policies control who sees what at the database level
Admin-first workflows - Built for internal tools, not marketing sites
API-first - Direct access to your database through Supabase's REST and GraphQL APIs
Why this over Next.js alone:
React Admin handles the entire admin UI pattern (lists, forms, filters, sorting, pagination)
Supabase provides auth and database access without building an API layer
Less code to maintain than building custom CRUD interfaces
For the client platform managing ~800 members and ~1,200 contacts, this stack meant building a full-featured admin panel in weeks instead of months.
The Stack
Supabase provides the backend, PostgreSQL database, authentication, Row Level Security, and auto-generated APIs through PostgREST.
React Admin is a framework for building admin interfaces. It handles the UI patterns so you focus on your data model and business logic.
ra-supabase connects the two. This official package by the React Admin team provides a dataProvider, authProvider, and UI components. Under the hood, it uses ra-data-postgrest to communicate with Supabase's PostgREST API.
Setting Up the Foundation
Start with a Supabase project and define your schema:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33-- Members table create table members ( id uuid primary key default uuid_generate_v4(), name text not null, email text unique not null, organization text, created_at timestamp with time zone default now() ); -- Contacts table with foreign key to members create table contacts ( id uuid primary key default uuid_generate_v4(), member_id uuid references members(id) on delete cascade, name text not null, phone text, role text, created_at timestamp with time zone default now() ); -- Enable Row Level Security alter table members enable row level security; alter table contacts enable row level security; -- Policy: Only authenticated users can access create policy "Authenticated users can view members" on members for select to authenticated using (true); create policy "Authenticated users can view contacts" on contacts for select to authenticated using (true);
The RLS policies ensure only logged-in users can access the data. You can make these more granular based on user roles if needed.
On the frontend, install React Admin and the Supabase package:
npm install react-admin ra-supabase @supabase/supabase-js
Set up the Supabase client and providers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25import { Admin, Resource } from 'react-admin'; import { supabaseDataProvider, supabaseAuthProvider } from 'ra-supabase'; import { createClient } from '@supabase/supabase-js'; const supabase = createClient( 'YOUR_SUPABASE_URL', 'YOUR_SUPABASE_ANON_KEY' ); const dataProvider = supabaseDataProvider({ instanceUrl: 'YOUR_SUPABASE_URL', apiKey: 'YOUR_SUPABASE_ANON_KEY', supabaseClient: supabase, }); const authProvider = supabaseAuthProvider(supabase); function App() { return ( <Admin dataProvider={dataProvider} authProvider={authProvider}> <Resource name="members" /> <Resource name="contacts" /> </Admin> ); }
That's the basic setup. React Admin automatically generates list, create, edit, and show views based on your data structure.
Customizing Resources
The auto-generated views work, but you'll want to customize them. Here's a customized member list:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24import { List, Datagrid, TextField, EmailField, DateField, TextInput } from 'react-admin'; const memberFilters = [ <TextInput label="Search" source="name@ilike" alwaysOn />, <TextInput label="Organization" source="organization@ilike" />, ]; export const MemberList = () => ( <List filters={memberFilters}> <Datagrid rowClick="edit"> <TextField source="name" /> <EmailField source="email" /> <TextField source="organization" /> <DateField source="created_at" /> </Datagrid> </List> );
The @ilike suffix creates case-insensitive search queries. Since ra-supabase uses PostgREST under the hood, you can use PostgREST operators in your filter sources. React Admin doesn't provide all filter types out of the box, so knowing these operators helps:
@ilike- case-insensitive string matching@cs- array contains@cd- array contained by@ov- array overlap@gte/@lte- greater/less than or equal@fts- full text search (requires tsvector column)
See the PostgREST operators documentation for the complete list.
For relationships, React Admin makes it straightforward:
1 2 3 4 5 6 7 8 9 10 11export const ContactList = () => ( <List> <Datagrid rowClick="edit"> <TextField source="name" /> <TextField source="phone" /> <ReferenceField source="member_id" reference="members"> <TextField source="name" /> </ReferenceField> </Datagrid> </List> );
The ReferenceField automatically fetches and displays the related member's name. On forms, use ReferenceInput for selecting relationships:
1 2 3 4 5 6 7 8 9 10 11 12 13import { Create, SimpleForm, TextInput, ReferenceInput, SelectInput } from 'react-admin'; export const ContactCreate = () => ( <Create> <SimpleForm> <TextInput source="name" /> <TextInput source="phone" /> <ReferenceInput source="member_id" reference="members"> <SelectInput optionText="name" /> </ReferenceInput> </SimpleForm> </Create> );
This creates a dropdown populated with all members when creating a new contact.
The Export Limitation
One gotcha: React Admin's built-in export feature only exports up to 1,000 records and only from the first page. For the client platform with 1,200+ contacts, this wasn't going to work.
The issue runs deeper, Supabase's select query also has a 1,000 record limit. Simply increasing perPage won't help. The solution is to recursively fetch pages until all records are retrieved:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40import { downloadCSV } from 'react-admin'; import jsonExport from 'jsonexport/dist'; const customExporter = async (records, fetchRelatedRecords, dataProvider, resource) => { let allData = []; let page = 1; const perPage = 1000; // Recursively fetch all pages while (true) { const { data } = await dataProvider.getList(resource, { pagination: { page, perPage }, sort: { field: 'id', order: 'ASC' }, filter: {}, }); if (data.length === 0) break; allData = [...allData, ...data]; if (data.length < perPage) break; // Last page page++; } const dataWithRelations = await fetchRelatedRecords( allData, 'member_id', 'members' ); jsonExport(dataWithRelations, {}, (err, csv) => { downloadCSV(csv, resource); }); }; export const ContactList = () => ( <List exporter={customExporter}> {/* ... */} </List> );
Not ideal that this isn't handled by default, but it's a one-time fix that works across all resources.
Authentication
Supabase's auth integrates cleanly with React Admin. The basic setup is simple:
1 2 3 4import { supabaseAuthProvider } from 'ra-supabase'; import { supabase } from './supabase'; const authProvider = supabaseAuthProvider(supabase);
Combined with RLS policies, you get database-level security without writing middleware. The authProvider handles login, logout, and permission checks automatically.
Deployment
The setup is straightforward:
Frontend deploys to Netlify
Database and APIs handled by Supabase
No backend server to maintain
Environment variables for the Supabase URL and anon key are all you need. The RLS policies handle security, so the anon key can be public.
Type safety bonus: Supabase can generate TypeScript types from your database schema. Combined with React Admin's TypeScript support, you get end-to-end type safety from database to UI without manual type definitions.
Real-time capabilities: While not needed for the client’s platform, Supabase supports real-time subscriptions. If you need live updates when data changes, React Admin can integrate with Supabase's real-time features.
What This Approach Delivers
After building the client’s member management platform with this stack:
Faster development - Building forms, lists, and filters manually would've taken 3-4x longer. The auto-generated CRUD interfaces meant shipping features in days instead of weeks.
Consistent UI patterns - React Admin's components create a predictable interface without custom design work. New features follow the same patterns users already know.
Security by default - RLS policies at the database level mean auth bugs are less likely. You're not relying on application-level checks that can be bypassed.
Flexibility when needed - When the built-in export didn't work, customizing it was straightforward. When React Admin's abstractions don't fit, you can drop down to custom components or direct Supabase queries.
Minimal backend maintenance - No Express server to deploy, no API routes to write, no ORM to configure. Database schema changes automatically reflect in the admin interface through the data provider.
The main tradeoff is the learning curve. You need to understand both React Admin's patterns and Supabase's query syntax. But once you do, building admin interfaces becomes much faster.
For larger datasets, consider adding database indexes on frequently filtered columns and using React Admin's infinite scroll. The React Admin performance docs and Supabase optimization guide cover these scenarios.
When to Reach for This Stack
Supabase + React Admin works well when you need full control over your data model, authentication tied directly to database access, and admin interfaces without building everything from scratch.
It's less ideal for content-heavy sites, complex workflow automation, or teams that don't want to deal with SQL and database design.
For the client’s platform's needs, managing members and their contacts with role-based access, this stack delivered a production-ready admin panel efficiently.
Useful Resources: