Skip to main content
Neon Postgres Docs
current

Search documentation

Type to search this documentation.

On this pageOverview

Migrate to Managed Better Auth

This guide shows you the code differences between legacy Neon Auth (Stack Auth) and Managed Better Auth. Use it as a reference to understand what changes if you decide to upgrade. If you're using lega...

This guide shows you the code differences between legacy Neon Auth (Stack Auth) and Managed Better Auth. Use it as a reference to understand what changes if you decide to upgrade.

  • Native Branching Support

    Authentication branches automatically with your database. Each branch gets isolated users, sessions, and auth configuration, perfect for preview environments and testing.

  • Database as Source of Truth

    Your Neon database is the single source of truth for authentication data. No webhooks, no sync delays, no external dependencies. Query users directly with SQL.

  • Simplified Configuration

    One environment variable instead of four. Easier setup, fewer moving parts.

  • Open-Source Foundation

    Built on Better Auth, enabling faster development of new features and better community support.

Update your environment variables to use Better Auth's configuration.

.env (before - Stack Auth)
NEXT_PUBLIC_STACK_PROJECT_ID=your-project-id
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=your-client-key
STACK_SECRET_SERVER_KEY=your-server-secret
.env (after - Better Auth)
NEON_AUTH_BASE_URL=https://ep-xxx.neonauth.us-east-2.aws.neon.build/neondb/auth
NEON_AUTH_COOKIE_SECRET=your-secret-at-least-32-characters-long

You can find your Auth URL in the Neon Console under Auth → Configuration.

What changed
You replace multiple Stack Auth-specific keys with a single Better Auth URL that points at your Neon project. For Next.js, you also need a cookie secret for session caching.

Uninstall Stack Auth packages and install @neondatabase/auth

Bash
npm uninstall @stackframe/stack
npm install @neondatabase/auth@latest @neondatabase/auth-ui

What changed
Your app now depends on Managed Better Auth's Next.js SDK and UI package instead of the Stack Auth SDK.

Before (Stack Auth)
// stack.ts
import { StackServerApp } from '@stackframe/stack';

export const stackServerApp = new StackServerApp({
  tokenStore: 'nextjs-cookie',
});
After (Better Auth)
// ./lib/auth/client.ts
'use client';
import { createAuthClient } from '@neondatabase/auth/next';

// to use in react client components
export const authClient = createAuthClient();

// ./lib/auth/server.ts
import { createNeonAuth } from '@neondatabase/auth/next/server';

// to use in react server components, server actions, and API routes
export const auth = createNeonAuth({
  baseUrl: process.env.NEON_AUTH_BASE_URL!,
  cookies: {
    secret: process.env.NEON_AUTH_COOKIE_SECRET!,
  },
});

What changed
You initialize the Managed Better Auth client with createAuthClient for client components and with createNeonAuth() for server-side auth. The unified auth instance provides .handler(), .middleware(), .getSession(), and all Better Auth server methods.

Before (Stack Auth)
import { SignIn } from '@stackframe/stack';

export default function SignInPage() {
  return <SignIn />;
}
After (Better Auth)
import { AuthView } from '@neondatabase/auth-ui';

export default function SignInPage() {
  return <AuthView pathname="sign-in" />;
}

What changed
You render Managed Better Auth's AuthView client component and tell it which flow to show using the pathname prop.

Before (Stack Auth)
import { SignUp } from '@stackframe/stack';

export default function SignUpPage() {
  return <SignUp />;
}
After (Better Auth)
import { AuthView } from '@neondatabase/auth-ui';

export default function SignUpPage() {
  return <AuthView pathname="sign-up" />;
}

What changed
You swap the dedicated <SignUp /> component for the same AuthView component, configured with the "sign-up" pathname.

Before (Stack Auth)
import { UserButton } from '@stackframe/stack';

export function Header() {
  return <UserButton />;
}
After (Better Auth)
import { UserButton } from '@neondatabase/auth-ui';

export function Header() {
  return <UserButton />;
}

What changed
You keep the same UserButton API but import it from the Managed Better Auth UI package and mark the component as client-side.

