Clete Access Hub

Setup Guide

This app needs a Supabase project for auth, database storage, client onboarding tokens, and file uploads.

1. Create Supabase Project

Create a Supabase project, then copy your project URL, anon key, and service role key.

2. Add Environment Variables

NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
NEXT_PUBLIC_AGENCY_EMAIL=clete@youragency.com
NEXT_PUBLIC_META_PARTNER_BUSINESS_ID=...
META_APP_ID=...
META_APP_SECRET=...
META_BUSINESS_LOGIN_CONFIG_ID=...
META_GRAPH_VERSION=v24.0
GOOGLE_OAUTH_CLIENT_ID=...
GOOGLE_OAUTH_CLIENT_SECRET=...

3. Run SQL

Paste this into the Supabase SQL editor.

create extension if not exists pgcrypto;

create table if not exists public.clients (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid not null references auth.users(id) on delete cascade,
  name text not null,
  package_key text not null,
  created_at timestamptz not null default now()
);

create table if not exists public.onboarding_links (
  id uuid primary key default gen_random_uuid(),
  client_id uuid not null references public.clients(id) on delete cascade,
  token text not null unique,
  is_active boolean not null default true,
  created_at timestamptz not null default now()
);

create table if not exists public.client_tasks (
  id uuid primary key default gen_random_uuid(),
  client_id uuid not null references public.clients(id) on delete cascade,
  task_key text not null,
  task_type text not null check (task_type in ('access', 'intake')),
  title text not null,
  category text not null,
  why text not null,
  status text not null default 'not_started' check (status in ('not_started', 'waiting', 'submitted', 'confirmed', 'blocked', 'setup_needed')),
  metadata jsonb not null default '{}'::jsonb,
  response text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table if not exists public.task_files (
  id uuid primary key default gen_random_uuid(),
  task_id uuid not null references public.client_tasks(id) on delete cascade,
  client_id uuid not null references public.clients(id) on delete cascade,
  file_name text not null,
  file_path text not null,
  file_size bigint,
  mime_type text,
  created_at timestamptz not null default now()
);

create index if not exists clients_owner_id_idx on public.clients(owner_id);
create index if not exists onboarding_links_token_idx on public.onboarding_links(token);
create index if not exists onboarding_links_client_id_idx on public.onboarding_links(client_id);
create index if not exists client_tasks_client_id_idx on public.client_tasks(client_id);
create index if not exists task_files_client_id_idx on public.task_files(client_id);
create index if not exists task_files_task_id_idx on public.task_files(task_id);

grant usage on schema public to authenticated;
grant usage on schema public to service_role;
grant select, insert, update, delete on public.clients to authenticated;
grant select, insert, update, delete on public.onboarding_links to authenticated;
grant select, insert, update, delete on public.client_tasks to authenticated;
grant select on public.task_files to authenticated;
grant select, insert, update, delete on public.clients to service_role;
grant select, insert, update, delete on public.onboarding_links to service_role;
grant select, insert, update, delete on public.client_tasks to service_role;
grant select, insert, update, delete on public.task_files to service_role;

alter table public.clients enable row level security;
alter table public.onboarding_links enable row level security;
alter table public.client_tasks enable row level security;
alter table public.task_files enable row level security;

drop policy if exists "Owners can manage their clients" on public.clients;
create policy "Owners can manage their clients"
on public.clients
for all
using (auth.uid() = owner_id)
with check (auth.uid() = owner_id);

drop policy if exists "Owners can manage onboarding links" on public.onboarding_links;
create policy "Owners can manage onboarding links"
on public.onboarding_links
for all
using (
  exists (
    select 1 from public.clients
    where clients.id = onboarding_links.client_id
    and clients.owner_id = auth.uid()
  )
)
with check (
  exists (
    select 1 from public.clients
    where clients.id = onboarding_links.client_id
    and clients.owner_id = auth.uid()
  )
);

drop policy if exists "Owners can manage client tasks" on public.client_tasks;
create policy "Owners can manage client tasks"
on public.client_tasks
for all
using (
  exists (
    select 1 from public.clients
    where clients.id = client_tasks.client_id
    and clients.owner_id = auth.uid()
  )
)
with check (
  exists (
    select 1 from public.clients
    where clients.id = client_tasks.client_id
    and clients.owner_id = auth.uid()
  )
);

drop policy if exists "Owners can view task files" on public.task_files;
create policy "Owners can view task files"
on public.task_files
for select
using (
  exists (
    select 1 from public.clients
    where clients.id = task_files.client_id
    and clients.owner_id = auth.uid()
  )
);

-- Client-facing onboarding access is handled by Next.js API routes using the
-- Supabase service role key. Do not expose SUPABASE_SERVICE_ROLE_KEY to the browser.

insert into storage.buckets (id, name, public)
values ('client-uploads', 'client-uploads', false)
on conflict (id) do nothing;

4. Create Admin Account

Open login, create your admin account, then create your first client.