DEV Community

Ivan Cernja for Encore

Posted on • Originally published at encore.dev

BetterAuth Integration with Encore.ts

Authentication is essential for most applications, but implementing it securely requires careful attention to session management, password hashing, and security best practices. BetterAuth is a modern, open-source TypeScript authentication framework that handles these complexities while remaining lightweight and flexible.

In this tutorial, we'll build a complete authentication backend using BetterAuth and Encore.ts. You'll learn how to set up user registration, login, session management, and protected API endpoints with full type safety from backend to frontend, while Encore handles infrastructure provisioning and provides built-in observability.

What is BetterAuth?

BetterAuth is a comprehensive TypeScript authentication framework designed for modern web applications. It provides:

  • Email & Password Authentication with secure password hashing and session management
  • Social Sign-On with OAuth providers (Google, GitHub, and more)
  • Two-Factor Authentication (2FA) for enhanced security
  • Plugin Ecosystem for extending functionality (organizations, multi-tenant, etc.)
  • Framework-Agnostic works with any TypeScript backend
  • Type-Safe built with TypeScript from the ground up

BetterAuth provides a complete authentication solution out of the box, from password hashing to OAuth integration.

What we're building

We'll create a backend authentication system with:

  • User registration with email and password
  • Login/logout with JWT session management
  • Protected API endpoints that require authentication
  • User profile endpoint to retrieve authenticated user data
  • PostgreSQL database for user storage
  • Type-safe auth data accessible throughout your application

The backend will handle all authentication logic, with BetterAuth managing sessions, password hashing, and security best practices. Encore's type-safe architecture ensures that you can protect any API endpoint with a simple auth: true flag and access authenticated user information throughout your application with full TypeScript support.

Getting started

First, install Encore if you haven't already. It automatically provisions infrastructure like databases and pub/sub, while providing built-in local development tools including a service dashboard, distributed tracing, and API documentation:

# macOS
brew install encoredev/tap/encore

# Linux
curl -L https://encore.dev/install.sh | bash

# Windows
iwr https://encore.dev/install.ps1 | iex
Enter fullscreen mode Exit fullscreen mode

Create a new Encore application from the TypeScript hello-world template. This will prompt you to create a free Encore account if you don't have one (required for secret management):

encore app create auth-app --example=ts/hello-world
cd auth-app
Enter fullscreen mode Exit fullscreen mode

Backend implementation

Installing dependencies

Install BetterAuth, Drizzle ORM, and required dependencies:

npm install better-auth drizzle-orm pg
npm install -D drizzle-kit
Enter fullscreen mode Exit fullscreen mode

We're using Drizzle ORM for type-safe database queries, which integrates seamlessly with Encore's database infrastructure. Drizzle requires the pg (node-postgres) package as its PostgreSQL driver, which BetterAuth also uses for database connections.

Setting up the database

User accounts and sessions need to be persisted in a database. With Encore, you can create a PostgreSQL database by simply defining it in code. The framework automatically provisions the infrastructure locally using Docker.

First, define the Drizzle schema for BetterAuth's tables:

// auth/schema.ts
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";

export const user = pgTable("user", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  emailVerified: boolean("emailVerified").notNull().default(false),
  image: text("image"),
  createdAt: timestamp("createdAt").notNull().defaultNow(),
  updatedAt: timestamp("updatedAt").notNull().defaultNow(),
});

export const session = pgTable("session", {
  id: text("id").primaryKey(),
  expiresAt: timestamp("expiresAt").notNull(),
  token: text("token").notNull().unique(),
  createdAt: timestamp("createdAt").notNull().defaultNow(),
  updatedAt: timestamp("updatedAt").notNull().defaultNow(),
  ipAddress: text("ipAddress"),
  userAgent: text("userAgent"),
  userId: text("userId")
    .notNull()
    .references(() => user.id, { onDelete: "cascade" }),
});

export const account = pgTable("account", {
  id: text("id").primaryKey(),
  accountId: text("accountId").notNull(),
  providerId: text("providerId").notNull(),
  userId: text("userId")
    .notNull()
    .references(() => user.id, { onDelete: "cascade" }),
  accessToken: text("accessToken"),
  refreshToken: text("refreshToken"),
  idToken: text("idToken"),
  accessTokenExpiresAt: timestamp("accessTokenExpiresAt"),
  refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"),
  scope: text("scope"),
  password: text("password"),
  createdAt: timestamp("createdAt").notNull().defaultNow(),
  updatedAt: timestamp("updatedAt").notNull().defaultNow(),
});

export const verification = pgTable("verification", {
  id: text("id").primaryKey(),
  identifier: text("identifier").notNull(),
  value: text("value").notNull(),
  expiresAt: timestamp("expiresAt").notNull(),
  createdAt: timestamp("createdAt"),
  updatedAt: timestamp("updatedAt"),
});
Enter fullscreen mode Exit fullscreen mode

Now create the database instance with Encore:

// auth/db.ts
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";

export const DB = new SQLDatabase("auth", {
  migrations: "./migrations",
});

// Create Drizzle instance
const pool = new Pool({
  connectionString: DB.connectionString,
});

export const db = drizzle(pool, { schema });
Enter fullscreen mode Exit fullscreen mode

Note: We're using Encore's SQL migration files instead of Drizzle Kit's migration tools. Encore handles applying migrations across all environments and integrates with the deployment pipeline, while Drizzle gives us type-safe queries.

The authentication system requires database tables for users, sessions, accounts, and verification tokens. Encore uses SQL migration files to define your database schema, which are automatically applied when your application starts. Create the initial migration file with the required BetterAuth tables:

-- auth/migrations/1_create_auth_tables.up.sql
CREATE TABLE IF NOT EXISTS "user" (
    "id" TEXT PRIMARY KEY NOT NULL,
    "name" TEXT NOT NULL,
    "email" TEXT NOT NULL UNIQUE,
    "emailVerified" BOOLEAN NOT NULL DEFAULT false,
    "image" TEXT,
    "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS "session" (
    "id" TEXT PRIMARY KEY NOT NULL,
    "expiresAt" TIMESTAMP NOT NULL,
    "token" TEXT NOT NULL UNIQUE,
    "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "ipAddress" TEXT,
    "userAgent" TEXT,
    "userId" TEXT NOT NULL REFERENCES "user"("id") ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS "account" (
    "id" TEXT PRIMARY KEY NOT NULL,
    "accountId" TEXT NOT NULL,
    "providerId" TEXT NOT NULL,
    "userId" TEXT NOT NULL REFERENCES "user"("id") ON DELETE CASCADE,
    "accessToken" TEXT,
    "refreshToken" TEXT,
    "idToken" TEXT,
    "accessTokenExpiresAt" TIMESTAMP,
    "refreshTokenExpiresAt" TIMESTAMP,
    "scope" TEXT,
    "password" TEXT,
    "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS "verification" (
    "id" TEXT PRIMARY KEY NOT NULL,
    "identifier" TEXT NOT NULL,
    "value" TEXT NOT NULL,
    "expiresAt" TIMESTAMP NOT NULL,
    "createdAt" TIMESTAMP,
    "updatedAt" TIMESTAMP
);
Enter fullscreen mode Exit fullscreen mode

Creating the auth service

Every Encore service starts with a service definition file (encore.service.ts). Services let you divide your application into logical components. At deploy time, you can decide whether to colocate them in a single process or deploy them as separate microservices, without changing a single line of code:

// auth/encore.service.ts
import { Service } from "encore.dev/service";

export default new Service("auth");
Enter fullscreen mode Exit fullscreen mode

Configuring BetterAuth

Now we'll configure BetterAuth to use our PostgreSQL database. We'll create a connection pool using the pg package and pass it to BetterAuth. Encore's SQLDatabase provides a connectionString that we can use. We'll also use Encore's secrets management to securely store the authentication secret key:

// auth/better-auth.ts
import { betterAuth } from "better-auth";
import { Pool } from "pg";
import { DB } from "./db";
import { secret } from "encore.dev/config";

// Secrets let you store sensitive values like API keys securely
// Learn more: https://encore.dev/docs/ts/primitives/secrets
const authSecret = secret("BetterAuthSecret");

// Create a PostgreSQL pool for BetterAuth
const pool = new Pool({
  connectionString: DB.connectionString,
});

// Create BetterAuth instance with database connection
export const auth = betterAuth({
  database: pool,
  secret: authSecret(),
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: false, // Set to true in production
  },
  session: {
    expiresIn: 60 * 60 * 24 * 7, // 7 days
    updateAge: 60 * 60 * 24, // Update session every 24 hours
  },
});
Enter fullscreen mode Exit fullscreen mode

Set your auth secret using Encore's CLI (learn more about secrets management). You'll be prompted to enter the secret value securely:

# Set the secret for local development
encore secret set --dev BetterAuthSecret

# For production environments
encore secret set --prod BetterAuthSecret
Enter fullscreen mode Exit fullscreen mode

Tip: Generate a strong random secret using openssl rand -base64 32 or a password manager.

Implementing authentication endpoints

With the configuration in place, let's build the API endpoints that handle user registration, login, and logout. In Encore, endpoints are defined using the api function with TypeScript interfaces for request and response validation, providing automatic request parsing, validation, and API documentation:

// auth/auth.ts
import { api } from "encore.dev/api";
import { auth } from "./better-auth";
import log from "encore.dev/log";

// Register a new user
interface SignUpRequest {
  email: string;
  password: string;
  name: string;
}

interface AuthResponse {
  user: {
    id: string;
    email: string;
    name: string;
  };
  session: {
    token: string;
    expiresAt: Date;
  };
}

export const signUp = api(
  { expose: true, method: "POST", path: "/auth/signup" },
  async (req: SignUpRequest): Promise<AuthResponse> => {
    log.info("User signup attempt", { email: req.email });

    // Use BetterAuth to create user
    const result = await auth.api.signUpEmail({
      body: {
        email: req.email,
        password: req.password,
        name: req.name,
      },
    });

    if (!result.user || !result.token) {
      throw new Error("Failed to create user");
    }

    return {
      user: {
        id: result.user.id,
        email: result.user.email,
        name: result.user.name,
      },
      session: {
        token: result.token,
        expiresAt: new Date(Date.now() + 60 * 60 * 24 * 7 * 1000), // 7 days from now
      },
    };
  }
);

// Login existing user
interface SignInRequest {
  email: string;
  password: string;
}

export const signIn = api(
  { expose: true, method: "POST", path: "/auth/signin" },
  async (req: SignInRequest): Promise<AuthResponse> => {
    log.info("User signin attempt", { email: req.email });

    const result = await auth.api.signInEmail({
      body: {
        email: req.email,
        password: req.password,
      },
    });

    if (!result.user || !result.token) {
      throw new Error("Invalid credentials");
    }

    return {
      user: {
        id: result.user.id,
        email: result.user.email,
        name: result.user.name,
      },
      session: {
        token: result.token,
        expiresAt: new Date(Date.now() + 60 * 60 * 24 * 7 * 1000), // 7 days from now
      },
    };
  }
);

// Logout user
interface SignOutRequest {
  token: string;
}

export const signOut = api(
  { expose: true, method: "POST", path: "/auth/signout" },
  async (req: SignOutRequest): Promise<{ success: boolean }> => {
    await auth.api.signOut({
      body: { token: req.token },
    });

    return { success: true };
  }
);
Enter fullscreen mode Exit fullscreen mode

Creating the auth handler

To protect endpoints and enable authentication across your application, we need to create an auth handler. The auth handler is a special function that Encore calls automatically for any incoming request containing authentication parameters (like an Authorization header). It verifies session tokens and makes authenticated user data available to protected endpoints.

Note on session validation: BetterAuth's built-in session management (auth.api.getSession()) is designed for cookie-based authentication in web browsers. For REST API bearer tokens, we validate sessions by querying the database directly. This approach is standard for API authentication and gives us full control over the validation logic while still leveraging BetterAuth for the security-critical parts (password hashing, user creation, and session storage).

// auth/handler.ts
import { APIError, Gateway, Header } from "encore.dev/api";
import { authHandler } from "encore.dev/auth";
import { db } from "./db";
import { session, user } from "./schema";
import { eq } from "drizzle-orm";
import log from "encore.dev/log";

