docs/ts/develop/integrations/better-auth.md
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.
<Callout type="info">If you haven't installed Encore yet, see the installation guide first.
</Callout>$ npm install better-auth pg
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",
});
Locally, Encore starts a PostgreSQL instance automatically when you run encore run. You'll need Docker running for the local database.
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
Tip: Generate a strong auth secret with openssl rand -base64 32 and paste it when prompted for AuthSecret.
Locally, secrets are stored on your machine and injected when you run encore run. No .env files needed.
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 });
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());
}
);
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 };
}
);
When you deploy, Encore automatically provisions and manages the infrastructure your app needs:
Build a Docker image and deploy anywhere:
$ encore build docker my-app:latest
See the self-hosting docs for more details.
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.