Skip to content

Authentication - Getting Started

Glasswork provides a focused auth module that integrates Better Auth for authentication with CASL for authorization, giving you type-safe, role-based access control with minimal setup.

For multi-tenant applications, Row Level Security adds an optional PostgreSQL tenant boundary alongside CASL permissions. Set up authentication and abilities here first, then follow that guide for database policies and tenant context.

After reading this guide, you will know:

  • How to configure Better Auth with Prisma
  • How to define CASL abilities for authorization
  • How to set up auth middleware for routes
  • How to integrate auth with OpenAPI documentation

Quick Start

1. Install Dependencies

bash
npm install better-auth @casl/ability @casl/prisma
bash
pnpm add better-auth @casl/ability @casl/prisma
bash
yarn add better-auth @casl/ability @casl/prisma

2. Generate Prisma Schema

Better Auth requires tables for users, sessions, and accounts. Generate the schema using the Better Auth CLI:

bash
npx @better-auth/cli@latest generate

This adds the required models to your schema.prisma. Run the migration:

bash
npx prisma migrate dev --name add-auth-tables

3. Configure Better Auth

Create your Better Auth configuration using the Prisma adapter:

typescript
// src/auth/auth.config.ts
import { betterAuth } from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
import { openAPI } from 'better-auth/plugins';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export const auth = betterAuth({
  database: prismaAdapter(prisma, {
    provider: 'postgresql', // or 'sqlite', 'mysql'
  }),
  emailAndPassword: {
    enabled: true,
  },
  plugins: [
    openAPI(), // Enables OpenAPI spec generation
  ],
  user: {
    additionalFields: {
      role: { type: 'string', defaultValue: 'member' },
    },
  },
});

Performance Tip

Enable database joins for 2-3x performance improvements on session queries:

typescript
export const auth = betterAuth({
  experimental: { joins: true },
  // ... rest of config
});

4. Define Abilities

Create CASL abilities to define what each role can do:

typescript
// src/auth/abilities.ts
import { defineRoleAbilities, type AuthUser } from 'glasswork/auth';
import type { Subjects } from '@casl/prisma';
import type { User, Project, Organization } from '@prisma/client';

type AppSubjects = Subjects<{
  User: User;
  Project: Project;
  Organization: Organization;
}> | 'all';

type AppAction = 'create' | 'read' | 'update' | 'delete' | 'manage';
type AppRole = 'admin' | 'member' | 'guest';

export const abilities = defineRoleAbilities<AppSubjects, AppAction, AppRole>({
  admin: ({ can }) => {
    can('manage', 'all');
  },

  member: ({ can, cannot, user }) => {
    can('read', 'Organization', { id: user.tenantId });
    can('read', 'Project', { organizationId: user.tenantId });
    can('create', 'Project', { organizationId: user.tenantId });
    can('update', 'Project', { createdBy: user.id });
    cannot('delete', 'Project');
  },

  guest: () => {
    // No abilities
  },
});

export type AppAbility = ReturnType<typeof abilities.for>;

5. Create Auth Middleware

Wire up the middleware that validates sessions and builds abilities:

typescript
// src/auth/auth.middleware.ts
import { createAuthMiddleware, createBetterAuthProvider } from 'glasswork/auth';
import { auth } from './auth.config';
import { abilities } from './abilities';

// Create the provider that wraps Better Auth
const provider = createBetterAuthProvider({
  auth,
  mapUser: (user) => ({
    id: user.id as string,
    email: user.email as string,
    role: (user.role as string) ?? 'member',
    tenantId: user.organizationId as string | undefined,
  }),
});

// Create the middleware
export const authMiddleware = createAuthMiddleware({
  provider,
  buildAbility: (user) => abilities.for(user),
  guestAbility: () => abilities.forRole('guest'),
  allowGuest: false,
  cookieName: 'session',
});

6. Mount Better Auth Routes

Mount Better Auth's handler to expose auth endpoints:

typescript
// src/auth/auth.routes.ts
import { Hono } from 'hono';
import { auth } from './auth.config';

const router = new Hono();

// Mount all Better Auth routes
router.all('/*', (c) => auth.handler(c.req.raw));

export { router as authRoutes };

7. Use in Your App

Register the auth module and use middleware in routes:

typescript
// src/app.ts
import { bootstrap } from 'glasswork/core';
import { authMiddleware } from './auth/auth.middleware';
import { authRoutes } from './auth/auth.routes';

const { app } = await bootstrap(AppModule, {
  // ... options
});

// Mount auth routes
app.route('/auth', authRoutes);

// Use auth middleware on protected routes
app.use('/api/*', authMiddleware());

8. Protected Routes with Authorization

Use the authorize option to enforce permissions:

typescript
import { createRoutes, route } from 'glasswork/http';
import type { AuthenticatedAuthContext, AuthSession, AuthUser } from 'glasswork/auth';
import { authMiddleware } from './auth/auth.middleware';
import type { AppAbility } from './auth/abilities';

type AppRouteContext = AuthenticatedAuthContext<AuthUser, AuthSession, AppAbility>;

export const projectRoutes = createRoutes<
  { projectService: ProjectService },
  AppRouteContext
>((router, services, route) => {
  // Apply auth middleware
  router.use('*', authMiddleware());

  // List projects - requires 'read' permission on 'Project'
  router.get('/', ...route({
    authorize: { action: 'read', subject: 'Project' },
    responses: { 200: ProjectListSchema },
    handler: async ({ ability }) => {
      return services.projectService.findAll(ability);
    },
  }));

  // Create project
  router.post('/', ...route({
    authorize: { action: 'create', subject: 'Project' },
    body: CreateProjectSchema,
    responses: { 201: ProjectSchema },
    handler: async ({ body, user }) => {
      return services.projectService.create(body, user!.id);
    },
  }));
});

Route Context

The auth middleware adds these values to the Hono context, available in all route handlers:

PropertyTypeDescription
userAuthUser | nullAuthenticated user, or null for guests
sessionAuthSession | nullCurrent session data
abilityAppAbilityCASL ability instance
isAuthenticatedbooleanWhether user is authenticated

Pass your concrete AuthenticatedAuthContext as the second createRoutes type argument when allowGuest: false and an authorization check guarantee an authenticated request. Public or guest-capable routes should use AuthContext, whose user and session remain nullable. Without a concrete context argument, Glasswork deliberately exposes the broad default Hono context types.

Access these in route handlers:

typescript
handler: async ({ user, ability, isAuthenticated }) => {
  if (!isAuthenticated) {
    // Handle guest access
  }

  // Use ability for permission checks
  if (ability.can('delete', 'Project')) {
    // Show delete option
  }
}

Environment Variables

Configure Better Auth via environment variables:

env
# Required
DATABASE_URL=postgresql://...

# Optional - Session configuration
BETTER_AUTH_SECRET=your-secret-key  # For JWT signing

Next Steps

Released under the MIT License.