// Define what we extract from the Authorization header
interface AuthParams {
  authorization: Header<"Authorization">;
}

// Define what authenticated data we make available to endpoints
interface AuthData {
  userID: string;
  email: string;
  name: string;
}

const myAuthHandler = authHandler(
  async (params: AuthParams): Promise<AuthData> => {
    const token = params.authorization.replace("Bearer ", "");

    if (!token) {
      throw APIError.unauthenticated("no token provided");
    }

    try {
      // Query the session directly from the database using Drizzle
      // BetterAuth's getSession() is designed for cookie-based web apps,
      // so for REST API bearer tokens we validate by querying the session table
      const sessionRows = await db
        .select({
          userId: session.userId,
          expiresAt: session.expiresAt,
        })
        .from(session)
        .where(eq(session.token, token))
        .limit(1);

      const sessionRow = sessionRows[0];

      if (!sessionRow) {
        throw APIError.unauthenticated("invalid session");
      }

      // Check if session is expired
      if (new Date(sessionRow.expiresAt) < new Date()) {
        throw APIError.unauthenticated("session expired");
      }

      // Get user info
      const userRows = await db
        .select({
          id: user.id,
          email: user.email,
          name: user.name,
        })
        .from(user)
        .where(eq(user.id, sessionRow.userId))
        .limit(1);

      const userRow = userRows[0];

      if (!userRow) {
        throw APIError.unauthenticated("user not found");
      }

      return {
        userID: userRow.id,
        email: userRow.email,
        name: userRow.name,
      };
    } catch (e) {
      log.error(e);
      throw APIError.unauthenticated("invalid token", e as Error);
    }
  }
);

// Create gateway with auth handler
export const gateway = new Gateway({ authHandler: myAuthHandler });
Enter fullscreen mode Exit fullscreen mode

Creating protected endpoints

Let's build a profile service to demonstrate how authentication works with protected endpoints. Any endpoint marked with auth: true will automatically require authentication, and you can access the authenticated user's information using the getAuthData() function throughout your application:

// profile/encore.service.ts
import { Service } from "encore.dev/service";

export default new Service("profile");
Enter fullscreen mode Exit fullscreen mode
// profile/profile.ts
import { api } from "encore.dev/api";
import { getAuthData } from "~encore/auth";
import log from "encore.dev/log";

interface UserProfile {
  id: string;
  email: string;
  name: string;
}

export const getProfile = api(
  {
    expose: true,
    auth: true, // Requires authentication
    method: "GET",
    path: "/profile",
  },
  async (): Promise<UserProfile> => {
    // Get authenticated user data from auth handler
    const authData = getAuthData()!;

    log.info("Profile accessed", { userID: authData.userID });

    return {
      id: authData.userID,
      email: authData.email,
      name: authData.name,
    };
  }
);

interface UpdateProfileRequest {
  name: string;
}

export const updateProfile = api(
  {
    expose: true,
    auth: true,
    method: "PUT",
    path: "/profile",
  },
  async (req: UpdateProfileRequest): Promise<UserProfile> => {
    const authData = getAuthData()!;

    log.info("Profile update", {
      userID: authData.userID,
      newName: req.name,
    });

    // In a real app, update the database here
    // For now, just return the updated data
    return {
      id: authData.userID,
      email: authData.email,
      name: req.name,
    };
  }
);
Enter fullscreen mode Exit fullscreen mode

Testing the backend

Start your Encore backend using the built-in development server (make sure Docker is running first):

encore run
Enter fullscreen mode Exit fullscreen mode

Your API is now running locally with hot-reloading enabled. Encore automatically starts the PostgreSQL database in a Docker container and runs all migrations. Open the local development dashboard at http://localhost:9400 to explore your API with interactive documentation, view distributed traces for each request, and test endpoints directly in the browser.

Exploring the database: The local development dashboard includes a built-in database explorer powered by Drizzle Studio. You can browse your database tables, view user records and sessions, and run queries visually - perfect for debugging and understanding how BetterAuth stores authentication data.

Drizzle Studio in Encore's local development dashboard

