Where to Find Your Supabase URL (and What Breaks If It's Wrong)
The Project URL, the dashboard URL and the connection string look alike. Four files read it here, one accepts the wrong value silently, and an unset one errors every page.
Your Supabase project URL is https://<project-ref>.supabase.co, and the dashboard shows it on the project's API settings page — the same address @supabase/ssr prints in its own error message: supabase.com/dashboard/project/_/settings/api. Copy it into NEXT_PUBLIC_SUPABASE_URL. The project ref is the first label of the hostname, and it is also what names your session cookie.
The lookup takes ten seconds. What goes wrong afterwards is the real subject here: this storefront reads that one variable in five places, the client library checks it far less than you would assume, and a missing value does not fail one page — it fails every request.
The three values people confuse
Supabase shows several URL-shaped strings, and pasting the wrong one produces errors that do not mention the URL at all.
| Value | Looks like | Goes in | Used for |
|---|---|---|---|
| Project URL | https://abcdefghij.supabase.co | NEXT_PUBLIC_SUPABASE_URL | The REST, Auth and Storage API — what supabase-js talks to |
| Dashboard URL | https://supabase.com/dashboard/project/abcdefghij | Nowhere | It is the address bar of your browser, not an API |
| Database connection string | postgresql://postgres:…@db.abcdefghij.supabase.co:5432/postgres | DATABASE_URL, if you use a Postgres driver or ORM | Direct SQL, not supabase-js |
The Project URL and the dashboard URL share the project ref (abcdefghij above), which is why people grab the wrong one from the browser bar. The project ref is the identifier; the Project URL is that ref plus .supabase.co.
Where this repo puts it
.env.example documents exactly three Supabase variables:
# .env.example
NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-or-publishable-key
# SECRET — server-only, bypasses RLS. Get it from the Supabase dashboard:
SUPABASE_SERVICE_ROLE_KEY=your-service-role-secret
The URL and the anon key carry the NEXT_PUBLIC_ prefix because the browser client needs both; Next.js inlines them into the bundle at build time. They are safe to expose — the anon key does nothing the Row Level Security policies do not allow. The service role key is the opposite case and is covered in the service role key post.
Five places read the URL. Four are Supabase clients and read it identically:
| File | Client it builds | Runs |
|---|---|---|
src/lib/supabase/client.ts | createBrowserClient | Browser |
src/lib/supabase/server.ts | createServerClient bound to request cookies | Server Components, Route Handlers, Server Actions |
src/lib/supabase/admin.ts | Service-role client | Server only |
src/proxy.ts | createServerClient for session refresh | Every matching request |
The fifth, next.config.ts, is covered below. The browser client is as short as it looks:
// src/lib/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";
/** Browser Supabase client (anon key). Use in Client Components. */
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
}
Note the trailing !. It tells TypeScript the value exists; it checks nothing at runtime. If you would rather the app fail with a sentence you wrote than with a library's, validate once at startup instead of sprinkling assertions across four files.
What the library checks (measured)
We constructed a server client against @supabase/ssr 0.12.0 with four different URLs and recorded what happened:
| URL passed | Result |
|---|---|
undefined | Throws: Your project's URL and Key are required to create a Supabase client! |
"" (empty string) | Throws the same error |
https://supabase.com/dashboard/project/abcd (the dashboard URL) | Constructs without error |
https://abcdefghij.supabase.co | Constructs without error |
Two things follow. A missing variable is caught immediately, and the error even carries the dashboard address of the settings page to fix it. But a wrong variable is not caught at all: the constructor accepts the dashboard URL, and the failure arrives later, at the first network call, as a failed fetch or a 404 that never names the variable. If auth "does nothing" after a fresh setup, compare the value against the table above before debugging anything else.
The missing-variable failure is site-wide here
src/proxy.ts is the Next 16 replacement for middleware.ts, and its job is to refresh the Supabase session on every request:
// src/proxy.ts (abridged)
export async function proxy(request: NextRequest) {
let response = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{ cookies: { /* getAll / setAll */ } },
);
await supabase.auth.getClaims();
return response;
}
Its matcher excludes only static assets, images, sitemap.xml, robots.txt and the Search Console verification file. Everything else — the homepage, /pricing, /blog — passes through that constructor first. So an unset URL does not break the login page; it throws on essentially every page view.
This is a defect this project actually carries a warning for. Production served an earlier build with no Supabase code; the commerce phase was merged to main before the variables were set on the host, and the repo's CLAUDE.md records that deploying it as-is would error site-wide. The ordering rule it implies is simple: set the environment variables on the host before the first deploy that contains the proxy, not after.
Because NEXT_PUBLIC_ values are inlined when next build runs, setting them on Vercel after a build exists changes nothing for that build. Redeploy. The prefix rules and the build-time inlining are explained in Next.js environment variables.
The fifth reader: your Content-Security-Policy
next.config.ts builds the CSP connect-src directive from the same variable, so the policy is correct per environment:
// next.config.ts
const supabaseOrigin = (() => {
try {
return new URL(process.env.NEXT_PUBLIC_SUPABASE_URL!).origin;
} catch {
return "https:";
}
})();
This changes what the two failure modes look like. An unset URL does not crash the build — new URL(undefined) throws, the catch swallows it, and connect-src quietly widens to every https: host. An unparseable URL does the same. A dashboard URL, though, parses fine: its origin is https://supabase.com, so the policy allows requests to supabase.com and the browser then blocks every call to <ref>.supabase.co with a CSP violation. If the console shows a connect-src error naming your Supabase host, the policy was built from the wrong value — check the variable and rebuild, since the header is computed at build time too.
The project ref is also your cookie name
The URL has a second job you never configure: it names the session cookie. In supabase-js 2.110.0 the storage key is built from the first label of the hostname — sb-${hostname.split(".")[0]}-auth-token — and @supabase/ssr stores the session under that name, chunked into .0, .1 and so on when the token is large.
This storefront relies on it. src/lib/supabase/lazyClient.ts keeps the roughly 68 KiB auth stack off every page by first probing for the cookie:
// src/lib/supabase/lazyClient.ts
export function hasAuthCookie(): boolean {
if (typeof document === "undefined") return false;
return /(?:^|;\s*)sb-.+?-auth-token/.test(document.cookie);
}
A visitor with no sb-…-auth-token cookie is signed out, so the library is never downloaded. The regex deliberately does not hard-code the ref, which means changing NEXT_PUBLIC_SUPABASE_URL to a different project does not break the probe — but it does orphan every existing session, because the old cookie name no longer matches what the client looks for. Treat a URL change as a forced sign-out.
Local development
For supabase start, the CLI prints an API URL (a 127.0.0.1 address with a port) and local keys when the stack boots. Put those in .env.local, and keep the hosted values in your deployment environment. The two never share a file, which is the whole point: a stray local URL committed to production is a quick way to produce the previous section's outage. Local stack setup is in Supabase local development.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Every page errors; Your project's URL and Key are required | Variable unset or empty on the host | Set it, then redeploy — it is inlined at build time |
| Works locally, fails on Vercel | .env.local is not deployed | Add the variable in the host's project settings for the right environment |
Auth silently does nothing; network tab shows 404s or a CSP connect-src violation | Dashboard URL pasted instead of the Project URL | Use https://<ref>.supabase.co, then rebuild |
Failed to fetch on every call | Typo in the ref, or a trailing path on the URL | Remove anything after .supabase.co |
| Everyone signed out after a deploy | URL points at a different project, so the cookie name changed | Expected; restore the original URL or accept the reset |
| Postgres driver cannot connect | Project URL used where the connection string belongs | Use the postgresql:// string from the database settings |
Frequently asked questions
Is the Supabase project URL a secret?
No. It is sent by every browser that loads your app, which is why it carries the NEXT_PUBLIC_ prefix. Protection comes from Row Level Security and from keeping the service role key off the client, not from hiding the URL.
Is the project URL the same as the project ID?
Not quite. The project ref (or ID) is the identifier, such as abcdefghij. The URL is that ref plus .supabase.co. You need the full URL for createClient.
Why does my Next.js app not see the variable I just added?
NEXT_PUBLIC_ values are inlined during next build. Restart the dev server locally, or redeploy on the host. Variables without the prefix are not available in the browser at all.
Can I use a custom domain instead of the supabase.co URL?
If you put a custom domain in front of the project, the value in NEXT_PUBLIC_SUPABASE_URL is the custom domain, and the cookie name is derived from its first hostname label rather than the project ref.
Templates in this post
ASoc Surge is a neural-network startup marketing site with tabbed capabilities and two-tier pricing, ASoc Synth is an AI-workspace landing page with a feature toolkit and tiered pricing, and ASoc Tempo is a time-tracking SaaS page with a live timer preview. All three are Next.js + Tailwind landing pages that stay static until you add a backend like Supabase — so there is no proxy to misconfigure on day one.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
