CtrlK
BlogDocsLog inGet started
Tessl Logo

pleaseai/better-auth

Better Auth authentication framework skills for JavaScript/TypeScript projects

72

Quality

91%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

adapters.mdskills/use-better-auth/references/

Framework Adapters

Per-framework wiring for mounting auth.handler and instantiating the matching client. Verify every import path against $(ask src better-auth)/packages/better-auth/src/integrations/ and $(ask src better-auth)/docs/content/docs/integrations/ before pasting — exports are renamed between minor releases.

Source-level helpers (shipped in packages/better-auth/src/integrations/):

next-js.ts   node.ts   solid-start.ts   svelte-kit.ts
tanstack-start.ts   tanstack-start-solid.ts

All other frameworks (Nuxt, Hono, Astro, Fastify, Elysia, Express, Bun) mount the generic auth.handler directly — no source-level helper is required.


Next.js — App Router

File: app/api/auth/[...all]/route.ts

import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";

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

Enable the cookie bridge so server components read fresh session data:

// lib/auth.ts
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";

export const auth = betterAuth({
  plugins: [nextCookies()], // must be LAST in the plugin array
});

Client:

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

export const authClient = createAuthClient();
export const { signIn, signUp, signOut, useSession } = authClient;

Next.js — Pages Router

File: pages/api/auth/[...all].ts

import { toNodeHandler } from "better-auth/node";
import { auth } from "@/lib/auth";

export const config = { api: { bodyParser: false } };

export default toNodeHandler(auth.handler);

bodyParser: false is required — Next.js's default JSON parser will swallow the auth request body.


Nuxt

File: server/api/auth/[...all].ts

import { auth } from "~~/lib/auth";

export default defineEventHandler((event) => {
  return auth.handler(toWebRequest(event));
});

Client (Nuxt uses the Vue client):

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

export const authClient = createAuthClient();
export const { signIn, signUp, signOut, useSession } = authClient;

Call authClient.useSession(useFetch) from inside a page setup() — passing useFetch is what forwards cookies server-side during SSR.


SvelteKit

File: src/hooks.server.ts

import { auth } from "$lib/auth";
import { svelteKitHandler } from "better-auth/svelte-kit";
import { building } from "$app/environment";

export async function handle({ event, resolve }) {
  return svelteKitHandler({ event, resolve, auth, building });
}

svelteKitHandler does not populate event.locals.session on its own. To expose the session inside +layout.server.ts / actions / endpoints, fetch it in handle:

const session = await auth.api.getSession({ headers: event.request.headers });
if (session) {
  event.locals.session = session.session;
  event.locals.user = session.user;
}

Enable the cookie plugin so form actions / server endpoints persist cookies:

import { sveltekitCookies } from "better-auth/svelte-kit";
betterAuth({ plugins: [sveltekitCookies()] });

Client:

import { createAuthClient } from "better-auth/svelte";
export const authClient = createAuthClient();

TanStack Start

File: src/routes/api/auth/$.ts

import { auth } from "@/lib/auth";
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/api/auth/$")({
  server: {
    handlers: {
      GET:  async ({ request }: { request: Request }) => auth.handler(request),
      POST: async ({ request }: { request: Request }) => auth.handler(request),
    },
  },
});

Add the cookie plugin (must be the last plugin):

// React variant
import { tanstackStartCookies } from "better-auth/tanstack-start";
// Solid variant
import { tanstackStartCookies } from "better-auth/tanstack-start-solid";

betterAuth({ plugins: [tanstackStartCookies()] });

Prefer the authClient for sign-in flows over calling auth.api.* from server functions — the client handles cookie propagation via the plugin.


Solid Start

File: src/routes/api/auth/[...auth].ts

import { auth } from "~/lib/auth";
import { toSolidStartHandler } from "better-auth/solid-start";

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

Client:

import { createAuthClient } from "better-auth/solid";
export const authClient = createAuthClient();

Hono

import { Hono } from "hono";
import { cors } from "hono/cors";
import { auth } from "./auth";

const app = new Hono();

app.use(
  "/api/auth/*",
  cors({
    origin: "http://localhost:3001",
    allowHeaders: ["Content-Type", "Authorization"],
    allowMethods: ["POST", "GET", "OPTIONS"],
    exposeHeaders: ["Content-Length"],
    maxAge: 600,
    credentials: true, // required so cookies survive cross-origin
  }),
);

app.on(["POST", "GET"], "/api/auth/*", (c) => auth.handler(c.req.raw));

Without credentials: true on CORS and credentials: "include" on the client fetch, the session cookie will not round-trip.


Astro

File: src/pages/api/auth/[...all].ts

import type { APIRoute } from "astro";
import { auth } from "~/auth";

export const ALL: APIRoute = async (ctx) => {
  // For rate-limiting: ctx.request.headers.set("x-forwarded-for", ctx.clientAddress);
  return auth.handler(ctx.request);
};

Client: pick the import that matches the island framework — better-auth/react, better-auth/vue, better-auth/svelte, better-auth/solid, or better-auth/client for vanilla.


Express

import express from "express";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./auth";

const app = express();

// Express 4:
app.all("/api/auth/*", toNodeHandler(auth));
// Express 5:
// app.all("/api/auth/*splat", toNodeHandler(auth));

// Mount express.json() AFTER auth — never before, or the client will hang on "pending".
app.use(express.json());

CommonJS is not supported — set "type": "module" in package.json, or use a tsconfig "module" setting that emits ESM.


Bun

import { auth } from "./auth";

Bun.serve({
  port: 3000,
  fetch: (req) => {
    if (new URL(req.url).pathname.startsWith("/api/auth")) return auth.handler(req);
    return new Response("OK");
  },
});

Bun's fetch signature matches the Web Request / Response shape auth.handler expects — no adapter needed.


Nitro (standalone)

Nitro uses the same pattern as Nuxt:

// routes/api/auth/[...all].ts
import { auth } from "~/lib/auth";

export default defineEventHandler((event) => auth.handler(toWebRequest(event)));

CORS & cookie invariants (every framework)

  • Server response Access-Control-Allow-Origin must be the specific origin — not * — when credentials: "include" is used client-side.
  • Set cookies with sameSite: "lax" (or "none" + secure: true for cross-site) via advanced.defaultCookieAttributes in betterAuth({}).
  • Behind a reverse proxy, trust the forwarded host headers so callback URLs resolve correctly (trustedOrigins option).

See references/common-errors.md for concrete cookie/CORS misconfig symptoms.

README.md

tile.json