Testing with curl

Sign up a new user:

curl -X POST http://localhost:4000/auth/signup \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "SecurePass123!",
    "name": "John Doe"
  }'
Enter fullscreen mode Exit fullscreen mode

You'll get a response with a session token:

{
  "user": {
    "id": "...",
    "email": "user@example.com",
    "name": "John Doe"
  },
  "session": {
    "token": "eyJhbGci...",
    "expiresAt": "2025-01-23T..."
  }
}
Enter fullscreen mode Exit fullscreen mode

Sign in:

curl -X POST http://localhost:4000/auth/signin \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "SecurePass123!"
  }'
Enter fullscreen mode Exit fullscreen mode

Access protected endpoint:

curl http://localhost:4000/profile \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN"
Enter fullscreen mode Exit fullscreen mode

Update profile:

curl -X PUT http://localhost:4000/profile \
  -H "Authorization: Bearer YOUR_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Jane Doe"}'
Enter fullscreen mode Exit fullscreen mode

Every request generates a detailed trace that shows the complete execution flow. Here's what a trace looks like for the update profile request, showing the auth handler validation, database query, and response:

Distributed tracing in Encore's local development dashboard

Using the API Explorer

Open the local development dashboard at http://localhost:9400 and you'll see Encore's built-in development tools:

  • Service Catalog: All your API endpoints with auto-generated documentation
  • Request/Response Schemas: Type-safe interfaces automatically extracted from your code
  • API Testing: Test endpoints directly in the browser with a built-in API client
  • Distributed Tracing: Visual timeline of each request showing database queries, service calls, and auth handler execution
  • Authentication Status: See which endpoints require authentication and test them with bearer tokens

Try signing up a user through the API Explorer or curl, then use the returned session token to access the protected /profile endpoint. You'll see the full request trace, including the auth handler execution and database queries.

Encore's local development dashboard showing API Explorer, traces, and Drizzle Studio

Connecting to a frontend

One of Encore's most powerful features is its ability to automatically generate type-safe API clients for your frontend. This ensures that your frontend and backend stay in sync with zero manual work. Generate the client for your frontend application:

encore gen client frontend/src/lib/client.ts
Enter fullscreen mode Exit fullscreen mode

This creates a fully typed TypeScript client that matches your backend API exactly. Here's how to use it in your frontend application:

import Client, { Local } from "./lib/client";

// Sign up
const client = new Client(Local);
const { user, session } = await client.auth.signUp({
  email: "user@example.com",
  password: "SecurePass123!",
  name: "John Doe",
});

// Store session token (e.g., in localStorage or a cookie)
localStorage.setItem("authToken", session.token);

// Make authenticated requests
const authedClient = new Client(Local, {
  auth: { authorization: `Bearer ${session.token}` },
});

const profile = await authedClient.profile.getProfile();
Enter fullscreen mode Exit fullscreen mode

CORS Configuration:

When your frontend runs on a different origin (like localhost:5173 for Vite or localhost:3000 for Next.js), you need to configure CORS to allow authenticated requests. Update the encore.app file in your project root:

{
  "id": "auth-app",
  "global_cors": {
    "allow_origins_with_credentials": ["http://localhost:5173"]
  }
}
Enter fullscreen mode Exit fullscreen mode

This tells Encore to accept authenticated requests from your frontend's origin. In production, Encore automatically configures CORS based on your deployment settings.

For complete frontend integration guides, see the frontend integration documentation.

Deployment

Deploying your authentication backend with Encore is straightforward. Simply push your code:

git add .
git commit -m "Add BetterAuth authentication"
git push encore
Enter fullscreen mode Exit fullscreen mode

Before your production deployment can run, set the production authentication secret:

# Generate a strong random secret for production
encore secret set --prod BetterAuthSecret
Enter fullscreen mode Exit fullscreen mode

Note: Encore Cloud is great for prototyping and development with fair use limits (100k requests/day, 1GB database). For production workloads, you can connect your AWS or GCP account and Encore will provision and deploy infrastructure directly in your cloud account.

Advanced features

Adding OAuth providers