Before (Stack Auth)
'use client';
import { useUser } from '@stackframe/stack';

export function MyComponent() {
  const user = useUser();
  return <div>{user ? `Hello, ${user.displayName}` : 'Not logged in'}</div>;
}
After (Better Auth)
'use client';
import { useSession } from '@/lib/auth/client';

export function MyComponent() {
  const { data } = useSession();
  const user = data?.user;

  return <div>{user ? `Hello, ${user.name || user.email}` : 'Not logged in'}</div>;
}

What changed
Instead of useUser(), you call useSession() hook from authClient and read the user & session data from response.

Before (Stack Auth)
import { StackProvider, StackTheme } from '@stackframe/stack';
import { stackServerApp } from './stack';

export default function RootLayout({ children }) {
  return (
    <StackProvider app={stackServerApp}>
      <StackTheme>{children}</StackTheme>
    </StackProvider>
  );
}
After (Better Auth)
'use client';
import { NeonAuthUIProvider } from '@neondatabase/auth-ui';
import '@neondatabase/auth-ui/css';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { authClient } from '@/lib/auth/client';

export default function RootLayout({ children }) {
  const router = useRouter();

  return (
    <NeonAuthUIProvider
      authClient={authClient}
      navigate={router.push}
      replace={router.replace}
      onSessionChange={router.refresh}
      Link={Link}
    >
      {children}
    </NeonAuthUIProvider>
  );
}

What changed
You wrap your app in NeonAuthUIProvider, pass it the authClient, and import the Managed Better Auth UI styles.

Before (Stack Auth)
// app/handler/[...stack]/page.tsx
import { StackHandler } from '@stackframe/stack';
import { stackServerApp } from '@/stack';

export default function Handler(props: any) {
  return <StackHandler fullPage app={stackServerApp} {...props} />;
}
After (Better Auth)
// app/api/auth/[...path]/route.ts
import { auth } from '@/lib/auth/server';

export const { GET, POST } = auth.handler();

What changed
You proxy Managed Better Auth APIs from your Next.js application. The auth.handler() method forwards all API requests to the Managed Better Auth server.

Before (Stack Auth)
'use client';
import { useUser } from '@stackframe/stack';

export default function ProtectedPage() {
  const user = useUser({ or: 'redirect' });
  return <div>Protected content</div>;
}
After (Better Auth)
'use client';
import { SignedIn, RedirectToSignIn } from '@neondatabase/auth-ui';

export default function ProtectedPage() {
  return (
    <SignedIn>
      <div>Protected content</div>
      <RedirectToSignIn />
    </SignedIn>
  );
}

What changed
You switch from hook-based redirects to declarative UI helpers that show content only when the user is signed in.

proxy.ts (new)
import { auth } from '@/lib/auth/server';

export default auth.middleware({
  // Redirects unauthenticated users to sign-in page
  loginUrl: '/auth/sign-in',
});

