Supabase
Store themes in Supabase with Row Level Security (RLS) for multi-tenant applications.
Table Setup
Create a themes table in Supabase:
supabase/migrations/create_themes.sql
-- Create themes table
CREATE TABLE themes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
theme_id TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
tokens JSONB NOT NULL,
version INTEGER DEFAULT 1,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Create index for fast lookups
CREATE INDEX idx_themes_theme_id ON themes(theme_id);
-- Enable RLS
ALTER TABLE themes ENABLE ROW LEVEL SECURITY;
-- Policy: Users can only read their own theme
CREATE POLICY "Users can read own theme"
ON themes
FOR SELECT
USING (theme_id = current_setting('app.theme_id', true));
-- Policy: Service role can do everything
CREATE POLICY "Service role full access"
ON themes
FOR ALL
USING (auth.role() = 'service_role');
-- Auto-update updated_at
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER themes_updated_at
BEFORE UPDATE ON themes
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();Supabase Client Setup
lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
// Admin client for server-side operations
export const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);Resolver Setup
lib/resolver.ts
import { createResolver, validate, type InferTheme } from '@livery/core';
import { supabaseAdmin } from './supabase';
import { schema } from './schema';
// Type-safe default theme using schema inference
type Theme = InferTheme<typeof schema.definition>;
const defaultTheme: Theme = {
colors: {
primary: '#14B8A6',
background: '#FFFFFF',
text: '#0F172A',
},
};
export const resolver = createResolver({
schema,
fetcher: async ({ themeId }) => {
const { data, error } = await supabaseAdmin
.from('themes')
.select('tokens')
.eq('theme_id', themeId)
.eq('is_active', true)
.single();
if (error || !data) {
return {}; // Resolver merges with schema defaults
}
// Validate tokens from database
const result = validate({ schema, data: data.tokens });
if (!result.success) {
console.error('Invalid theme tokens:', result.errors);
return {};
}
return result.data;
},
cache: {
ttl: 5 * 60 * 1000,
},
});TypeScript Types
Generate types from your Supabase schema:
npx supabase gen types typescript --project-id your-project-id > lib/database.types.tsUse the generated types:
lib/resolver.ts
import type { Database } from './database.types';
type Theme = Database['public']['Tables']['themes']['Row'];
const { data } = await supabaseAdmin
.from('themes')
.select('*')
.eq('theme_id', themeId)
.single<Theme>();Real-Time Updates
Supabase supports real-time subscriptions. Update themes instantly:
components/theme-listener.tsx
'use client';
import { useEffect } from 'react';
import { supabase } from '@/lib/supabase';
import { useTheme } from '@/lib/livery';
export function ThemeListener({ themeId }: { themeId: string }) {
const { refresh } = useTheme();
useEffect(() => {
const channel = supabase
.channel('theme-changes')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'themes',
filter: `theme_id=eq.${themeId}`,
},
() => {
// Theme was updated, refresh Livery
refresh();
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [themeId, refresh]);
return null;
}Add to your layout:
<DynamicThemeProvider initialThemeId={themeId} resolver={resolver}>
<ThemeListener themeId={themeId} />
{children}
</DynamicThemeProvider>CRUD API Routes
Create Theme
app/api/themes/route.ts
import { supabaseAdmin } from '@/lib/supabase';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const body = await request.json();
const { data, error } = await supabaseAdmin
.from('themes')
.insert({
theme_id: body.themeId,
name: body.name,
tokens: body.tokens,
})
.select()
.single();
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json(data);
}Update Theme
export async function PUT(request: Request) {
const body = await request.json();
const { data, error } = await supabaseAdmin
.from('themes')
.update({
name: body.name,
tokens: body.tokens,
version: body.version + 1,
})
.eq('theme_id', body.themeId)
.select()
.single();
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json(data);
}Row Level Security Patterns
Per-User Themes
Let users customize their own themes:
-- Users can only access their own theme
CREATE POLICY "Users manage own theme"
ON themes
FOR ALL
USING (theme_id = auth.uid()::text);Organization-Based Access
For team/organization themes:
-- Users can access their organization's theme
CREATE POLICY "Org members can read theme"
ON themes
FOR SELECT
USING (
theme_id IN (
SELECT organization_id::text
FROM organization_members
WHERE user_id = auth.uid()
)
);Edge Functions
For server-side theme fetching with caching:
supabase/functions/get-theme/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
serve(async (req) => {
const { themeId } = await req.json();
const { data, error } = await supabase
.from('themes')
.select('tokens')
.eq('theme_id', themeId)
.single();
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 404,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300', // Cache 5 min
},
});
}
return new Response(JSON.stringify(data.tokens), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300',
},
});
});Performance Tips
- Use service role for server fetches — Bypasses RLS overhead
- Cache at the edge — Use Supabase Edge Functions
- Batch requests — Fetch multiple themes in one query if needed
- Index JSONB paths — If querying specific token values
Last updated on