Post 1.4 — Next.js App Router Setup + Connecting Supabase

This post covers project setup, Supabase package installation, environment variables, and creating separate Supabase clients for the browser and the server — the foundation every later post builds on.

Step 1 — Create the Next.js App


    npx create-next-app@latest my-app

Answer the prompts:


    TypeScript?         → Yes
    ESLint?             → Yes
    Tailwind CSS?       → Yes
    App Router?         → Yes
    src/ directory?     → Your choice (this course assumes No)
    Import alias?       → Keep default (@/*)

This gives you TypeScript and Tailwind CSS fully configured out of the box — nothing extra to set up.

Shortcut: Supabase also maintains an official starter template that scaffolds this entire setup (packages, env file, both clients) automatically: 

    npx create-next-app -e with-supabase   

This course builds everything manually instead, so you understand exactly what each piece does. Once you're comfortable, the template is a legitimate way to start real projects faster.

Step 2 — Install the Supabase Packages


    cd my-app
    npm install @supabase/supabase-js @supabase/ssr

  • @supabase/supabase-js — the core client library: query builder, auth methods, storage methods, realtime, everything.
  • @supabase/ssr — manages the Supabase session correctly across Server Components, Client Components, Route Handlers, and Middleware in frameworks like Next.js. This is the current, officially maintained package — the older @supabase/auth-helpers-nextjs package is deprecated and no longer receives updates.

Step 3 — Add Environment Variables

From your Supabase Dashboard → Project Settings → API Keys → "Publishable and secret API keys" tab, copy your Project URL, publishable key, and secret key.

Finding your Project URL: it's not on the API Keys page — go to Project Settings → Data API (or click the green Connect button at the top of the dashboard). You'll see a URL like https://your-ref.supabase.co/rest/v1/ — use only the base part (without /rest/v1/) as NEXT_PUBLIC_SUPABASE_URL.

supabase-js and @supabase/ssr automatically append /rest/v1/, /auth/v1/, /storage/v1/, etc. depending on which service you're calling — so the env variable should only hold the base domain, not the full path.

Create .env.local in your project root:


    NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
    NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxxxxxxxxxxxxxxx
    SUPABASE_SECRET_KEY=sb_secret_xxxxxxxxxxxxxxxx

Why the naming matters: any variable prefixed with NEXT_PUBLIC_ gets bundled into browser-visible JavaScript by Next.js. That's why the publishable key uses that prefix (it's meant to be public) and the secret key does not (it must never reach the browser).

Step 4 — Why You Need Two Different Clients

A Next.js App Router app runs code in two different environments:

Environment

Where

Cookie access

Browser

Client Components

Browser cookie APIs

Server

Server Components, Route Handlers, Middleware

Next.js server cookie APIs (next/headers)

Because Supabase Auth stores the session in cookies, the client must read/write cookies using whichever API matches its environment. @supabase/ssr gives you two dedicated functions for exactly this.

Step 5 — Create the Browser Client

lib/supabase/client.ts:


    import { createBrowserClient } from '@supabase/ssr'

    export function createClient() {
        return createBrowserClient(
            process.env.NEXT_PUBLIC_SUPABASE_URL!,
            process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
        )
    }

Use this inside Client Components ('use client' files) — for example, a login form that calls supabase.auth.signInWithPassword() directly from the browser.

Step 6 — Create the Server Client

lib/supabase/server.ts:


    import { createServerClient } from '@supabase/ssr'
    import { cookies } from 'next/headers'

    export async function createClient() {
        const cookieStore = await cookies()

        return createServerClient(
            process.env.NEXT_PUBLIC_SUPABASE_URL!,
            process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
            {
                cookies: {
                    getAll() {
                        return cookieStore.getAll()
                    },
                    setAll(cookiesToSet) {
                        try {
                            cookiesToSet.forEach(({ name, value, options }) =>
                                cookieStore.set(name, value, options)
                            )
                        } catch {
                            // setAll can be called from a Server Component, where
                            // cookies can't be modified. Safe to ignore if you're
                            // refreshing sessions in Middleware (covered in Phase 4).
                        }
                    },
                },
            }
        )
    }

Use this inside Server Components, Server Actions, and Route Handlers — anywhere code runs on the server. Notice it's async because Next.js's cookies() function is asynchronous.

Both clients use the publishable key, not the secret key. Even server-rendered code should generally act "as the logged-in user" and rely on Row Level Security for protection — the secret key is reserved for trusted backend logic that deliberately needs to bypass RLS (covered when we get to Auth in Phase 4).

Step 7 — Verify the Connection

Create a table to test against. In the Supabase SQL Editor:


  create table instruments (
    id bigint primary key generated always as identity,
    name text not null
  );

  insert into instruments (name) values ('violin'), ('viola'), ('cello');

  alter table instruments enable row level security;

  create policy "public can read instruments"
  on public.instruments
  for select
  to anon
  using (true);

Then fetch it from a Server Component — app/instruments/page.tsx:


    import { createClient } from '@/lib/supabase/server'

    export default async function InstrumentsPage() {
        const supabase = await createClient()
        const { data: instruments, error } = await supabase
            .from('instruments')
            .select()

        if (error) {
            return <p className="p-6 text-red-600">Error: {error.message}</p>
        }

        return (
            <main className="p-6">
                <h1 className="text-2xl font-bold mb-4">Instruments</h1>
                <ul className="space-y-2">
                    {instruments?.map((instrument) => (
                        <li key={instrument.id} className="rounded-lg border border-gray-200 p-3 shadow-sm">
                            {instrument.name}
                        </li>
                    ))}
                </ul>
            </main>
        )
    }

Run npm run dev and visit http://localhost:3000/instruments. Seeing violin, viola, cello confirms your Next.js app, environment keys, and Supabase client are all wired correctly.


Next up — Post 1.5: Supabase CLI basics — running Supabase locally, linking your project, and why local development matters before you touch production data.

No comments:

Post a Comment

Post 1.4 — Next.js App Router Setup + Connecting Supabase

This post covers project setup, Supabase package installation, environment variables, and creating separate Supabase clients for the browser...