export const config = {
  matcher: [
    // Protected routes requiring authentication
    '/dashboard/:path*',
    '/settings/:path*',

    // Do not run the middleware for the static resources
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

What changed
You can optionally add middleware to enforce auth at the edge for specific paths.

Before (Stack Auth)
import { stackServerApp } from '@/stack';

export default async function ServerComponent() {
  const user = await stackServerApp.getUser();
  return <div>{user?.displayName}</div>;
}
After (Better Auth)
import { auth } from '@/lib/auth/server';

// Server components using auth methods must be rendered dynamically
export const dynamic = 'force-dynamic';

export default async function ServerComponent() {
  const { data: session } = await auth.getSession();
  return <div>{session?.user?.name || session?.user?.email}</div>;
}

What changed
Server components now call auth.getSession() and read the user from the returned session. Components using auth methods must set dynamic = 'force-dynamic'.

Uninstall Stack Auth packages and install @neondatabase/neon-js

Bash
npm uninstall @stackframe/stack
npm install @neondatabase/neon-js@latest @neondatabase/auth-ui

What changed
You use the framework-agnostic Neon JS SDK plus the shared UI package instead of the Stack Auth client SDK.

Before (Stack Auth)
// src/stack.ts
import { StackClientApp } from '@stackframe/stack';

export const stackClientApp = new StackClientApp({
  urls: {
    signIn: '/sign-in',
    signUp: '/sign-up',
  },
});
After (Better Auth)
// src/auth.ts
import { createAuthClient } from '@neondatabase/neon-js/auth';
import { BetterAuthReactAdapter } from '@neondatabase/neon-js/auth/react/adapters';

export const authClient = createAuthClient(import.meta.env.VITE_NEON_AUTH_URL, {
  adapter: BetterAuthReactAdapter(),
});
const { useSession } = authClient;

What changed
You replace the Stack Auth client app with a Managed Better Auth authClient wired to your Managed Better Auth URL.

Components are the same as Next.js. Use <AuthView>, <UserButton>, <SignedIn>, and <SignedOut> from @neondatabase/auth-ui.

What changed
The UI building blocks are shared across frameworks, so you can reuse the same auth components in SPAs.

Before (Stack Auth)
import { useUser } from '@stackframe/stack';

export function MyComponent() {
  const user = useUser();
  return <div>{user ? `Hello, ${user.displayName}` : 'Not logged in'}</div>;
}
After (Better Auth)
import { useSession } from './auth';

export function MyComponent() {
  const { data } = useSession();
  const user = data?.user;

  return <div>{user ? `Hello, ${user.name || user.email}` : 'Not logged in'}</div>;
}

What changed
Instead of a React hook from Stack Auth, you call the useSession() hook from authClient and read the user from its response.

Before (Stack Auth)
import { StackProvider, StackTheme } from '@stackframe/stack';
import { stackClientApp } from './stack';

function App() {
  return (
    <StackProvider app={stackClientApp}>
      <StackTheme>{/* Your app */}</StackTheme>
    </StackProvider>
  );
}
After (Better Auth)
import { NeonAuthUIProvider } from '@neondatabase/auth-ui';
import '@neondatabase/auth-ui/css';
import { authClient } from './auth';

function App() {
  return <NeonAuthUIProvider authClient={authClient}>{/* Your app */}</NeonAuthUIProvider>;
}

What changed
You drop the Stack Auth provider/theme and wrap your app in NeonAuthUIProvider with the Managed Better Auth UI styles.

Delete any StackHandler routes. Create custom pages for sign-in and sign-up using <AuthView>.

src/pages/SignIn.tsx
import { AuthView } from '@neondatabase/auth-ui';

export default function SignIn() {
  return <AuthView pathname="sign-in" />;
}

What changed
Routing is fully controlled by your SPA, and the AuthView component just renders the appropriate view for each path.

If you're using React Router, pass navigation helpers to the provider.

src/App.tsx (React Router)
import { NeonAuthUIProvider } from '@neondatabase/auth-ui';
import { useNavigate, Link } from 'react-router-dom';
import { authClient } from './auth';

function App() {
  const navigate = useNavigate();

  return (
    <NeonAuthUIProvider authClient={authClient} navigate={navigate} Link={Link}>
      {/* Your app */}
    </NeonAuthUIProvider>
  );
}

What changed
You let Better Auth reuse your router's navigation and Link components so redirects and links stay in sync with your SPA.

If you prefer to continue using Stack Auth independently instead of migrating to Better Auth, you can claim your Stack Auth project and manage it directly.

  1. Claim your project via the Neon Console

    1. Go to your project's Auth page, Configuration tab in the Neon Console.
    2. Click Claim project in the Claim project section.
    3. Follow the prompts to select the Stack Auth account that should receive ownership.

    After claiming, you'll have direct access to manage your project in the Stack Auth dashboard.

  2. Update your environment variables

    Once claimed, update your environment variables to use Stack Auth's direct configuration. Your existing code will continue to work without changes since you're already using the Stack Auth SDK (@stackframe/stack).

  3. Manage independently

    After claiming, you can:

    • Manage OAuth providers directly in Stack Auth.
    • Configure production security settings.
    • Access Stack Auth's dashboard and features.
Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu