BiTree
  • Search For Lessons
  • Curriculum
  • Pricing
  • For Educators
  • Become a Tutor
  • About
  • Contact
Log InGet Started

Questions, concerns, bug reports, or suggestions? We read every message, write to us at [email protected].

More ways to reach us →
BiTree

Live coding lessons for aspiring developers and security professionals.

[email protected]

(201) 785-7951

Mon–Fri, 9 AM–5 PM EST

Learn

  • Search For Lessons
  • Curriculum
  • Pricing

Company

  • About
  • For Educators & Schools
  • Become a Tutor
  • Contact Us

Legal

  • Terms of Service
  • Privacy Policy
© 2026 BiTree. All rights reserved.
Curriculum/Web Development/React and Next.js/Connecting Supabase: Database and Auth
60 minIntermediate

Connecting Supabase: Database and Auth

After this lesson, you will be able to: Stand up a Supabase project, connect the Supabase JS client to a Next.js app, read and write data with Row-Level Security enabled, and wire up email/password authentication end to end.

Supabase is an open-source backend-as-a-service built on PostgreSQL. It gives a Next.js app three things you'd otherwise build yourself: a managed Postgres database, authentication (email/password, OAuth, magic links), and storage for files. It's the modern open-source alternative to Firebase, and the default Supabase + Next.js + Vercel stack is what most new full-stack apps in 2025 use. This lesson takes you from never having opened the Supabase dashboard to a working database-backed, auth-protected Next.js feature.

Prerequisites:API Routes in Next.js

Why Supabase

Three reasons it's the default new-project backend in 2025. **Postgres, not a custom database.** You learn SQL skills that transfer to every other job. **Auth, storage, realtime, and edge functions on the same platform** — no stitching together five vendors for a starter project. **Open source.** You can self-host the same software if you need to. Firebase, the previous default, is closed-source and proprietary; that matters when you scale or audit. Supabase covers 90% of what a small-to-medium SaaS app needs. When you outgrow it, you migrate the database (it's just Postgres) without re-architecting.

Setup — create a project and grab the keys

Sign in at supabase.com, create a new project (free tier is enough), then go to Project Settings → API. You need TWO values for Next.js: the Project URL and the `anon` (public) key. The `service_role` key is the admin key — keep it server-side ONLY.

bash
# In your Next.js app
npm install @supabase/supabase-js @supabase/ssr
# .env.local (NEVER commit this file)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=ey... # safe to expose to browser
SUPABASE_SERVICE_ROLE_KEY=ey... # SERVER-ONLY. Never NEXT_PUBLIC_.

Two clients — server and browser

Modern Supabase + Next.js uses separate clients for Server Components (which handle cookies for auth) and Client Components. The `@supabase/ssr` package generates both.

python
// lib/supabase/server.ts — for Server Components and API routes
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function getSupabaseServer() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll(); },
setAll(cookiesToSet) {
for (const { name, value, options } of cookiesToSet) {
cookieStore.set(name, value, options);
}
},
},
},
);
}
// lib/supabase/client.ts — for Client Components
"use client";
import { createBrowserClient } from "@supabase/ssr";
export function getSupabaseBrowser() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
}

Reading data from a Server Component

Create a table in Supabase (Table Editor → New Table → `notes` with `id`, `body`, `user_id`). Then read it from a page.

html
// app/notes/page.tsx — Server Component
import { getSupabaseServer } from "@/lib/supabase/server";
export default async function NotesPage() {
const supabase = await getSupabaseServer();
const { data: notes, error } = await supabase
.from("notes")
.select("id, body, created_at")
.order("created_at", { ascending: false });
if (error) return <p>Error: {error.message}</p>;
return (
<ul className="space-y-2">
{notes?.map((n) => (
<li key={n.id} className="border rounded p-3">{n.body}</li>
))}
</ul>
);
}

Row-Level Security (RLS)

By default, the Supabase `anon` key can do ANYTHING to your tables. That's a problem the moment your app goes live. Row-Level Security is Postgres's built-in answer: define policies that say WHO can SELECT / INSERT / UPDATE / DELETE which rows. Once RLS is enabled on a table, the default is 'deny everything,' and your queries return empty until you add a permitting policy. Always enable RLS on every user-data table BEFORE you put real users on it. Forgetting is the most expensive Supabase mistake.

