All skills
neondatabase avatar

/neon-auth

@9fd97c5 official

Add authentication to a new app. Use for "add auth", "add login", Neon Auth (Managed Better Auth), identity routing, sign-up, sign-in, password reset, email OTP, magic links, organizations, phone OTP, OAuth, passkeys, MFA, trusted domains, invalid domain, and @neondatabase/auth. No existing identity: default to Managed Better Auth. Keep working Better Auth, Clerk, Supabase Auth, or another IdP. User asked to migrate from Supabase Auth: Managed Better Auth. A required plugin outside Managed support: self-managed Better Auth on a Neon Function or the existing app host. Also use for auth APIs in @neondatabase/neon-js.

Use this Skill: https://skilld.dev/gh/neondatabase/agent-skills/neon-auth

This session only. Nothing lands on disk.

referencesmanaged-auth.md

≈2.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Managed Better Auth: implement login

Enabling auth: true is not implementing login. Follow the matching live quickstart, then verify sign-up, sign-in, sign-out, session, and a protected route.

Keep Clerk, existing Better Auth, Supabase Auth, or another working provider. Do not migrate it unless the user asks. The SKILL.md Supabase case is that explicit login-migration request.

Framework-specific companion skills also live in neondatabase/neon-js (neon-auth-nextjs, neon-auth-react, neon-js-react). Prefer the live Neon guides above; do not copy those SDK-local files into this repo.

Auth emails

Managed Auth sends verification, email OTP, magic-link, and password-reset messages. Getting started uses the shared SMTP provider (auth@mail.myneon.app). Implementing login does not require Resend, SendGrid, or other application email code.

Production requires custom SMTP. Verification codes work on shared or custom SMTP. Verification links require custom SMTP. Checklist: https://neon.com/docs/auth/production-checklist.md. Branding and webhook delivery: https://neon.com/docs/auth/guides/customize-emails.md.

SMS is separate: the Phone Number plugin needs an application send.otp webhook.

Packages

Need Package
Auth only @neondatabase/auth
Already using the combined SDK @neondatabase/neon-js/auth re-export
Pre-built UI @neondatabase/auth-ui

Keep an existing SupabaseAuthAdapter() caller on that API (signInWithPassword, signInWithOAuth). Do not mix those methods into default Better Auth examples. Password hashes do not migrate from Supabase; updateUser() cannot change email or password; email verification needs app UI. Guide: https://neon.com/docs/auth/migrate/from-supabase.md

The Managed client is Better Auth methods through Neon's wrapper. It is not interchangeable with bare better-auth/client while Auth is managed: the wrapper rejects extra plugins and implements Neon OAuth verifier / iframe popup / JWT extraction.

Environment

Variable Purpose
NEON_AUTH_BASE_URL Branch Managed Auth URL (includes path). Next server; injected into Functions.
NEON_AUTH_COOKIE_SECRET Next app secret for cached session cookies. Generate with openssl rand -base64 32 (32+ characters). Not injected by Neon.
VITE_NEON_AUTH_URL Public Auth URL for Vite / TanStack browser code. Assign the actual branch URL; env pull does not create this alias.
NEON_AUTH_JWKS_URL Injected Functions JWKS. Verify tokens in neon-functions, not here.

neon env pull / neon deploy write Managed NEON_AUTH_BASE_URL and NEON_AUTH_JWKS_URL when Auth is declared. The cookie secret and VITE_* name are application config.

Next.js

createNeonAuth from @neondatabase/auth/next/server. Optional Next peer on current @neondatabase/auth is >=16.0.0; check the installed package before changing an existing app's router file.

import { createNeonAuth } from "@neondatabase/auth/next/server";

export const auth = createNeonAuth({
  baseUrl: process.env.NEON_AUTH_BASE_URL!,
  cookies: { secret: process.env.NEON_AUTH_COOKIE_SECRET! },
});

app/api/auth/[...path]/route.ts:

import { auth } from "@/lib/auth/server";

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

Those five methods are what the installed SDK returns. Existing apps that export only GET/POST keep serving GET/POST routes.

Browser client takes no arguments and talks to that same-origin proxy:

import { createAuthClient } from "@neondatabase/auth/next";

export const authClient = createAuthClient();

Protect routes with auth.middleware({ loginUrl: "/auth/sign-in" }) from proxy.ts on Next 16. Earlier Next apps may still use middleware.ts; match the installed SDK. Always set config.matcher to the protected pages. A matcher that covers every path redirects JavaScript and CSS for unauthenticated visitors, so the login page cannot load:

import { auth } from "@/lib/auth/server";

export default auth.middleware({ loginUrl: "/auth/sign-in" });

export const config = {
  matcher: ["/account/:path*"],
};

