Authentication & Authorization
Zelt provides lightweight primitives for managing authentication state and enforcing role-based access control.
Overview
The authentication API consists of:
setUser(user, roles)— Set the authenticated user and their roles in middlewarecurrentUser()— Retrieve the current user in handlerscurrentRoles()— Retrieve the current user's roles@Authorized(roles?)— Declarative decorator for access control
Setting Up Authentication
Authentication is typically handled in middleware. Zelt doesn't prescribe a specific authentication strategy—use JWT, session cookies, API keys, or any method that fits your needs.
Authentication Middleware
import type { FunctionMiddleware } from '@zeltjs/core';
import { setUser } from '@zeltjs/core';
export const jwtAuth: FunctionMiddleware = async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '');
if (token) {
const payload = await verifyJwt(token);
setUser(
{ id: payload.sub, name: payload.name },
payload.roles // e.g., ['admin', 'user']
);
}
await next();
};
Register as Global Middleware
import { createHttpApp } from '@zeltjs/core';
const app = createHttpApp({
controllers: [UserController, AdminController],
middlewares: [jwtAuth],
});
Using Authentication State
In Handlers
Use currentUser() and currentRoles() to access authentication state:
import { Controller, Get } from '@zeltjs/core';
import { currentUser, currentRoles } from '@zeltjs/core';
@Controller('/profile')
class ProfileController {
@Get('/me')
me() {
const user = currentUser();
const roles = currentRoles();
return {
user,
roles,
isAdmin: roles.includes('admin'),
};
}
}
With Default Parameters
Use default parameters for cleaner handler signatures:
@Controller('/profile')
class ProfileController {
@Get('/me')
me(user = currentUser()) {
return user;
}
}
Authorization with @Authorized
The @Authorized decorator provides declarative access control at the method level.
Require Authentication
Use @Authorized() without arguments to require any authenticated user:
import { Controller, Get, Authorized } from '@zeltjs/core';
@Controller('/dashboard')
class DashboardController {
@Authorized()
@Get('/')
index() {
return { stats: [] };
}
}
Returns 401 Unauthorized if no user is set:
{
"code": "UNAUTHORIZED",
"message": "Authentication required"
}
Require Specific Roles
Pass role names to restrict access:
@Controller('/admin')
class AdminController {
@Authorized(['admin'])
@Get('/users')
listUsers() {
return { users: [] };
}
@Authorized(['admin', 'moderator'])
@Delete('/posts/:id')
removePost(id = pathParam('id')) {
return { deleted: id };
}
}
Access is granted if the user has any of the specified roles (OR logic).
Returns 403 Forbidden if the user lacks required roles:
{
"code": "FORBIDDEN",
"message": "Insufficient permissions"
}
Type-Safe User Context
Extend RequestContextSchema to type your user object:
declare module '@zeltjs/core' {
interface RequestContextSchema {
user: {
id: string;
name: string;
email: string;
};
authRoles: ('admin' | 'editor' | 'user')[];
}
}
Now currentUser() and setUser() are fully typed:
const user = currentUser();
// TypeScript knows: user?.id, user?.name, user?.email
setUser(
{ id: '123', name: 'Alice', email: 'alice@example.com' },
['admin', 'user']
);
Authorization Flow
Request
↓
Authentication Middleware
├── Token valid? → setUser(user, roles)
└── No token? → continue (user remains undefined)
↓
@Authorized() check
├── No user? → 401 UNAUTHORIZED
├── Missing role? → 403 FORBIDDEN
└── OK → Route Handler
↓
Response
Combining with Other Decorators
@Authorized works with other method decorators:
@Controller('/posts')
class PostController {
@Authorized()
@UseMiddleware(rateLimitMiddleware)
@Post('/')
create(body = bodyParam(CreatePostSchema)) {
return { created: true };
}
}
Using @zeltjs/auth-jwt
For JWT authentication, Zelt provides the @zeltjs/auth-jwt package with ready-to-use middleware and services.
Installation
pnpm add @zeltjs/auth-jwt
Basic Setup
- Set the
JWT_SECRETenvironment variable - Register the
JwtMiddlewareandJwtConfig:
import { createHttpApp } from '@zeltjs/core';
import { JwtMiddleware, JwtConfig } from '@zeltjs/auth-jwt';
const app = createHttpApp({
controllers: [UserController],
middlewares: [JwtMiddleware],
configs: [JwtConfig],
});
Generating Tokens
Use JwtService to sign tokens:
import { Controller, Post, bodyParam, inject } from '@zeltjs/core';
import { JwtService } from '@zeltjs/auth-jwt';
import * as v from 'valibot';
const LoginSchema = v.object({
email: v.string(),
password: v.string(),
});
@Controller('/auth')
class AuthController {
constructor(private jwtService = inject(JwtService)) {}
@Post('/login')
async login(body = bodyParam(LoginSchema)) {
const user = await validateCredentials(body.email, body.password);
const token = await this.jwtService.sign({ sub: user.id, roles: user.roles });
return { token };
}
}
Custom Configuration
Extend JwtConfig to customize behavior:
import { JwtConfig, type ResolveUserResult, type JwtPayload } from '@zeltjs/auth-jwt';
import { Config } from '@zeltjs/core';
@Config
class CustomJwtConfig extends JwtConfig {
override get expiresIn(): string {
return '7d';
}
override get resolveUser(): (payload: JwtPayload) => Promise<ResolveUserResult> {
return async (payload) => {
const user = await findUserById(payload.sub);
return {
user: { id: user.id, name: user.name, email: user.email },
roles: user.roles,
};
};
}
}
Register the custom config:
const app = createHttpApp({
controllers: [AuthController, UserController],
middlewares: [JwtMiddleware],
configs: [CustomJwtConfig],
});
JwtService Methods
| Method | Description |
|---|---|
sign(payload) | Create a signed JWT token |
verify(token) | Verify and decode a token (throws on invalid) |
decode(token) | Decode without verification (returns null on error) |
Best Practices
- Set authentication early — Register auth middleware globally so it runs before route handlers
- Use typed context — Extend
RequestContextSchemato get type safety for user objects - Keep roles simple — Use flat role strings; complex permission logic belongs in services
- Separate concerns — Middleware handles authentication,
@Authorizedhandles authorization - Default to secure — Use
@Authorized()on protected routes rather than checking manually