Test Better Auth social login behind a tunnel
Run Better Auth social sign-in on localhost behind a Horizon tunnel, with baseURL, trustedOrigins and the right callback path.
Sign in with Better Auth on your laptop while the provider redirects to a stable HTTPS URL.
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 Next.js App Router app with Better Auth installed (
npm install better-auth) - An OAuth app at a provider. This page uses GitHub. For other providers, see Test OAuth redirect URLs locally.
Set up Better Auth
Better Auth uses baseURL as the root URL of your app. Without it, Better Auth reads BETTER_AUTH_URL, then infers the URL from the request, which its docs discourage. Behind a tunnel the request host is localhost:3000, so set the public URL explicitly.
BETTER_AUTH_SECRET=replace-with-a-secret-of-at-least-32-characters
BETTER_AUTH_URL=https://my-app.hrzn.run
NEXT_PUBLIC_APP_URL=https://my-app.hrzn.run
GITHUB_CLIENT_ID=replace-with-your-client-id
GITHUB_CLIENT_SECRET=replace-with-your-client-secretBetter Auth trusts its base URL by default. List the public URL in trustedOrigins as well if you also open the app on another origin, such as http://localhost:3000.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
baseURL: process.env.BETTER_AUTH_URL,
trustedOrigins: [
"https://my-app.hrzn.run",
"http://localhost:3000",
],
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
});Expose the route handler. The path decides your callback URL.
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);Create the client with the public URL.
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
});Add a sign-in button.
"use client";
import { authClient } from "@/lib/auth-client";
export function SignInButton() {
return (
<button
type="button"
onClick={() =>
authClient.signIn.social({ provider: "github", callbackURL: "/" })
}
>
Sign in with GitHub
</button>
);
}Render <SignInButton /> on a page, then start the app on port 3000.
npm run devStart a tunnel
Use -s with a subdomain you reserved. Without it, the subdomain is random and changes every run, so BETTER_AUTH_URL and the registered redirect URI 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-appKeep this terminal open.
Register the callback URL
By default Better Auth uses /api/auth/callback/<provider name> as the redirect path. For GitHub the redirect URI is:
https://my-app.hrzn.run/api/auth/callback/githubRegister it in the provider. The GitHub guide shows where.
If you set redirectURI on the provider, that value replaces the default path. Register that exact URL instead.
Sign in
Open https://my-app.hrzn.run in your browser. On the first visit, select Continue to site on the Before you continue page. Then select Sign in with GitHub.
Check it works
The provider shows its consent screen. After you approve, your browser lands back on https://my-app.hrzn.run.
Your Horizon terminal prints a line for the callback. The status is a 3xx, because Better Auth redirects after it creates the session.
GET 302 /api/auth/callback/githubTo read the session on the server, call auth.api.getSession({ headers: await headers() }). In a client component, use authClient.useSession().
Troubleshooting
The provider says the redirect URI doesn't match
- Compare the URI registered at the provider with
https://my-app.hrzn.run/api/auth/callback/<provider name>. The scheme, host and path must match. - Check that
BETTER_AUTH_URLholds the public URL, and that you restartednpm run devafter you edited.env.local. Better Auth builds the redirect URI from the base URL. - Check that the subdomain in the provider matches the one you pass to
-s.
Better Auth rejects the request origin
Better Auth trusts its base URL by default. A request from any other origin needs an entry in trustedOrigins. Add the origin your browser shows in the address bar, for example https://my-app.hrzn.run.
The URL changed after a restart
You started the tunnel without -s, so Horizon gave you a new random subdomain. Restart with -s my-app and the registered URI stays valid. -s needs a subdomain you reserved, see Pricing.
The browser shows a Before you continue page
Horizon shows this page once every 7 days per IP address. Select Continue to site.
Nothing reaches your app
- Check that the Horizon terminal is still running. If its last line is
Connection lost. Reconnecting…, wait forReconnected. - Check that the app listens on port 3000, the port in your tunnel command.
Next steps
- Read Better Auth's options reference for
baseURLandtrustedOrigins, including wildcard patterns. - Test the same flow with Auth.js.
Test Auth.js (NextAuth v5) login behind a tunnel
Run Auth.js sign-in on localhost behind a Horizon tunnel, with AUTH_TRUST_HOST, AUTH_URL and the right callback path.
Frameworks
Share a Next.js, Vite, Django, Rails, Laravel, Flask, FastAPI or Express dev server, or a Docker container, through a Horizon tunnel.