# ctx.identity

> Reference for ctx.identity with the user and org shape, roles as the authorization source, null on excluded paths, and dev identity injection.

`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

```typescript
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.

:::note{title="Staged availability"}
In production today, `user.id` is reliably populated. Treat `email`, `name`, `groups`, `roles`, and `org` as arriving with the full Helix Identity rollout: document-complete here as the target shape, but code should handle them as potentially empty rather than assuming SSO data is present.
:::

## Example

```typescript
// 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](/documentation/guides/identity-and-roles/).

---

Canonical: https://helix.tray.ai/documentation/reference/context/identity/
Any link on this page is available as markdown by appending .md to its URL.
Full corpus: https://helix.tray.ai/documentation/llms-full.txt