Skip to main content

User Context

Zelt provides request-scoped functions to access and manage the authenticated user.

Core Functions

FunctionDescription
setUser(user, roles)Set the authenticated user (call in middleware)
currentUser()Get the current user (returns undefined if not authenticated)
currentRoles()Get the current user's roles (returns [] if not authenticated)

Setting the User

Call setUser() in your authentication middleware after validating credentials:

import type { FunctionMiddleware } from '@zeltjs/core';
import { setUser } from '@zeltjs/core';

export const authMiddleware: FunctionMiddleware = async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '');

if (token) {
const payload = await verifyToken(token);
setUser(
{ id: payload.sub, name: payload.name, email: payload.email },
payload.roles
);
}

await next();
};

Parameters

  • user — Any object representing the authenticated user
  • roles — Array of role strings (e.g., ['admin', 'user'])

Accessing the User

In Route Handlers

Use currentUser() to access the authenticated user:

import { Controller, Get, currentUser, currentRoles } from '@zeltjs/core';

@Controller('/profile')
class ProfileController {
@Get('/me')
me() {
const user = currentUser();
const roles = currentRoles();

if (!user) {
throw new HTTPException(401, { message: 'Not authenticated' });
}

return { user, roles, isAdmin: roles.includes('admin') };
}
}

With Default Parameters

For cleaner handler signatures, use default parameters:

@Controller('/profile')
class ProfileController {
@Get('/me')
me(user = currentUser()) {
return user;
}
}

Type-Safe User Context

By default, currentUser() returns unknown. Extend RequestContextSchema to get full type safety:

declare module '@zeltjs/core' {
interface RequestContextSchema {
user: {
id: string;
name: string;
email: string;
};
authRoles: ('admin' | 'editor' | 'user')[];
}
}

Now all user-related functions are typed:

const user = currentUser();
// TypeScript knows: user?.id, user?.name, user?.email

const roles = currentRoles();
// TypeScript knows: roles is ('admin' | 'editor' | 'user')[]

setUser(
{ id: '123', name: 'Alice', email: 'alice@example.com' },
['admin', 'user']
);
// Type-checked against RequestContextSchema

Where to Put the Type Declaration

Create a types/zelt.d.ts file in your project:

// types/zelt.d.ts
declare module '@zeltjs/core' {
interface RequestContextSchema {
user: {
id: string;
name: string;
email: string;
avatarUrl?: string;
};
authRoles: ('admin' | 'moderator' | 'user')[];
}
}

export {};

Make sure your tsconfig.json includes this file:

{
"include": ["src/**/*", "types/**/*"]
}

User Design Best Practices

Keep It Minimal

Only include fields you need in handlers. Don't copy the entire database record:

// ✅ Good — minimal context
interface RequestContextSchema {
user: {
id: string;
name: string;
};
}

// ❌ Avoid — too much data
interface RequestContextSchema {
user: {
id: string;
name: string;
email: string;
passwordHash: string; // Never include sensitive data
createdAt: Date;
updatedAt: Date;
preferences: object;
// ... 20 more fields
};
}

Fetch Additional Data When Needed

Use the user ID to fetch more data in specific handlers:

@Controller('/settings')
class SettingsController {
@Authorized()
@Get('/')
async getSettings(user = currentUser()) {
const fullUser = await db.users.findById(user.id);
return { preferences: fullUser.preferences };
}
}

Consider Role Granularity

Roles should be simple strings. Complex permission logic belongs in services:

// ✅ Good — simple roles
authRoles: ('admin' | 'editor' | 'viewer')[];

// ❌ Avoid — overly specific roles
authRoles: ('can_edit_posts' | 'can_delete_posts' | 'can_view_analytics' | ...)[];

For fine-grained permissions, check roles in your service layer:

class PostService {
canEdit(post: Post, user = currentUser(), roles = currentRoles()): boolean {
if (roles.includes('admin')) return true;
if (roles.includes('editor') && post.authorId === user.id) return true;
return false;
}
}