The idea

The deciding rule (spec 12): storage and execution capabilities go into the engine; interfaces and services are composed around it and stay optional. Auth is a service capability, so it composes — better-auth runs in your process and writes through an adapter, rather than being welded into the engine core.

The adapter (@twilldb/bun/better-auth) translates better-auth's CRUD contract onto the engine's SQL subset. A session check is then a local function call against an in-process database — no network hop, no separate auth server to deploy or scale.

A library, not a bundled service

better-auth is a dependency you add to your app, not part of libengine. The engine has no notion of users or sessions; it only sees rows. Swap better-auth for another library and the engine is unchanged.

Wire it up

Pass the Twill adapter straight to betterAuth({ database }). The adapter creates its tables on first use (one CREATE TABLE per better-auth model, in autocommit), so no migration step is required for the embedded path.

import { betterAuth } from "better-auth";
import { open } from "@twilldb/bun";
import { twillAdapter } from "@twilldb/bun/better-auth";

using db = open("file://./app.db"); // or s3://bucket/app for disaggregated

const auth = betterAuth({
  database: twillAdapter(db),
  emailAndPassword: { enabled: true },
  secret: process.env.BETTER_AUTH_SECRET!,
});

// Sign-up writes the user, account, and session rows into the engine.
await auth.api.signUpEmail({
  body: { email: "[email protected]", password: "correct-horse-battery", name: "Ada" },
});

// Sign-in verifies the hash and mints a session token.
const { token } = await auth.api.signInEmail({
  body: { email: "[email protected]", password: "correct-horse-battery" },
});

The auth state is just rows — you can read it with ordinary SQL. Resolving a session token is the two-step lookup an in-process check performs:

const [{ userId }] = db.query("SELECT userId FROM session WHERE token = ?", [token]);
const [{ name }]   = db.query("SELECT name FROM user WHERE id = ?", [userId]);

A runnable version of this is clients/bun/examples/auth-app.ts (bun run examples/auth-app.ts).

Auth state branches with the database

Because users and sessions are ordinary rows, branching the database branches the auth state. A branch sees the base's committed users (read-through below the fork LSN) but writes in isolation — a sign-up on a branch never leaks back to the base.

using staging = db.branch("staging");
const stagingAuth = betterAuth({ database: twillAdapter(staging), /* …same config… */ });

// A user created on the branch is invisible to the base.
await stagingAuth.api.signUpEmail({
  body: { email: "[email protected]", password: "staging-only", name: "Bel" },
});
// db (base) still shows only [email protected]; staging shows both.

This is the property the composition test asserts (clients/bun/test/better-auth.test.ts): the same database guarantees that protect your rows protect your auth state for free.

How the adapter maps to the SQL subset

Twill DB's SQL surface is a focused subset, and the adapter translates better-auth's query contract onto it. better-auth handles id generation, password hashing, and date/boolean field transforms; the adapter only translates queries and coerces row values back from the engine's text representation.

better-auth needsEngine realityAdapter strategy
in / not_inNo native INExpand to an OR / AND chain of equalities
contains / starts_with / ends_withLIKEBind a %-wrapped pattern
offsetNo OFFSETOver-fetch by the offset, slice client-side
limit (dynamic)LIMIT needs an integer literalEmit the integer directly, never a parameter
Dates, booleans, JSONSix storage classes (text/int/real/blob/null/vector)supports*: false — better-auth serializes them; the adapter maps types to affinity

Scope follows the engine

The adapter is as capable as the engine's SQL subset. Joins, group-by, and subqueries are out of scope by design, so plugins that depend on database-side joins are not supported through this adapter. The core email/password and session flows — and anything expressible as single-table CRUD — work.

Related

Twill DB documentation · Licensed under BUSL-1.1. · Author