Add Supabase (optional)
Turn ContentAI into a multi-user SaaS with auth and cloud-synced data.
The shipping codebase uses NextAuth (Auth.js v5) + Supabase Postgres with OAuth and a server-only service role. Read Authentication & database (NextAuth + Supabase) for the exact file layout, env vars, and migrations.
The sections below are an alternate DIY recipe (for example if you prefer Supabase Auth and @supabase/ssr in middleware). They do not match every line of the reference app.
ContentAI can be single-user (localStorage only) — great for personal tools and internal teams. If you want to adapt it as a multi-tenant SaaS with accounts and cross-device sync, here is one possible Supabase-centric recipe.
What you gain
- User accounts with email/password, Google, GitHub, Apple, etc.
- Cross-device sync — generations and documents live in Postgres, not just localStorage
- Shared workspaces (if you want)
- Server-side billing (combine with Stripe)
1. Create a Supabase project
- Go to supabase.com → New project
- Choose a region close to your users
- Grab the Project URL and anon key
2. Install packages
pnpm add @supabase/supabase-js @supabase/ssr3. Environment variables
Create .env.local:
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...4. Create Supabase client helpers
lib/supabase/client.ts:
"use client";
import { createBrowserClient } from "@supabase/ssr";
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
}lib/supabase/server.ts:
import { cookies } from "next/headers";
import { createServerClient } from "@supabase/ssr";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => cookieStore.getAll(),
setAll: (all) => all.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
),
},
},
);
}5. Schema
In the Supabase SQL editor:
create table public.generations (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users on delete cascade,
template_id text,
template_name text,
category text,
content text,
provider text,
model text,
word_count int,
inputs jsonb,
created_at timestamptz default now()
);
create table public.documents (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users on delete cascade,
title text,
content text,
updated_at timestamptz default now()
);
create table public.api_keys (
user_id uuid references auth.users on delete cascade primary key,
groq text,
google text,
openai text,
anthropic text,
active_provider text,
selected_models jsonb
);
-- Row Level Security
alter table public.generations enable row level security;
alter table public.documents enable row level security;
alter table public.api_keys enable row level security;
create policy "own generations" on public.generations
for all using (auth.uid() = user_id);
create policy "own documents" on public.documents
for all using (auth.uid() = user_id);
create policy "own keys" on public.api_keys
for all using (auth.uid() = user_id);6. Protect the dashboard
Add middleware.ts at the project root:
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => request.cookies.getAll(),
setAll: (all) => all.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options)
),
},
},
);
const { data: { user } } = await supabase.auth.getUser();
if (!user && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return response;
}
export const config = {
matcher: ["/dashboard/:path*", "/generate", "/editor", "/history", "/settings"],
};7. Swap the Zustand store for Supabase
Two approaches:
Hybrid (easiest): Keep Zustand for UX, but mirror writes to Supabase in addGeneration, addDocument, etc.
Full replacement: Replace Zustand actions with React Query hooks (useQuery/useMutation) that hit Supabase directly. Best for larger teams.
8. Encrypt API keys at rest
create extension if not exists pgcrypto;
-- Insert
insert into api_keys(user_id, groq)
values (auth.uid(), pgp_sym_encrypt('gsk_...', current_setting('app.enc_key')));
-- Select
select pgp_sym_decrypt(groq::bytea, current_setting('app.enc_key')) as groq
from api_keys where user_id = auth.uid();Set app.enc_key via a Supabase Edge Function or as a session-level setting.
9. Add billing
Drop in Stripe and gate providers (or template categories) by subscription plan.
Done
You now have a fully multi-tenant version of ContentAI running on Supabase. All existing features (templates, editor, history, settings) work the same way — they just sync through Supabase instead of only living in the browser.
Back to: Deployment overview · Next: Troubleshooting →