ContentAIDOCS
Deployment

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

  1. Go to supabase.com → New project
  2. Choose a region close to your users
  3. Grab the Project URL and anon key

2. Install packages

pnpm add @supabase/supabase-js @supabase/ssr

3. 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 →

On this page