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.

Terminal
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
WhatWho owns it
Payments & subscriptionsStripe
Blog / CMS contentSanity
User profiles & sessionsSupabase Auth
Orders, wishlists, custom dataSupabase Postgres
Product & user mediaSupabase 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.

src/lib/supabase.ts
# 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.

Terminal
// 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.

Terminal
-- 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.

Terminal
// 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]);