Add Email and Password Auth to libSQL with Better Auth

libSQLAuthenticationDatabases

Most "auth as a service" products hand you a hosted black box. The other option is a blank database and a weekend of wiring. libSQL plus Better Auth lands in the middle: a real SQLite-compatible database you own, with email and password sign-in working in minutes.

This post builds an auth server on libSQL locally with SpinDB, then deploys the exact same setup to Layerbase Cloud, where the database ships pre-loaded with the Better Auth schema.

What is SpinDB? It is a single CLI for running any database engine locally, no Docker required.

Run libSQL locally

bash
npm i -g spindb
spindb create auth --engine libsql --start

That gives you a libSQL server with an HTTP endpoint. Grab its connection string:

bash
spindb url auth
text
http://127.0.0.1:8080

Wire up Better Auth

Better Auth talks to libSQL through its Kysely dialect. Install the pieces:

bash
pnpm add better-auth @libsql/client @libsql/kysely-libsql kysely
ts
// auth.ts
import { betterAuth } from 'better-auth'
import { createClient } from '@libsql/client'
import { LibsqlDialect } from '@libsql/kysely-libsql'

export const auth = betterAuth({
  database: {
    dialect: new LibsqlDialect({
      client: createClient({
        url: process.env.LIBSQL_URL!,
        authToken: process.env.LIBSQL_AUTH_TOKEN,
      }),
    }),
    type: 'sqlite',
  },
  emailAndPassword: { enabled: true },
})

Better Auth needs four tables (user, session, account, verification). Generate and apply them with its CLI:

bash
npx @better-auth/cli migrate

That is the only migration step, and on Layerbase Cloud you can skip it entirely (more on that below).

Sign up and sign in

The client SDK reads like Supabase's, so if you have used auth.signUp and signInWithPassword before, this will feel familiar:

ts
// client.ts
import { createAuthClient } from 'better-auth/client'

export const authClient = createAuthClient()

await authClient.signUp.email({
  email: 'ada@example.com',
  password: 'a-strong-password',
  name: 'Ada Lovelace',
})

await authClient.signIn.email({
  email: 'ada@example.com',
  password: 'a-strong-password',
})

Passwords are hashed with scrypt and stored in the account table with providerId set to credential. Nothing lands in plaintext, and you never wrote a hashing line.

Go to production without changing engines

Your local libSQL is bit for bit the same engine you run in production. On Layerbase Cloud, create a libSQL database and toggle Set up as Auth Server. The provisioned database comes pre-loaded with the Better Auth schema, so there is no migration to run. You can even seed an admin user (email and password) at create time and sign in with it immediately.

Swap two environment variables and ship:

bash
# .env
LIBSQL_URL=libsql://your-db.cloud.layerbase.dev
LIBSQL_AUTH_TOKEN=your-token

Same auth.ts, same client calls, real TLS connection. If you would rather use Auth.js (Drizzle, Kysely, or Prisma) instead of Better Auth, the dashboard ships copy-paste recipes for each.

Not just Next.js

Better Auth is framework agnostic, so the same config works behind Express, Hono, SvelteKit, or plain Node. And because libSQL speaks the SQLite protocol, you can connect from Python (libsql-client plus passlib) or hand-roll sessions with the raw @libsql/client if you want zero dependencies.

SpinDB command reference

bash
spindb create auth --engine libsql --start   # create + start a local libSQL
spindb url auth                              # print the connection string
spindb connect auth                          # open a shell
spindb stop auth                             # stop when you are done
spindb start auth                            # bring it back

SpinDB runs 20+ engines the same way, so the Valkey cache and Postgres database your app also needs are one command each. When you are ready for production, the same engines are a click away on Layerbase Cloud. Prefer a desktop app? Grab Layerbase Desktop.

Something not working?