Guides

How to Build a CRM With Next.js and Supabase

This covers the foundational pieces of a custom CRM built on Next.js and Supabase — the data model, access control, and a basic pipeline view — rather than every feature a full CRM eventually needs.

Prerequisites

  • arrow_rightA Supabase project
  • arrow_rightA Next.js App Router project with Supabase auth already set up

Step 1: Design the Core Schema

create table contacts (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid references auth.users(id),
  name text not null,
  email text,
  created_at timestamptz default now()
);

create table deals (
  id uuid primary key default gen_random_uuid(),
  contact_id uuid references contacts(id),
  owner_id uuid references auth.users(id),
  stage text not null default 'new',
  value numeric,
  created_at timestamptz default now()
);

Step 2: Add Row-Level Security

alter table contacts enable row level security;

create policy "Users see their own contacts"
  on contacts for select
  using (owner_id = auth.uid());

Repeat similar policies for insert, update, and the deals table — RLS is what actually enforces that one sales rep can't see another's pipeline, regardless of what the frontend shows.

Step 3: Build the Pipeline View

Fetch deals grouped by stage in a server component, and render them as columns — a simple starting point before adding drag-and-drop stage changes, which needs a client component and an update call back to Supabase.

Common Errors

  • arrow_rightData visible across all users despite RLS being enabled — check that a policy actually exists for the operation (select/insert/update/delete) being performed; RLS with no matching policy denies access by default, but a missing policy for one operation while others exist can create confusing gaps
  • arrow_rightForeign key errors when creating a deal — confirm the referenced contact_id actually exists and belongs to the same owner
  • arrow_rightSlow pipeline queries as data grows — add an index on the stage and owner_id columns used in the main query

Best Practices

  • arrow_rightDesign RLS policies before writing any frontend code, not after
  • arrow_rightKeep the schema close to the actual sales process rather than a generic template
  • arrow_rightAdd an activity/notes table early — it's usually needed sooner than expected

Frequently Asked Questions

Do I need a separate backend API, or can the frontend talk to Supabase directly?add

For most CRM use cases, querying Supabase directly from server components (or via the client library with RLS enforcing access) is sufficient — a separate API layer is worth adding only once business logic gets complex enough to need it centralized.

How do I add automation, like AI-drafted follow-ups?add

That's the AI-powered CRM layer — once the core schema and RLS are solid, AI features read from and write back to the same tables, calling an LLM API for the generative parts.

Can multiple sales reps share visibility into the same deals?add

Yes, if that's the intended workflow — the RLS policy would check team or organization membership instead of a strict owner_id match, depending on how sharing should work.

BUILDING SOMETHING SIMILAR?

If you'd rather have this built and shipped than build it yourself, I'm available for freelance and contract work.