Skip to content

Guards

Guards run before your procedure handler and reject the request if the caller doesn’t meet the required conditions — whether that’s being logged in, having a specific role, or satisfying a custom check. They compose with allOf, anyOf, and not combinators for complex authorization logic.

Requires a logged-in user:

import { authenticated } from '@veloxts/auth';
getProfile: procedure()
.guard(authenticated)
.query(({ ctx }) => ctx.user),

Requires a specific role:

import { hasRole } from '@veloxts/auth';
deleteUser: procedure()
.guard(authenticated)
.guard(hasRole('admin'))
.mutation(handler),

Requires a specific permission:

import { hasPermission } from '@veloxts/auth';
updateSettings: procedure()
.guard(authenticated)
.guard(hasPermission('settings:write'))
.mutation(handler),

The quickest way to create a custom guard:

import { guard } from '@veloxts/auth';
// Check function + message (most common)
const isVerified = guard(
(ctx) => ctx.user?.emailVerified === true,
'Email verification required'
);
// Usage
updateEmail: procedure()
.guard(authenticated)
.guard(isVerified)
.mutation(handler),

For more control, use the fluent builder:

import { guard } from '@veloxts/auth';
const isPremium = guard((ctx) => ctx.user?.subscription === 'premium')
.named('isPremium')
.msg('Premium subscription required')
.status(402);

For maximum explicitness:

import { defineGuard } from '@veloxts/auth';
const isVerified = defineGuard({
name: 'isVerified',
check: (ctx) => ctx.user?.emailVerified === true,
message: 'Email verification required',
statusCode: 403,
});
import { allOf } from '@veloxts/auth';
const adminAndVerified = allOf(hasRole('admin'), isVerified);
sensitiveOperation: procedure()
.guard(authenticated)
.guard(adminAndVerified)
.mutation(handler),
import { anyOf } from '@veloxts/auth';
const adminOrModerator = anyOf(hasRole('admin'), hasRole('moderator'));
moderateContent: procedure()
.guard(authenticated)
.guard(adminOrModerator)
.mutation(handler),
import { not } from '@veloxts/auth';
const notBanned = not(defineGuard({
name: 'isBanned',
check: (ctx) => ctx.user?.banned === true,
message: 'Account is banned',
}));

Guards run in order. If any fails, subsequent guards don’t run:

getSecretData: procedure()
.guard(authenticated) // 1. Must be logged in
.guard(isVerified) // 2. Must have verified email
.guard(hasRole('admin')) // 3. Must be admin
.guard(notBanned) // 4. Must not be banned
.query(handler),

Add multiple guards at once for cleaner code:

import { authenticated, hasRole, emailVerified } from '@veloxts/auth';
// Multiple guards in one call
getSecretData: procedure()
.guards(authenticated, emailVerified, hasRole('admin'))
.query(handler),
// Equivalent to chaining .guard() calls
getSecretData: procedure()
.guard(authenticated)
.guard(emailVerified)
.guard(hasRole('admin'))
.query(handler),

When a guard fails:

{
"error": {
"message": "Email verification required",
"code": "FORBIDDEN"
}
}

HTTP status code is set based on guard configuration (default: 403).

For per-resource authorization, use policies instead:

// Guard: "Is user an admin?" (role-based)
.guard(hasRole('admin'))
// Policy: "Can user edit THIS post?" (resource-based)
.guard(authorize('posts', 'edit'))

See Policies for resource-based authorization.

  • Policies - Resource-based authorization
  • JWT - Token authentication
  • Sessions - Cookie-based auth