rhythmjs

Search documentation

Search guides, the tutorial and every package.

On this page

Better Auth is framework-agnostic: its handler takes a standard Request and returns a Response. @rhythmjs/better-auth mounts it on a Rhythm app and gives you session middleware. Create your auth instance as Better Auth's docs describe, and import it here.

Install#

Shell
bun add better-auth @rhythmjs/better-auth

@rhythmjs/http and @rhythmjs/security come with the package.

Mount the handler#

Register betterAuthModule.forRoot({ auth }). It mounts auth.handler at path (default: the instance's basePath, else /api/auth) and provides auth. Nothing reaches the context until you export it in the register callback, so export auth for the middleware below.

TypeScript
// src/app.module.ts
import { betterAuthModule } from "@rhythmjs/better-auth";
import { Rhythm } from "@rhythmjs/rhythm";
import type { RhythmHttpContext } from "@rhythmjs/router/adapters/context";
import { auth } from "./lib/auth";

export const appModule = new Rhythm<RhythmHttpContext>({ name: "app", type: "module" }).register(
  betterAuthModule.forRoot({ auth, path: "/api/auth" }),
  (m) => ({ auth: m.auth }),
);

Under the hood the module uses mount. Without the module, authHandler({ auth }) is the bare mounting middleware; provide ctx.auth yourself in that case.

CORS#

For a frontend on another origin, use the cors exported by @rhythmjs/better-auth (it is cors from @rhythmjs/security/cors, unchanged). Put it before the module, so preflights are answered and Better Auth's own responses carry the headers. Cookie sessions need credentials: true, and the origin must also be in Better Auth's trustedOrigins: Better Auth checks origins separately from CORS.

TypeScript
import { betterAuthModule, cors } from "@rhythmjs/better-auth";

export const appModule = new Rhythm<RhythmHttpContext>({ name: "app", type: "module" })
  .use(cors({ origin: "http://localhost:3001", credentials: true, allowHeaders: ["Content-Type", "Authorization"] }))
  .register(betterAuthModule.forRoot({ auth, path: "/api/auth" }), (m) => ({ auth: m.auth }));

Session middleware#

Both read ctx.auth, so they go after the register above.

  • withSession() puts session and user on the context, both null when anonymous.
  • requireSession() answers 401 when there is no session, and its type tells the handler that ctx.session and ctx.user are set.
TypeScript
import { requireSession, withSession } from "@rhythmjs/better-auth";

export const appModule = new Rhythm<RhythmHttpContext>({ name: "app", type: "module" })
  .use(cors({ origin: "http://localhost:3001", credentials: true }))
  .register(betterAuthModule.forRoot({ auth, path: "/api/auth" }), (m) => ({ auth: m.auth }))
  .use(withSession())
  .use(appController.middleware());

Guard a route with requireSession(). Its context type needs AuthContext and SessionContext, both exported by the package. Put the guard on the route: a router-level guard narrows the router's context, and that router would then not type-check when mounted.

TypeScript
// src/app.controller.ts
import { requireSession, type AuthContext, type SessionContext } from "@rhythmjs/better-auth";
import { RhythmRouter } from "@rhythmjs/router";
import type { RhythmHttpContext } from "@rhythmjs/router/adapters/context";

export type AppContext = RhythmHttpContext & AuthContext & SessionContext;

export const appController = new RhythmRouter<AppContext>().get("/me", requireSession(), (ctx) => {
  ctx.json({ id: ctx.user.id, name: ctx.user.name, email: ctx.user.email });
});

session and user have Better Auth's base types. For plugin or custom fields, call ctx.auth.api.getSession({ headers: ctx.request.headers }) yourself: it is typed from your instance. For roles, check ctx.user.role in a guard of your own.

Things to check#

  • Better Auth rate-limits its own endpoints; Rhythm middleware does not see requests that the module answers.
  • Set BETTER_AUTH_SECRET and BETTER_AUTH_URL outside local runs.

The full app, with tests, is examples/better-auth, built from the template.