Enabling RLS with a sane default policy

Run this in the Supabase SQL Editor. The policy says: a user can only SELECT/INSERT their own notes, identified by matching `user_id` to their auth ID.

sql
-- 1. Enable RLS
alter table notes enable row level security;
-- 2. Anyone authenticated can read their own notes
create policy "read own notes"
on notes for select
using (auth.uid() = user_id);
-- 3. Anyone authenticated can insert a note as themselves
create policy "insert own notes"
on notes for insert
with check (auth.uid() = user_id);
-- 4. Anyone authenticated can update/delete their own notes
create policy "modify own notes"
on notes for update using (auth.uid() = user_id);
create policy "delete own notes"
on notes for delete using (auth.uid() = user_id);

Auth — sign up, sign in, sign out

Supabase Auth ships email/password, magic links, and OAuth out of the box. Below is the minimum email/password flow as a Client Component form.

python
"use client";
import { useState } from "react";
import { getSupabaseBrowser } from "@/lib/supabase/client";
export function AuthForm() {
const supabase = getSupabaseBrowser();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
async function signUp() {
setError(null);
const { error } = await supabase.auth.signUp({ email, password });
if (error) setError(error.message);
else alert("Check your email to confirm.");
}
async function signIn() {
setError(null);
const { error } = await supabase.auth.signInWithPassword({ email, password });
if (error) setError(error.message);
else window.location.assign("/notes");
}
return (
<form className="max-w-sm space-y-3" onSubmit={(e) => e.preventDefault()}>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" className="w-full border p-2 rounded" />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" className="w-full border p-2 rounded" />
{error && <p className="text-red-600 text-sm">{error}</p>}
<div className="flex gap-2">
<button onClick={signIn} className="rounded bg-slate-900 px-4 py-2 text-white">Sign in</button>
<button onClick={signUp} className="rounded border px-4 py-2">Sign up</button>
</div>
</form>
);
}

Reading the current user on the server

Use the server client to check who's signed in. Protect routes by redirecting unauthenticated users.

python
// app/notes/page.tsx — gate behind auth
import { redirect } from "next/navigation";
import { getSupabaseServer } from "@/lib/supabase/server";
export default async function NotesPage() {
const supabase = await getSupabaseServer();
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect("/login");
const { data: notes } = await supabase
.from("notes")
.select("id, body")
.order("created_at", { ascending: false });
return (
<ul>
{notes?.map((n) => <li key={n.id}>{n.body}</li>)}
</ul>
);
}

💡 Supabase is PostgreSQL: learn the SQL underneath

Supabase is a friendly layer over a real Postgres database, and the `.from().select()` calls compile down to SQL. The RLS policies above are pure SQL. The better your SQL, the more you can do here: joins instead of N+1 round trips, window functions, views, and indexes that keep queries fast. The Programming Languages > SQL subtrack teaches all of it on PostgreSQL from zero, and the Software Engineering > System Design track shows how the same database scales. Worth taking in parallel once the basics here click.

💡 Common mistakes only experienced devs catch

Six Supabase pitfalls that have cost real teams real money. (1) **Exposing the service-role key to the browser** — prefixing it with `NEXT_PUBLIC_` or importing the wrong file in a Client Component leaks admin access. Always use the anon key in the browser; service-role only in server-only files. (2) **Forgetting to enable RLS** — a table without RLS is a public database to anyone with the anon key. Enable RLS as the first thing you do on every user-data table. (3) **RLS policies that look right but aren't tested** — verify each policy by querying as a different user (Supabase dashboard → Authentication → impersonate user). Untested policies usually have an off-by-one bug. (4) **Using `.single()` when expecting maybe-zero rows** — `.single()` throws if there are zero or more than one matches. Use `.maybeSingle()` when zero is allowed. (5) **Mixing the server and browser client** — calling the browser client in a Server Component (or vice versa) compiles, then breaks at runtime with confusing cookie errors. Stick to the file conventions in this lesson. (6) **Hardcoding the URL/anon key** — read from env vars so you can swap databases per environment (dev / preview / prod).

Sign in and purchase access to unlock this lesson.

Sign in to purchase
←API Routes in Next.js
Back to React and Next.js
Deploying to Vercel in Under 5 Minutes→