BetterAuth supports social login with popular OAuth providers like Google, GitHub, Discord, and many more. Here's how to add Google OAuth authentication to your backend:

// auth/better-auth.ts
import { betterAuth } from "better-auth";

const googleClientId = secret("GoogleClientId");
const googleClientSecret = secret("GoogleClientSecret");

export const auth = betterAuth({
  database: {
    provider: "postgres",
    url: DB.connectionString,
  },
  secret: authSecret(),
  emailAndPassword: {
    enabled: true,
  },
  socialProviders: {
    google: {
      clientId: googleClientId(),
      clientSecret: googleClientSecret(),
      redirectURI: "http://localhost:4000/auth/callback/google",
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Email verification

For production applications, you'll want to verify user email addresses before allowing them to access your application. Enable email verification in your BetterAuth configuration and integrate with an email service like Resend, SendGrid, or Amazon SES:

export const auth = betterAuth({
  // ... other config
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
  },
  emailVerification: {
    sendVerificationEmail: async (user, url) => {
      // Send email with verification link
      // Integrate with Resend, SendGrid, etc.
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Two-factor authentication

Enhance your application's security by adding two-factor authentication (2FA). BetterAuth provides a plugin system that makes it easy to add TOTP-based 2FA using authenticator apps like Google Authenticator or Authy:

import { twoFactor } from "better-auth/plugins";

export const auth = betterAuth({
  // ... other config
  plugins: [
    twoFactor({
      issuer: "YourApp",
    }),
  ],
});
Enter fullscreen mode Exit fullscreen mode

Session management

Give your users visibility and control over their active sessions by adding endpoints to list and revoke sessions. This is especially important for security-conscious applications where users might want to sign out of all devices:

export const listSessions = api(
  { expose: true, auth: true, method: "GET", path: "/sessions" },
  async () => {
    const authData = getAuthData()!;

    // Query sessions from database
    const sessions = await DB.query`
      SELECT id, created_at, ip_address, user_agent
      FROM session
      WHERE user_id = ${authData.userID}
      ORDER BY created_at DESC
    `;

    return { sessions: Array.from(sessions) };
  }
);

export const revokeSession = api(
  { expose: true, auth: true, method: "DELETE", path: "/sessions/:id" },
  async ({ id }: { id: string }) => {
    await auth.api.signOut({ body: { sessionId: id } });
    return { success: true };
  }
);
Enter fullscreen mode Exit fullscreen mode

Next steps

Now that you have a fully functional authentication backend, here are some ways to extend and enhance it:

  • Add role-based access control (RBAC) by extending the auth data with user roles and permissions, then check roles in your endpoints
  • Implement email verification with services like Resend or SendGrid to confirm user email addresses
  • Add OAuth providers (Google, GitHub, Discord, etc.) for social login to improve user experience
  • Enable two-factor authentication (2FA) for enhanced security using BetterAuth's 2FA plugin
  • Add password reset functionality with secure email links and token expiration
  • Implement organizations and teams using BetterAuth's organization plugin for B2B applications
  • Add session management features to let users view and revoke active sessions from different devices
  • Integrate with Pub/Sub to send welcome emails asynchronously when users sign up using Encore's Pub/Sub primitive
  • Add API rate limiting to protect your auth endpoints from brute force attacks
  • Implement refresh tokens for longer-lived authentication sessions

Conclusion

You've successfully built a complete, production-ready authentication backend with BetterAuth and Encore.ts! This combination gives you the best of both worlds:

BetterAuth handles the security-critical aspects of authentication (password hashing, session management, token generation, and OAuth integration) so you don't have to worry about implementing these complex features from scratch.

Encore.ts provides the infrastructure foundation with type-safe APIs, automatic database provisioning, built-in secrets management, and seamless deployment. The auth handler pattern makes it trivial to protect any endpoint with a simple auth: true flag, while getAuthData() gives you type-safe access to authenticated user information throughout your application.

The auto-generated TypeScript client ensures complete type safety between your frontend and backend, catching errors at compile time rather than runtime. The local development dashboard provides full observability into authentication flows with distributed tracing, making debugging authentication issues straightforward.

Ready to learn more?

Top comments (0)