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-nextjspackage 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