Replace /account/:path* with the app's protected routes. Keep login, registration, recovery, /api/auth, and static assets accessible without a session.

Before reading protected data or performing a mutation, check the session inside the Route Handler or Server Action and enforce the resource's authorization rules. Verify direct unauthenticated requests are denied, independently of page redirects:

const { data: session } = await auth.getSession();
if (!session?.user) {
  return Response.json({ error: "Unauthorized" }, { status: 401 });
}

Server session:

const { data: session, error } = await auth.getSession();
const user = session?.user;

Do not destructure { user } from the top-level result. Do not pass options into createAuthClient() from /next. Do not put fetchOptions on the Managed createAuthClient URL-style config; adapter factories accept fetch options inside BetterAuthReactAdapter({ fetchOptions }) / BetterAuthVanillaAdapter(...).

JWT: const { data, error } = await auth.token(); then data.token. Do not call getJWTToken() on the public client.

Phone OTP: the browser client exposes phoneNumber. Existing users link a number, then sign in; there is no phone-first signup. Next.js auth.handler() forwards the catch-all path to Managed Auth, including phone OTP. A missing auth.phoneNumber server method is a missing typed helper, not a proxy rejection. SMS e2e needs a configured send.otp webhook and custom UI.

Organization invitations

organization.inviteMember() does not send email unless send_invitation_email is on (default false) and "Verify email at signup" is enabled. Accepting an emailed invite needs /auth/accept-invitation?invitationId= via AuthView or a custom flow that signs the recipient in and calls organization.acceptInvitation({ invitationId }). If email delivery stays off, use an in-app invitation list. https://neon.com/docs/auth/guides/plugins/organization.md

React / Vite

import { createAuthClient } from "@neondatabase/auth";
import { BetterAuthReactAdapter } from "@neondatabase/auth/react/adapters";

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

Call adapter factories with (). Omit the adapter for vanilla Better Auth methods without useSession.

Public calls: signUp.email({ email, password, name }), signIn.email({ email, password }), signIn.social({ provider, callbackURL }), getSession(), signOut(). Result shape is { data, error }; user is data.user. Handle error and thrown HTTP errors, pending UI, and authenticated vs unauthenticated display.

JWT: authClient.token() then data.token.

UI

import "@neondatabase/auth-ui/css";
import { NeonAuthUIProvider, AuthView } from "@neondatabase/auth-ui";

Choose one CSS import: /css or /tailwind, never both. Current @neondatabase/auth-ui uses <AuthView path={path} />. Check installed types before copying a pathname example from older docs.

UI flags (emailOTP, magicLink, social providers, organization) do not enable the Managed plugin. Configure the plugin on the branch, then the UI.

Preserve existing @neondatabase/auth/react/ui imports rather than forcing a drive-by migration. New snippets use @neondatabase/auth-ui.

Cross-subdomain vs bearer JWT

cookies.domain shares session cookies across subdomains of one parent domain (see the neon-js cross-domain-cookies example). That is not cookie sharing across unrelated frontend and backend hosts. A Neon Function authenticates with Authorization: Bearer. With Managed Auth, verify against the injected JWKS. With another identity, use that identity's token contract — see neon-functions. https://neon.com/docs/compute/functions/authentication.md and https://neon.com/docs/auth/guides/plugins/jwt.md.

Data API identity

Only when the app already uses PostgREST or a Supabase database client:

import { defineConfig } from "@neon/config/v1";

export default defineConfig({ auth: true, dataApi: true });

Existing external IdP:

import { defineConfig } from "@neon/config/v1";

export default defineConfig({
  dataApi: {
    authProvider: "external",
    jwksUrl: "https://your-idp/.well-known/jwks.json",
  },
});

Do not enable Auth merely to satisfy a dataApi type error in an app that never needed the Data API. External JWKS on a Claimable project is accepted only after claim. Combined SDK createClient({ dataApi: { url, getToken } }) is a query client without .auth; confirm the installed @neondatabase/neon-js docs before introducing it.

Source: SKILL.md on GitHub

No alerts13d3 checks · Risk SAFE
  • Gen Agent Trust Hub13d

    The skill provides comprehensive instructions for implementing authentication using Neon Auth (Managed Better Auth). It follows security best practices, including secret management and origin validation, and relies on official vendor packages and documentation.

  • Socket13d

    No alerts

  • Snyk13d

    Risk: LOW · No issues

Signed by skilld at 9fd97c5. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last week.

Activeupdated 2 weeks ago
Other metadata
metadata
{
  "parent": "neon",
  "source": "https://github.com/neondatabase/agent-skills/tree/main/skills/neon-auth"
}

README badge

README badge for neondatabase/agent-skills/neon-auth