Server

Guards

Learn how to create runtime guards and access control rules in the OpenSya Nest runtime.

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.

Guards allow OpenSya applications to keep access control modular, reusable and runtime-aware.

Creating a Guard

A guard is created using the global defineGuard helper.

server/guards/roles.ts
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.

server/guards/roles.ts
declare module '#core/nest/types/controllers' {
  interface ControllerOptions {
    roles?: string[];
  }
}

After declaring this augmentation, controllers can use the roles option.

server/controllers/admin/users/index.get.ts
export default defineController(
  async () => {
    return useService('user.list')();
  },
  {
    roles: ['admin'],
  },
);
Controller options can be extended by guards to create application-specific access rules.

Role-based Access Control

A common use case is role-based access control.

server/guards/roles.ts
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:

server/controllers/admin/reports/index.get.ts
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.

server/controllers/config/index.get.ts
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.

server/guards/custom.ts
export default defineGuard(({ controllerOptions }) => {
  if (controllerOptions.public) return;

  // Protected-only logic here.
});
Be careful when creating global guards.A guard can affect every protected controller in the backend runtime.

Permission Guard Example

You can create custom access rules beyond simple roles.

server/guards/permissions.ts
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:

server/controllers/candidates/index.delete.ts
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 ValueBehavior
undefinedThe 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:

  1. Discovers guard files
  2. Registers guards in the generated backend runtime
  3. Resolves controller options
  4. Executes guards before controller handlers
  5. Blocks or allows requests based on guard results
Runtime Lifecycle
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
Use guards for access decisions and services for business logic.

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.