When your application starts growing, you inevitably need an admin dashboard to track metrics, analyze signups, and monitor revenue. While low-code tools like Retool are great for internal operations, sometimes a custom-built React dashboard deployed on Vercel is faster, cleaner, and exactly what you need.
However, building a custom admin dashboard connected to a Supabase backend presents a unique security challenge: How do you query all data (bypassing Row Level Security) without exposing your highly privileged service role key in the frontend?
Here is the step-by-step architecture for securely building an admin dashboard with Supabase.
The Security Problem
By default, Supabase enforces Row Level Security (RLS). When a user logs in, they only see the data they are permitted to see.
An admin dashboard needs to see *everything*—total users, all bills, revenue aggregates, etc.
The dangerous temptation is to initialize the Supabase client in your React dashboard using the `service_role` key. Never do this. Anything in a frontend React app is visible to users who inspect the page source. Exposing your service role key gives anyone total read/write access to your entire database, bypassing all security rules.
The Secure Architecture: Supabase Edge Functions
The correct way to handle admin queries is to use a secure proxy. We will build a small Supabase Edge Function that runs securely on the server.
* Dashboard (Browser): Makes a request to the Edge Function with a secret admin token. * Edge Function (Server): Verifies the token, uses the `service_role` key (safely hidden on the server) to query the database, and returns the aggregated data. * Database: Returns the data, bypassing RLS safely.
### Step 1: Create the Edge Function
Create a new Edge Function in your Supabase project (e.g., `admin-dashboard-stats`). This TypeScript function will securely fetch your data.
```typescript // supabase/functions/admin-dashboard-stats/index.ts import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' import { corsHeaders } from '../_shared/cors.ts'
Deno.serve(async (req) => { // Handle CORS preflight requests if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
// Simple secret check — verify the dashboard is authorized const authHeader = req.headers.get('Authorization') if (authHeader !== `Bearer ${Deno.env.get('ADMIN_DASHBOARD_SECRET')}`) { return new Response('Unauthorized', { status: 401 }) }
// Initialize Supabase client WITH the service_role key const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! )
// Fetch all necessary admin data simultaneously const [users, tenants, farmerBills, buyerBills, orders, farmers] = await Promise.all([ supabase.from('users').select('id,name,email,business_name,city,state,plan_type,created_at,is_active'), supabase.from('tenants').select('id,name,created_at,subscription_tier,is_active'), supabase.from('farmer_bills').select('id,created_at,final_payable'), supabase.from('buyer_bills').select('id,created_at,final_payable'), supabase.from('orders').select('id,amount,payment_status,created_at'), supabase.from('farmers').select('id,created_at'), ])
// Return the aggregated data return new Response(JSON.stringify({ users: users.data, tenants: tenants.data, farmerBills: farmerBills.data, buyerBills: buyerBills.data, orders: orders.data, farmers: farmers.data, }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) }) ```
### Step 2: Set Environment Variables
You need to define the secret password that your dashboard will use to communicate with this function.
In your Supabase Dashboard, go to Settings → Edge Functions → Secrets, and add a new secret: `ADMIN_DASHBOARD_SECRET` = `some-long-random-secret-you-make-up`
*(Note: `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY` are automatically injected into the Edge Function environment by Supabase.)*
### Step 3: Deploy the Function
Deploy your new function using the Supabase CLI: ```bash supabase functions deploy admin-dashboard-stats ```
### Step 4: Update Your React Dashboard
Now, your custom React dashboard simply makes a standard `fetch` request to your new Edge Function URL, passing the secret token in the Authorization header:
```javascript const fetchDashboardStats = async () => { const response = await fetch('https://your-project-id.supabase.co/functions/v1/admin-dashboard-stats', { method: 'POST', headers: { 'Authorization': `Bearer some-long-random-secret-you-make-up`, 'Content-Type': 'application/json' } }); const data = await response.json(); // Update your React state and render your charts! } ```
By following this pattern, your service role key never leaves the server, your database remains secure, and you get a blazingly fast, custom-built admin dashboard to monitor your business metrics.