Test Supabase Auth redirects and OAuth locally
Test Supabase Auth sign-in and OAuth providers on localhost with a Horizon tunnel, and set the Site URL and Redirect URLs once.
Sign in with Supabase Auth on your laptop, with a redirect URL that stays the same across restarts.
Supabase only redirects to URLs on its Redirect URLs list. A stable Horizon URL means you set that list once.
Before you begin
- Node.js 18 or later
- A Horizon account and the CLI (see Getting started)
- A reserved subdomain for
-s. Reserve one on the Subdomains page. - A Supabase project, and a Next.js app that uses
@supabase/ssr - An OAuth provider enabled in your Supabase project. This page uses GitHub.
Start a tunnel
Use -s with a subdomain you reserved. Without it, the subdomain is random and changes every run, so the URLs you save in Supabase would point at a dead URL after a restart. Reserved subdomains are a paid feature, see Pricing.
hrzn tunnel http://localhost:3000 -s my-appHORIZON: Tunnel connected
URL https://my-app.hrzn.run (reserved)
Forwarding http://localhost:3000
Request log https://hrzn.run/dashboard/tunnels/my-appYour public URL is https://my-app.hrzn.run. Keep this terminal open.
The first time you open the URL in a browser, Horizon shows a Before you continue page. Select Continue to site. You see it once every 7 days per IP address, so the redirect back from Supabase won't surprise you later.
Add the redirect URL in Supabase
- Open the URL Configuration page of your project.
- Add
https://my-app.hrzn.run/auth/callbackto Redirect URLs. - Save your changes.
Site URL is the default redirect URL when your code passes no redirectTo. Leave it alone unless you rely on that default. Your code in the next step passes redirectTo, so you don't need to change it.
An OAuth provider doesn't redirect to your app. It redirects to Supabase at https://<project-ref>.supabase.co/auth/v1/callback, and Supabase then redirects to your redirectTo URL. Keep that Supabase URL in the provider's callback setting, for GitHub or Google. Don't put the Horizon URL there.
Set the app environment variables
Install the packages.
npm install @supabase/supabase-js @supabase/ssrSupabase needs no variable for the public URL. The app takes it from the browser and from X-Forwarded-Host, which the later code uses.
NEXT_PUBLIC_SUPABASE_URL=your_project_url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your_publishable_keyCreate the server client, as in Supabase's server-side client guide.
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) {
cookiesToSet.forEach(({ name, value }) =>
cookieStore.set(name, value),
);
},
},
},
);
}Create the browser client.
import { createBrowserClient } from "@supabase/ssr";
export const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
);Create the callback route. Supabase's example redirects to origin in development, and origin is built from the Host header, which Horizon sets to localhost:3000. This version always prefers X-Forwarded-Host, so the redirect lands on the public URL.
import { NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
let next = searchParams.get("next") ?? "/";
if (!next.startsWith("/")) {
next = "/";
}
if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
const forwardedHost = request.headers.get("x-forwarded-host");
const baseUrl = forwardedHost ? `https://${forwardedHost}` : origin;
return NextResponse.redirect(`${baseUrl}${next}`);
}
}
return NextResponse.redirect(`${origin}/auth/auth-code-error`);
}Start sign-in from a client component. window.location.origin is the public URL when you open the app through it.
"use client";
import { supabase } from "@/lib/supabase/client";
export function SignInButton() {
const signIn = () =>
supabase.auth.signInWithOAuth({
provider: "github",
options: { redirectTo: `${window.location.origin}/auth/callback` },
});
return <button onClick={signIn}>Sign in with GitHub</button>;
}Render SignInButton on a page. Start the app on port 3000, and restart it after you edit .env.local.
npm run devSign in through the public URL
Open https://my-app.hrzn.run and select Sign in with GitHub. Start from the public URL so window.location.origin is the Horizon address.
Check it works
The browser goes to the provider, then to Supabase, then to https://my-app.hrzn.run/auth/callback. Your app exchanges the code for a session and redirects to /.
Your Horizon terminal prints a GET line for /auth/callback. A 3xx status means your app redirected you onward. If the line shows a 4xx or 5xx, see Troubleshooting.
Troubleshooting
The sign-in lands on the Site URL, not the callback
Supabase rejects a redirectTo that isn't on the Redirect URLs list. Check that https://my-app.hrzn.run/auth/callback is on the list and matches redirectTo exactly. Supabase supports glob patterns, where * matches any run of characters except . and /, and ** matches any run of characters.
The callback goes to localhost
The route builds its redirect from origin, which comes from the Host header. Horizon sets Host to localhost:3000 and sends the public host in X-Forwarded-Host. Use the route above, which reads X-Forwarded-Host, or build the redirect from a base URL you set yourself.
The provider says the redirect URI doesn't match
The provider's callback setting needs the Supabase URL, https://<project-ref>.supabase.co/auth/v1/callback. It doesn't need the Horizon URL.
The tunnel URL changed
You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-app and the URL stays the same. -s needs a subdomain you reserved, see Pricing.
Nothing reaches your app
- Check that the Horizon terminal is still running. If its last line is
Connection lost. Reconnecting…, wait forReconnected. - Check that your app listens on port 3000.
Next steps
- Read Supabase's guide to redirect URLs.
- Read Supabase's guide to login with GitHub.
Test Okta sign-in locally with an OIDC redirect URI
Test Okta OIDC sign-in on localhost with a Horizon tunnel, and set the sign-in redirect URI on your app integration once.
Test WorkOS AuthKit locally with an HTTPS redirect URI
Test WorkOS AuthKit sign-in on localhost with a Horizon tunnel, using a stable HTTPS redirect URI that WorkOS accepts in a production environment.