Better Auth

Better Auth is a TypeScript authentication library that supports email/password, OAuth, two-factor, magic links, and sessions. This guide shows how to use it with Encore's database provisioning and secrets management.

To get started quickly, create a new app from the example:

$ encore app create --example=ts/betterauth

Or follow the steps below to add Better Auth to an existing Encore app.

Please note

If you haven't installed Encore yet, see the installation guide first.

Install

$ npm install better-auth pg

Set up the database

Better Auth needs a database for users and sessions. Encore provisions and manages databases for you automatically, just define it in code:

db.ts
import { SQLDatabase } from "encore.dev/storage/sqldb"; export const db = new SQLDatabase("auth", { migrations: "./migrations", });
Please note

Locally, Encore starts a PostgreSQL instance automatically when you run encore run. You'll need Docker running for the local database.

Configure Better Auth

Create the Better Auth instance using Encore's database and secrets:

auth.ts
import { betterAuth } from "better-auth"; import { Pool } from "pg"; import { secret } from "encore.dev/config"; import { db } from "./db"; const authSecret = secret("AuthSecret"); const pool = new Pool({ connectionString: db.connectionString, }); export const auth = betterAuth({ secret: authSecret(), basePath: "/auth", database: pool, trustedOrigins: ["http://localhost:4000"], emailAndPassword: { enabled: true, }, socialProviders: { github: { clientId: secret("GithubClientId")(), clientSecret: secret("GithubClientSecret")(), }, }, });

Set the secrets using the Encore CLI:

$ encore secret set --type dev,local,pr,production AuthSecret
$ encore secret set --type dev,local,pr,production GithubClientId
$ encore secret set --type dev,local,pr,production GithubClientSecret
Please note

Tip: Generate a strong auth secret with openssl rand -base64 32 and paste it when prompted for AuthSecret.

Please note

Locally, secrets are stored on your machine and injected when you run encore run. No .env files needed.

Connect to Encore's auth handler

Wire Better Auth into Encore's authentication system so you can use auth: true on any API endpoint:

handler.ts
import { APIError, Gateway } from "encore.dev/api"; import { authHandler } from "encore.dev/auth"; import { Header } from "encore.dev/api"; import { auth } from "./auth"; interface AuthParams { authorization: Header<"Authorization">; } interface AuthData { userID: string; } const handler = authHandler<AuthParams, AuthData>( async (params) => { const session = await auth.api.getSession({ headers: new Headers({ authorization: params.authorization, }), }); if (!session) { throw APIError.unauthenticated("invalid session"); } return { userID: session.user.id }; } ); export const gateway = new Gateway({ authHandler: handler });

Expose Better Auth routes

Better Auth needs HTTP routes for sign-in, sign-up, and OAuth callbacks. Expose these using a raw endpoint:

routes.ts
import { api } from "encore.dev/api"; import { auth } from "./auth"; // Better Auth expects a Web Request, but Encore raw endpoints receive // a Node.js IncomingMessage. We convert between the two formats. export const authRoutes = api.raw( { expose: true, path: "/auth/*path", method: "*" }, async (req, res) => { // Read the request body const chunks: Buffer[] = []; for await (const chunk of req) { chunks.push(chunk); } const body = Buffer.concat(chunks); // Build a Web Request from the Node.js request const headers = new Headers(); for (const [key, value] of Object.entries(req.headers)) { if (value) headers.append(key, Array.isArray(value) ? value.join(", ") : value); } const url = `http://${req.headers.host}${req.url}`; const webReq = new Request(url, { method: req.method, headers, body: ["GET", "HEAD"].includes(req.method || "") ? undefined : body, }); // Pass to Better Auth and forward the response const response = await auth.handler(webReq); response.headers.forEach((value, key) => { res.setHeader(key, value); }); res.writeHead(response.status); res.end(await response.text()); } );

Use in your endpoints

Any endpoint with auth: true will now require a valid Better Auth session:

import { api } from "encore.dev/api"; import { getAuthData } from "~encore/auth"; export const getProfile = api( { auth: true, expose: true, method: "GET", path: "/profile" }, async (): Promise<{ userID: string }> => { const data = getAuthData()!; return { userID: data.userID }; } );

Deploy

When you deploy, Encore automatically provisions and manages the infrastructure your app needs:

  • Database provisioned as Cloud SQL on GCP or RDS on AWS. Migrations run automatically on deploy.
  • Secrets encrypted per environment (preview, staging, production), never shared between them.
  • Networking including TLS, load balancing, and DNS.

Self-hosting

Build a Docker image and deploy anywhere:

$ encore build docker my-app:latest

See the self-hosting docs for more details.

Encore Cloud

Deploy your application to a free staging environment in Encore's development cloud:

$ git push encore main

You can also connect your own AWS or GCP account and Encore will automatically provision databases, run migrations, and manage secrets in your cloud. See Connect your cloud account for details.