Search docs

Jump between documentation pages.

Browse docs

Use Better Auth with DaloyJS

Better Auth is a TypeScript authentication framework you host in your own application. Unlike Auth0, Okta, Clerk, or LoginRadius, it is not only a hosted identity provider integration. Your app owns the auth tables, the session cookies, and the auth endpoints.

Better Auth already documents Hono, Elysia, and Fastify adapters. DaloyJS does not need a special adapter because both libraries meet at the Web-standard boundary: Better Auth exposes auth.handler(request)and DaloyJS gives every route and hook the original Request.

Better Auth inside a DaloyJS app
BrowserDaloyJSBetter AuthDatabase
  1. 01requestBrowserDaloyJSPOST /api/auth/sign-in/emailcredentials, OAuth callbacks, session actions
  2. 02asyncDaloyJSBetter Authauth.handler(request)mounted under /api/auth/*
  3. 03asyncBetter AuthDatabaseusers, accounts, sessions
  4. 04responseBetter AuthBrowserResponse with Set-Cookie
  5. 05requestBrowserDaloyJSGET /me with session cookie
  6. 06asyncDaloyJSBetter Authauth.api.getSession({ headers })
The auth endpoints are Better Auth's own Request to Response handler. Normal DaloyJS API routes read the current session from request headers and enforce application authorization.

1. Install

ts
pnpm add better-auth

2. Create the auth instance

Configure Better Auth once and export the instance. Use the database adapter that matches your app. The example below keeps the database placeholder explicit because production apps should not copy a toy in-memory store into auth.

ts
// src/auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  baseURL: process.env.BETTER_AUTH_URL!,
  secret: process.env.BETTER_AUTH_SECRET!,
  trustedOrigins: [
    "http://localhost:3000",
    "https://app.example.com",
  ],
  emailAndPassword: {
    enabled: true,
  },
  // Pick the adapter for your database:
  // database: prismaAdapter(prisma, { provider: "postgresql" }),
  // database: drizzleAdapter(db, { provider: "pg" }),
});

3. Environment variables

ts
# .env
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=replace-with-at-least-32-random-bytes

4. Mount Better Auth routes

Better Auth owns all routes below /api/auth/*. Return the raw Response from a beforeHandle hook so cookies, redirects, status codes, and multiple Set-Cookie headers are preserved exactly.

ts
// src/routes/auth.ts
import { App } from "@daloyjs/core";
import { auth } from "../auth";

const app = new App();

const betterAuthHook = {
  beforeHandle: ({ request }: { request: Request }) => auth.handler(request),
};

app.route({
  method: "GET",
  path: "/api/auth/*path",
  operationId: "betterAuthGet",
  summary: "Better Auth GET endpoint",
  responses: {
    200: { description: "Handled by Better Auth" },
    302: { description: "Redirect" },
    400: { description: "Bad Request" },
    401: { description: "Unauthorized" },
  },
  hooks: betterAuthHook,
  handler: () => ({ status: 200, body: null }),
});

app.route({
  method: "POST",
  path: "/api/auth/*path",
  operationId: "betterAuthPost",
  summary: "Better Auth POST endpoint",
  responses: {
    200: { description: "Handled by Better Auth" },
    201: { description: "Created" },
    204: { description: "No Content" },
    400: { description: "Bad Request" },
    401: { description: "Unauthorized" },
  },
  hooks: betterAuthHook,
  handler: () => ({ status: 200, body: null }),
});

5. Protect DaloyJS routes

Use auth.api.getSession({ headers }) inside middleware. This keeps normal DaloyJS routes contract-first while Better Auth owns the session lookup.

ts
// src/plugins/better-auth.ts
import type { Middleware } from "@daloyjs/core";
import { auth } from "../auth";

export type BetterAuthSession = Awaited<
  ReturnType<typeof auth.api.getSession>
>;

export function requireBetterAuth(): Middleware {
  return async (ctx, next) => {
    const session = await auth.api.getSession({
      headers: ctx.request.headers,
    });

    if (!session) {
      return ctx.problem(401, "unauthorized", "Missing or expired session");
    }

    ctx.state.session = session;
    return next();
  };
}

declare module "@daloyjs/core" {
  interface AppState {
    session?: NonNullable<BetterAuthSession>;
  }
}
ts
import { z } from "zod";
import { App, secureHeaders, rateLimit } from "@daloyjs/core";
import { requireBetterAuth } from "./plugins/better-auth";

const app = new App();
app.use(secureHeaders());
app.use(rateLimit({ windowMs: 60_000, max: 100 }));

app.route({
  method: "GET",
  path: "/me",
  operationId: "getMe",
  middleware: [requireBetterAuth()],
  responses: {
    200: {
      description: "OK",
      body: z.object({
        userId: z.string(),
        email: z.string().email(),
      }),
    },
  },
  handler: ({ state }) => ({
    status: 200,
    body: {
      userId: state.session!.user.id,
      email: state.session!.user.email,
    },
  }),
});

Client usage

Browser apps use Better Auth's client. Point baseURL at the same origin or public API origin that serves your DaloyJS app.

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

export const authClient = createAuthClient({
  baseURL: "http://localhost:3000",
});

await authClient.signIn.email({
  email: "ada@example.com",
  password: "correct horse battery staple",
});

Runtime fit

RuntimeFitNotes
Node.jsRecommendedBest default for database-backed sessions and OAuth callbacks.
Bun / DenoDepends on adapterUse only with database drivers tested on that runtime.
Cloudflare WorkersDepends on adapterThe auth handler is Web-standard, but your database adapter must also work on Workers.
VercelYesUse Node functions unless every selected adapter is edge-safe.
AWS LambdaYesUse pooled or serverless database access.

Security notes

Secure deployment checklist
  1. 01configSecretBETTER_AUTH_SECRET from a real secret manager
  2. 02OrigintrustedOrigins pins browser origins
  3. 03Cookiespreserve raw Response from auth.handler
  4. 04Proxydeclare TRUST_PROXY_HOPS behind a platform edge
  5. 05Databasemigrate auth tables before traffic
Better Auth is part of your deployed app, so the auth route needs the same production posture as the rest of the API: secure secrets, trusted origins, proxy-aware URLs, preserved cookies, and database migrations.
  • Generate a strong BETTER_AUTH_SECRET and rotate it with the same care as a JWT signing key.
  • Keep trustedOrigins narrow. Do not allow arbitrary origins in production.
  • Preserve Better Auth's raw Response for auth endpoints. Rebuilding headers into a plain object can collapse multiple Set-Cookie headers.
  • When deployed behind Railway, Render, Fly.io, Vercel, Cloudflare, or another edge proxy, configure DaloyJS's proxy posture so generated URLs, cookies, rate limiting, and audit logs use the expected origin and client IP.
  • Put rateLimit() in front of sign-in, sign-up, password reset, and callback routes. Better Auth handles auth logic, but the API still needs abuse controls.

See also the auth integrations overview, Better Auth installation, Better Auth basic usage, and the framework integration docs for Hono, Elysia, and Fastify.