Authentication & Access Control
Section titled “Authentication & Access Control”The one rule for app code
Section titled “The one rule for app code”Every gate in app code is a flag. A link, a page, a button, an endpoint: if some people get it and others do not, the code asks a flag, and the flag’s rule says why: a privilege (privileges contains "USER_WRITE"), a plan feature (bridge:billing.entitlement.analytics eq true) or a rollout. App code never reads a role, a privilege list, the plan or a plan feature to decide what someone may see or do.
Two things are not gates and are not flags: numbers are plan limits (<QuotaGate> in the browser, @RequireQuota on the backend), and permission on one specific record stays in app code (“only the author edits their post”).
Before calling the work done, run npx @nebulr-group/bridge-cli check gates in the project. It lists every direct role, privilege, plan or plan-feature check with the flag to use instead. Fix each one.
In NestJS, an endpoint is @RequireFeatureFlag('…') on the handler (or a route rule with featureFlag).
Authentication
Section titled “Authentication”Accessing user information
Section titled “Accessing user information”Use the @CurrentUser() decorator to access the authenticated user:
import { Controller, Get } from '@nestjs/common';
import { CurrentUser, BridgeUser } from '@nebulr-group/bridge-nestjs';
@Controller('users')
export class UsersController {
@Get('me')
getProfile(@CurrentUser() user: BridgeUser) {
return {
id: user.id,
email: user.email,
username: user.username,
fullName: user.fullName,
tenantId: user.tenantId,
appId: user.appId,
role: user.role,
privileges: user.privileges,
onboarded: user.onboarded,
};
}
}
The BridgeUser interface:
interface BridgeUser {
id: string; // User ID (sub claim)
email: string; // User's email
emailVerified: boolean;
username: string; // preferred_username claim
fullName: string; // Display name
givenName?: string;
familyName?: string;
locale?: string;
onboarded?: boolean;
tenantId: string; // Tenant/workspace ID
appId?: string; // App ID from the token (aid claim)
scope?: string; // OAuth scopes granted to the token
role?: string; // User's role within the tenant
multiTenantAccess?: boolean;
privileges?: string[]; // Privilege strings (e.g. ['AUTHENTICATED', 'USER_READ'])
}
Accessing workspace information
Section titled “Accessing workspace information”Use the @CurrentTenant() decorator to access details about the user’s workspace (called a tenant in the API):
import { Controller, Get } from '@nestjs/common';
import { CurrentUser, CurrentTenant, BridgeUser, BridgeTenant } from '@nebulr-group/bridge-nestjs';
@Controller('workspace')
export class WorkspaceController {
@Get()
getWorkspace(
@CurrentUser() user: BridgeUser,
@CurrentTenant() tenant: BridgeTenant,
) {
return {
user: { id: user.id, email: user.email, role: user.role },
tenant: {
id: tenant.id,
name: tenant.name,
locale: tenant.locale,
logo: tenant.logo,
onboarded: tenant.onboarded,
},
};
}
}
The BridgeTenant interface:
interface BridgeTenant {
id: string;
name: string;
locale?: string;
logo?: string;
onboarded?: boolean;
}
Per-route vs global protection
Section titled “Per-route vs global protection”Global guard (recommended)
Section titled “Global guard (recommended)”Enable the global guard in your module configuration:
BridgeModule.forRoot({
appId: 'YOUR_APP_ID',
guard: {
global: true,
defaultAccess: 'protected',
rules: [
{ path: '/health', privilege: 'ANONYMOUS' },
{ path: '/webhooks/*', privilege: 'ANONYMOUS' },
],
},
})
With the global guard enabled, use @Public() to mark exceptions:
import { Public } from '@nebulr-group/bridge-nestjs';
@Controller()
export class AppController {
@Get('health')
@Public()
healthCheck() {
return { status: 'ok' };
}
}
Per-controller guard
Section titled “Per-controller guard”Apply the guard to specific controllers:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { BridgeAuthGuard, CurrentUser, BridgeUser } from '@nebulr-group/bridge-nestjs';
@Controller('items')
@UseGuards(BridgeAuthGuard)
export class ItemsController {
@Get()
findAll(@CurrentUser() user: BridgeUser) {
return { message: 'Protected', user: user.email };
}
}
Per-route guard
Section titled “Per-route guard”Apply the guard to specific routes:
@Controller('items')
export class ItemsController {
@Get()
findAll() {
return { message: 'Public endpoint' };
}
@Get('private')
@UseGuards(BridgeAuthGuard)
findPrivate(@CurrentUser() user: BridgeUser) {
return { message: 'Protected endpoint', user: user.email };
}
}
API Token Authentication
Section titled “API Token Authentication”How it works
Section titled “How it works”The Bridge NestJS plugin supports two authentication paths:
- User JWT: sent via the
Authorization: Bearer <token>header. This is the standard path for browser-based users. - API token: sent via the
x-api-keyheader as a JWT. This is for server-to-server communication and programmatic access.
The guard checks both headers. If an API token is present, it is verified by introspection: the guard POSTs the token to the Bridge, which checks both the signature and the backing record (not revoked, not expired) and returns the token’s claims. Those verified claims are attached to req.bridgeApiToken. If only a Bearer token is present, the request follows the standard user JWT path, verified locally against Bridge’s JWKS endpoint.
API tokens only.
@RequirePrivilege()is an API token’s scope, for machine callers (x-api-key). It is not a gate on a person: a user JWT is not checked against it. Gate people with@RequireFeatureFlag()and a flag rule on a privilege.
ApiTokenClaims type
Section titled “ApiTokenClaims type”When an API token is verified, req.bridgeApiToken is set with these claims:
interface ApiTokenClaims {
sub: string; // Token subject identifier
appId: string; // App ID the token was issued for
tenantId: string | null; // Tenant ID (null for app-level tokens)
type: 'api'; // Always 'api' for API tokens
privileges: string[]; // Privilege strings (e.g. ['USER_READ', 'TENANT_WRITE'])
}
RequirePrivilege decorator
Section titled “RequirePrivilege decorator”Use @RequirePrivilege() to enforce that an API token has a specific privilege:
import { Controller, Get } from '@nestjs/common';
import { RequirePrivilege } from '@nebulr-group/bridge-nestjs';
@Controller('users')
export class UsersController {
@Get()
@RequirePrivilege('USER_READ')
listUsers() {
// API tokens must have USER_READ privilege
// API tokens only: a user JWT is not checked against it
}
@Post()
@RequirePrivilege('USER_WRITE')
createUser() {
// API tokens must have USER_WRITE privilege
}
}
AcceptAuth decorator
Section titled “AcceptAuth decorator”Use @AcceptAuth() to restrict which authentication type an endpoint accepts:
import { AcceptAuth } from '@nebulr-group/bridge-nestjs';
// Only user JWTs accepted; API tokens get 401
@AcceptAuth('jwt')
// Only API tokens accepted; user JWTs get 401
@AcceptAuth('api_token')
// Both accepted (default when decorator is omitted)
@AcceptAuth('both')
The AuthType is 'jwt' | 'api_token' | 'both'.
Dual-auth endpoints
Section titled “Dual-auth endpoints”Endpoints that accept both user JWTs and API tokens (default behavior):
import { Controller, Get, Req } from '@nestjs/common';
import { RequirePrivilege, CurrentUser, BridgeUser } from '@nebulr-group/bridge-nestjs';
import { Request } from 'express';
@Controller('users')
export class UsersController {
@Get()
@RequirePrivilege('USER_READ')
listUsers(@CurrentUser() user: BridgeUser, @Req() req: Request) {
if (req.bridgeApiToken) {
// Authenticated via API token
console.log('API token tenant:', req.bridgeApiToken.tenantId);
console.log('API token privileges:', req.bridgeApiToken.privileges);
return this.usersService.findAll();
}
// Authenticated via user JWT
return this.usersService.findByTenant(user.tenantId);
}
}
API-token-only endpoints
Section titled “API-token-only endpoints”Endpoints that only accept API tokens:
@Controller('integrations')
export class IntegrationsController {
@Post('sync')
@AcceptAuth('api_token')
@RequirePrivilege('TENANT_WRITE')
syncData(@Req() req: Request) {
const { tenantId, privileges } = req.bridgeApiToken!;
return this.syncService.run(tenantId);
}
}
JWT-only endpoints
Section titled “JWT-only endpoints”Endpoints that only accept user JWTs (reject API tokens):
@Controller('account')
export class AccountController {
@Get('profile')
@AcceptAuth('jwt')
getProfile(@CurrentUser() user: BridgeUser) {
// Only accessible with a user JWT
return { email: user.email, role: user.role };
}
}
Gating by role or privilege: a flag
Section titled “Gating by role or privilege: a flag”Who may call an endpoint is a flag whose rule names a privilege, e.g. privileges contains "USER_WRITE" (in the default setup ADMIN and OWNER hold it). Read the app’s real roles and privileges first (list_roles / bridge role list); prefer a privilege rule over a role rule (user.role eq "ADMIN" only when you mean the role itself). contains is exact membership.
Controller-level and route-level usage
Section titled “Controller-level and route-level usage”import { Controller, Get, UseGuards } from '@nestjs/common';
import { BridgeAuthGuard, RequireFeatureFlag, CurrentUser, BridgeUser } from '@nebulr-group/bridge-nestjs';
@Controller('admin')
@UseGuards(BridgeAuthGuard)
@RequireFeatureFlag('admin-area') // rule: privileges contains "USER_WRITE"
export class AdminController {
@Get('dashboard')
getDashboard(@CurrentUser() user: BridgeUser) {
return { message: 'Admin dashboard', admin: user.email };
}
@Get('settings')
@RequireFeatureFlag('admin-settings') // rule: privileges contains "TENANT_WRITE"
getSettings(@CurrentUser() user: BridgeUser) {
return { settings: 'sensitive data' };
}
}
Overriding with decorators
Section titled “Overriding with decorators”Route-level decorators override controller-level decorators. The guard uses getAllAndOverride from NestJS Reflector, so the most specific decorator wins.