Guards
Guards
Guards define access control rules in the OpenSya backend runtime.
They are used to protect controllers, validate permissions and stop unauthorized requests before the controller handler is executed.
OpenSya guards are discovered automatically from the server/guards directory and executed by the generated backend runtime.
Creating a Guard
A guard is created using the global defineGuard helper.
export default defineGuard(({ controllerOptions, request }) => {
if (!controllerOptions.roles) return;
if (!controllerOptions.roles.includes(request._user?.role)) {
return {
pass: false,
errorMessage: $t('session.errors.not_authorized'),
};
}
});
This guard checks the roles option declared on a controller and blocks the request if the current user does not have the required role.
How Guards Work
A guard receives runtime information about the current request and the controller being executed.
It can:
- Read controller options
- Read the current request
- Check the authenticated user
- Validate roles or permissions
- Stop the request
- Return an error message
If the guard does not return anything, the request continues.
export default defineGuard(() => {
// No return means the request is allowed to continue.
});
To block the request, return an object with pass: false.
return {
pass: false,
errorMessage: 'Not authorized',
};
Extending Controller Options
Guards can extend controller options using TypeScript module augmentation.
This allows a guard to introduce custom options that controllers can use.
declare module '#core/nest/types/controllers' {
interface ControllerOptions {
roles?: string[];
}
}
After declaring this augmentation, controllers can use the roles option.
export default defineController(
async () => {
return useService('user.list')();
},
{
roles: ['admin'],
},
);
Role-based Access Control
A common use case is role-based access control.
declare module '#core/nest/types/controllers' {
interface ControllerOptions {
roles?: string[];
}
}
export default defineGuard(({ controllerOptions, request }) => {
if (!controllerOptions.roles) return;
const userRole = request._user?.role;
if (!controllerOptions.roles.includes(userRole)) {
return {
pass: false,
errorMessage: $t('session.errors.not_authorized'),
};
}
});
Usage inside a controller:
export default defineController(
async () => {
return useService('report.list')();
},
{
roles: ['admin', 'manager'],
},
);
Only users with the admin or manager role can access this endpoint.
Public Controllers
Public controllers are intentionally exposed without authentication or access checks.
export default defineController(
async () => {
return useService('config.get')();
},
{ public: true },
);
Guards should generally ignore public controllers unless they are designed to run globally for all requests.
export default defineGuard(({ controllerOptions }) => {
if (controllerOptions.public) return;
// Protected-only logic here.
});
Permission Guard Example
You can create custom access rules beyond simple roles.
declare module '#core/nest/types/controllers' {
interface ControllerOptions {
permissions?: string[];
}
}
export default defineGuard(({ controllerOptions, request }) => {
if (!controllerOptions.permissions?.length) return;
const userPermissions = request._user?.permissions ?? [];
const hasPermission = controllerOptions.permissions.every((permission) =>
userPermissions.includes(permission),
);
if (!hasPermission) {
return {
pass: false,
errorMessage: $t('session.errors.not_authorized'),
};
}
});
Usage:
export default defineController(
async ({ req }) => {
return useService('candidate.deleteMany')(req.body.ids);
},
{
permissions: ['candidate.delete'],
},
);
Guard Return Value
A guard can either allow the request to continue or block it.
| Return Value | Behavior |
|---|---|
undefined | The request continues. |
{ pass: true } | The request continues. |
{ pass: false, errorMessage: string } | The request is blocked with an error message. |
Runtime Discovery
During startup, OpenSya automatically:
- Discovers guard files
- Registers guards in the generated backend runtime
- Resolves controller options
- Executes guards before controller handlers
- Blocks or allows requests based on guard results
Filesystem Discovery
↓
Guard Registration
↓
Controller Option Resolution
↓
Guard Execution
↓
Controller Execution
Best Practices
Keep guards focused on access control.
A guard should usually answer one question:
Is this request allowed to continue?
Avoid putting business workflows inside guards. Business logic should usually live in services.
Good guard responsibilities include:
- Authentication checks
- Role validation
- Permission validation
- Ownership checks
- Request-level access rules
Philosophy
Guards are part of the OpenSya runtime orchestration system.
They allow recruitment and business platforms to define reusable access rules without scattering authorization logic across controllers.
This keeps controllers thin, access control consistent and backend infrastructures easier to maintain at scale.