Supabase
Supabase is an open-source Firebase alternative that gives you a Postgres database, authentication, real-time subscriptions, edge functions, and file storage. All from one dashboard. Flux uses Supabase to power user accounts, order history, wishlists, and any server-side data that Stripe or Sanity don't own.
Last updated: 2026-03-29
Where Supabase Fits
In a Flux project, each service owns a specific slice of data. Supabase handles everything that needs a persistent relational store tied to a logged-in user. Profiles, orders, wishlists, and custom business logic.
Client (browser)
│
├── Auth layer (Supabase Auth)
│ → sign-up / sign-in → issues JWT
│ → JWT attached to every request automatically
│
├── Database (Postgres via supabase-js)
│ → supabase.from('orders').select()
│ → Row-Level Security enforces per-user access
│ → Real-time subscriptions push live updates
│
├── Storage (Supabase Storage)
│ → product images, user avatars, receipts
│ → Signed URLs for private files
│
└── Edge Functions (Deno)
→ webhooks, background jobs, secrets-safe logic| What | Who owns it |
|---|---|
| Payments & subscriptions | Stripe |
| Blog / CMS content | Sanity |
| User profiles & sessions | Supabase Auth |
| Orders, wishlists, custom data | Supabase Postgres |
| Product & user media | Supabase Storage |
Setup & Configuration
Create a free project at supabase.com, copy your project URL and anon key, then install the JS client. The anon key is safe to expose to the browser. Row-Level Security on your tables enforces what each user can actually read or write.
# Install the Supabase client
npm install @supabase/supabase-js
# .env
PUBLIC_SUPABASE_URL=https://your-project.supabase.co
PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # server-side only
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
// Browser client — uses anon key, respects RLS
export const supabase = createClient(
import.meta.env.PUBLIC_SUPABASE_URL,
import.meta.env.PUBLIC_SUPABASE_ANON_KEY
);
// Server client — bypasses RLS, use in API routes only
export const supabaseAdmin = createClient(
import.meta.env.PUBLIC_SUPABASE_URL,
import.meta.env.SUPABASE_SERVICE_ROLE_KEY
);Authentication
Supabase Auth handles sign-up, sign-in, OAuth providers (Google, GitHub), magic links, and session management. The session JWT is automatically attached to every supabase-js request, so your RLS policies can reference auth.uid() to scope rows to the current user.
// Sign up
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'securepassword'
});
// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'securepassword'
});
// OAuth (Google)
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: 'https://yoursite.com/auth/callback' }
});
// Get current session
const { data: { session } } = await supabase.auth.getSession();
// Sign out
await supabase.auth.signOut();
// Listen for auth state changes (React)
import { useEffect } from 'react';
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
if (event === 'SIGNED_IN') console.log('User signed in:', session.user);
if (event === 'SIGNED_OUT') console.log('User signed out');
}
);
return () => subscription.unsubscribe();
}, []);Database & Row-Level Security
Supabase gives you a full Postgres database. Query it with the supabase-js client or raw SQL. Row-Level Security (RLS) policies run inside Postgres. They ensure users can only read or write their own rows, even if the anon key is exposed.
-- Create an orders table
CREATE TABLE orders (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid REFERENCES auth.users NOT NULL,
amount integer NOT NULL,
status text DEFAULT 'pending',
created_at timestamptz DEFAULT now()
);
-- Enable Row-Level Security
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Policy: users can only see their own orders
CREATE POLICY "Users see own orders"
ON orders FOR SELECT
USING (auth.uid() = user_id);
-- Policy: users can insert their own orders
CREATE POLICY "Users insert own orders"
ON orders FOR INSERT
WITH CHECK (auth.uid() = user_id);
// Query from the client (RLS applied automatically)
const { data: orders, error } = await supabase
.from('orders')
.select('id, amount, status, created_at')
.order('created_at', { ascending: false });
// Insert a new order
const { data, error } = await supabase
.from('orders')
.insert({ user_id: session.user.id, amount: 4900, status: 'paid' });
// Real-time subscription
const channel = supabase
.channel('orders-changes')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'orders' },
(payload) => console.log('New order:', payload.new)
)
.subscribe();File Storage
Supabase Storage is an S3-compatible object store. Use it for product images, user avatars, and any file uploads. Buckets can be public (CDN-served) or private (signed URLs required). Storage policies use the same RLS syntax as database tables.
// Create a bucket in the dashboard or via client
await supabaseAdmin.storage.createBucket('avatars', { public: true });
// Upload a file
const file = event.target.files[0];
const path = `${session.user.id}/avatar.webp`;
const { data, error } = await supabase.storage
.from('avatars')
.upload(path, file, { upsert: true, contentType: 'image/webp' });
// Get a public URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(path);
// Get a signed URL for private files (expires in 60s)
const { data: { signedUrl } } = await supabase.storage
.from('receipts')
.createSignedUrl(`${session.user.id}/receipt-123.pdf`, 60);
// Delete a file
await supabase.storage.from('avatars').remove([path]);