Horizon

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.

.env.local
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-secret

Better 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.

lib/auth.ts
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.

app/api/auth/[...all]/route.ts
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.

lib/auth-client.ts
import { createAuthClient } from "better-auth/react";

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_APP_URL,
});

Add a sign-in button.

app/sign-in-button.tsx
"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 dev

Start 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-app
Output
HORIZON: Tunnel connected
  URL          https://my-app.hrzn.run (reserved)
  Forwarding   http://localhost:3000
  Request log  https://hrzn.run/dashboard/tunnels/my-app

Keep 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/github

Register it in the provider. The GitHub guide shows where.

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.

Output
  GET     302  /api/auth/callback/github

To 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_URL holds the public URL, and that you restarted npm run dev after 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 for Reconnected.
  • Check that the app listens on port 3000, the port in your tunnel command.

Next steps

  • Read Better Auth's options reference for baseURL and trustedOrigins, including wildcard patterns.
  • Test the same flow with Auth.js.

On this page