Better Auth is a framework-agnostic authentication library for TypeScript. It stores its state, including users, sessions, accounts, and verification records, in a database you own, and ships a CLI that generates and applies the schema for you.
How does the integration work?
Appwrite native PostgreSQL and MySQL databases are managed engines that Appwrite provisions in your project's region, each with its own hostname, credentials, and TLS. Better Auth connects to them like any other PostgreSQL or MySQL server: you pass it a driver pool, which it drives through its built-in Kysely adapter, or an ORM adapter such as Prisma or Drizzle.
Auth workloads are short transactions, so your runtime connects through the database's connection pooler, while the Better Auth CLI creates tables over a direct connection to the engine.
Native PostgreSQL and MySQL databases run in every Appwrite Cloud region and are available on paid plans.
How to implement
To store Better Auth data in an Appwrite native database, there are several steps you must complete:
Step 1: Create a native database
Create an account on Appwrite Cloud if you haven't already. In your project, go to Databases, click Create database, and select PostgreSQL or MySQL from the Native databases group. Pick a specification and click Create database. The database becomes ready within a few minutes.
For PostgreSQL, open the database and click Credentials to copy the connection string from the DSN or .env tab. For MySQL, read connectionString from the database object with mysql.get() and an API key that has the databases.read scope.
Step 2: Set the connection strings
Use the pooler URL for application traffic and the direct engine URL for schema work:
# Runtime: pooler, transaction modeDATABASE_URL="postgresql://admin:<password>@db-<hash>.<region>.appwrite.center:6432/<database>?sslmode=require"
# Schema generation and migrations: direct connectionDIRECT_URL="postgresql://admin:<password>@db-<hash>.<region>.appwrite.center:5432/<database>?sslmode=require"# Runtime: pooler, transaction modeDATABASE_URL="mysql://admin.<hash>:<password>@db-<hash>.<region>.appwrite.center:6033/default"
# Schema generation and migrations: direct connectionDIRECT_URL="mysql://admin.<hash>:<password>@db-<hash>.<region>.appwrite.center:3306/default"
MYSQL_SSL="true"BETTER_AUTH_URL="https://example.com"BETTER_AUTH_SECRET="replace-with-a-long-random-secret"Step 3: Configure Better Auth
Install better-auth with the driver for your engine (pg or mysql2), then pass Better Auth a pool pointed at the pooled DATABASE_URL. Appwrite Cloud encrypts connections with TLS, so set rejectUnauthorized: true to verify the certificate:
import { betterAuth } from 'better-auth';import { Pool } from 'pg';
export const auth = betterAuth({ database: new Pool({ connectionString: process.env.DATABASE_URL, ssl: { rejectUnauthorized: true } }), emailAndPassword: { enabled: true }});import { betterAuth } from 'better-auth';import { createPool } from 'mysql2/promise';
export const auth = betterAuth({ baseURL: process.env.BETTER_AUTH_URL, secret: process.env.BETTER_AUTH_SECRET, database: createPool({ uri: process.env.DATABASE_URL, timezone: 'Z', ssl: process.env.MYSQL_SSL === 'true' ? { rejectUnauthorized: true } : undefined }), emailAndPassword: { enabled: true }});timezone: 'Z' keeps timestamps consistent between your app and MySQL.
If you already use Prisma or Drizzle, pass Better Auth that ORM's adapter instead, for example drizzleAdapter(db, { provider: 'pg' }). With Prisma on PostgreSQL, add pgbouncer=true to the pooled URL, and with postgres.js, set prepare: false, because the pooler's transaction mode does not support server-side prepared statements.
Step 4: Generate and migrate the schema
Run the Better Auth CLI with the direct URL so DDL runs over a session connection:
DATABASE_URL="$DIRECT_URL" npx @better-auth/cli@latest generateDATABASE_URL="$DIRECT_URL" npx @better-auth/cli@latest migrateDATABASE_URL="$DIRECT_URL" npx auth@latest generate --config ./auth.ts --yesDATABASE_URL="$DIRECT_URL" npx auth@latest migrate --config ./auth.ts --yesmigrate only works with the built-in Kysely adapter. With a Prisma or Drizzle adapter, run generate to produce that ORM's schema, then apply it with prisma migrate deploy or drizzle-kit migrate over DIRECT_URL.
Step 5: Test the integration
Mount Better Auth's handler in your framework and create a user from your server code:
import { auth } from './auth';
const result = await auth.api.signUpEmail({ body: { email: 'ada@example.com', password: 'a-strong-password', name: 'Ada Lovelace' }});
console.log(result.user.id);Create the auth instance once at module scope so warm serverless invocations reuse the pool, and let the pooler absorb cold-start connections.
Read more about Better Auth and Appwrite Databases
If you would like to learn more about Better Auth and Appwrite Databases, we have some resources that you should visit:
