Better Auth email/password on in-memory SQLite: working versions (tested 2026-10-15)
Configure Better Auth with better-sqlite3 in memory, run its migrations, then sign up, sign in through auth.handler with a session cookie, read the session, reject bad credentials and sign out, all offline.
passed5 passed · 0 failed2026-10-15 20:08 UTC- Status
- passed
- Last passed
- 2026-10-15 20:08 UTC
- Tests
- 5 / 5
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38799498972
The code
Exactly the files that passed. Your agent can pull them with get_recipe("better-auth-sqlite-email-password") (install), or see them on GitHub.
{
"name": "better-auth-sqlite-email-password",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"better-auth": "1.7.10",
"better-sqlite3": "13.0.3"
},
"devDependencies": {
"@types/node": "26.6.5",
"tsx": "4.23.15"
}
}
import { betterAuth } from "better-auth";
import { getMigrations } from "better-auth/db/migration"; // 1.7 moved it off better-auth/db
import Database from "better-sqlite3";
/** Better Auth on an in-memory SQLite database with email/password enabled. */
export async function makeAuth() {
const auth = betterAuth({
database: new Database(":memory:"),
secret: "test-secret-test-secret-test-secret-0123",
baseURL: "http://localhost:3000",
emailAndPassword: { enabled: true, minPasswordLength: 10 },
telemetry: { enabled: false },
});
const { runMigrations } = await getMigrations(auth.options);
await runMigrations(); // creates user/session/account/verification tables
return auth;
}
export type Auth = Awaited<ReturnType<typeof makeAuth>>;
import { test } from "node:test";
import assert from "node:assert/strict";
import { makeAuth } from "../src/auth.ts";
const creds = { email: "[email protected]", password: "correct-horse-battery", name: "JP" };
test("sign up creates a user and a session", async () => {
const auth = await makeAuth();
const r = await auth.api.signUpEmail({ body: creds });
assert.equal(r.user.email, "[email protected]");
assert.equal(r.user.name, "JP");
assert.equal(r.user.emailVerified, false);
assert.equal(typeof r.token, "string");
});
test("duplicate email and short password are rejected", async () => {
const auth = await makeAuth();
await auth.api.signUpEmail({ body: creds });
await assert.rejects(auth.api.signUpEmail({ body: creds }), (e: { status?: string; statusCode?: number }) => e.statusCode === 422 || e.status === "UNPROCESSABLE_ENTITY");
await assert.rejects(auth.api.signUpEmail({ body: { ...creds, email: "[email protected]", password: "short" } }), (e: { statusCode?: number }) => e.statusCode === 400);
});
test("sign in over HTTP returns a session cookie usable for getSession", async () => {
const auth = await makeAuth();
await auth.api.signUpEmail({ body: creds });
const res = await auth.handler(new Request("http://localhost:3000/api/auth/sign-in/email", {
method: "POST",
headers: { "content-type": "application/json", origin: "http://localhost:3000" },
body: JSON.stringify({ email: creds.email, password: creds.password }),
}));
assert.equal(res.status, 200);
const setCookie = res.headers.get("set-cookie") ?? "";
assert.match(setCookie, /better-auth\.session_token=/);
assert.match(setCookie, /HttpOnly/i);
const cookie = setCookie.split(";")[0];
const session = await auth.api.getSession({ headers: new Headers({ cookie }) });
assert.equal(session?.user.email, creds.email);
});
test("wrong password is 401 and no session without cookie", async () => {
const auth = await makeAuth();
await auth.api.signUpEmail({ body: creds });
const res = await auth.handler(new Request("http://localhost:3000/api/auth/sign-in/email", {
method: "POST",
headers: { "content-type": "application/json", origin: "http://localhost:3000" },
body: JSON.stringify({ email: creds.email, password: "wrong-password-123" }),
}));
assert.equal(res.status, 401);
assert.equal(await auth.api.getSession({ headers: new Headers() }), null);
});
test("sign out revokes the session", async () => {
const auth = await makeAuth();
const { headers } = await auth.api.signUpEmail({ body: creds, returnHeaders: true });
const cookie = (headers.get("set-cookie") ?? "").split(";")[0];
const h = new Headers({ cookie });
assert.ok(await auth.api.getSession({ headers: h }));
await auth.api.signOut({ headers: h });
assert.equal(await auth.api.getSession({ headers: h }), null);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
better-sqlite3 transactions with Zod-validated inputs
A small ledger on better-sqlite3 using db.transaction for atomic transfers, nested transactions as savepoints for all-or-nothing batches, and Zod 4 to validate inputs and rows, tested on in-memory SQLite.
Drizzle ORM + better-sqlite3 CRUD
Typed schema, migrations-free table creation and CRUD with Drizzle on an in-memory SQLite database.
Drizzle ORM + drizzle-zod validated inserts on SQLite
Generate Zod insert and select schemas from Drizzle tables with drizzle-zod, refine them, and validate input before inserting into SQLite.
Hono + Drizzle ORM + SQLite REST API (Vitest)
CRUD REST API in Hono backed by Drizzle ORM on better-sqlite3, with Zod validation, tested with Vitest and app.request().