ctx.identity carries the authenticated user and organization for HTTP-triggered functions when Helix Identity is enabled. On paths excluded from authentication it is null instead of throwing, so handlers can serve both authenticated and anonymous requests. Schedules, app triggers, and queue consumers run without a user; accessing ctx.identity there throws a clear error.
IdentityContext
interface IdentityContext {
user: {
id: string; // Helix user ID
email: string;
name: string;
groups: string[]; // IdP-synced group names, informational only
roles: string[]; // role IDs from helix.config.ts, use for authorization
};
org: {
id: string; // Helix org ID
name: string;
};
}
ctx.identity is typed IdentityContext | null.
| Field | Type | Description |
|---|---|---|
user.id | string | Helix user ID. Also mirrored on the top-level userId execution metadata field. |
user.email | string | The user’s email address |
user.name | string | Display name |
user.groups | string[] | IdP-synced group names. For logging and display only; never branch authorization on groups. |
user.roles | string[] | Role IDs declared in helix.config.ts, resolved from direct assignments plus group-to-role mappings. The authorization source. |
org.id | string | Helix organization ID |
org.name | string | Organization name |
Roles are the app’s authorization vocabulary; groups are an IdP concern. The platform maps IdP groups to roles, and your code should only reason in roles: declarative restrictions via the access named export, or user.roles.includes(...) for branching inside a handler.
Example
// functions/api/v1/dashboard.get.ts
import { defineFunction } from '@trayai/helix-sdk';
export default defineFunction(async (ctx) => {
if (!ctx.identity) {
// Reachable only on paths excluded from authentication
throw ctx.error(401, 'Sign-in required');
}
const { user, org } = ctx.identity;
ctx.log.info('Dashboard accessed', { userId: user.id, org: org.name });
const isAdmin = user.roles.includes('admin');
return isAdmin ? getFullDashboard(ctx) : getLimitedDashboard(ctx);
});
Identity in local development
helix dev has no SSO flow in front of it. The dev server injects a static dev identity into every request, so ctx.identity is always populated locally. The default dev user (dev-user, dev@localhost) holds every role declared in config, so no route is blocked out of the box. To test as a specific persona, pass an identity JSON file with helix dev --identity <path>, or override a single request with the X-Helix-Dev-Roles and X-Helix-Dev-User headers (dev only; the Helix edge strips them in production). A file containing just null simulates an unauthenticated request.
Role definitions, the access export, enablement, and excluded paths are covered in the identity and roles guide.