Skip to content

Organization Authorization

FRAME uses Better Auth organization access control for the permission vocabulary and NestJS policies for API enforcement.

flowchart LR
    A[Better Auth session] --> B[Active organization]
    B --> C[OrganizationPermissionGuard]
    C --> D[OrganizationPolicyService]
    D --> E[Standard or dynamic role]
    E --> F[Controller and use case]
    F --> G[Organization-scoped repository query]
    G --> H[(PostgreSQL constraints)]

OrganizationPermissionGuard reads the permission declared by @RequireOrganizationPermission. OrganizationPolicyService verifies that the session user is a member of the active organization, checks the standard roles from @frame/auth/permissions, and then evaluates any dynamic organization roles stored by Better Auth.

Protected organization endpoints use both authorization primitives:

  • @RequireOrganizationPermission(ORGANIZATION_PERMISSIONS.<resource>.<action>) declares the required permission.
  • @ActiveOrganizationId() injects the active organization identifier from the authenticated session and rejects requests without one.

Controllers pass the organization identifier through the use case and repository port. They do not accept a tenant identifier from a request body as the authority for cats, owners, or similar product records.

Controller authorization and query scoping solve different problems. Repositories must include organizationId in every tenant-owned operation:

  • inserts assign the active organization identifier;
  • lists filter by the organization identifier;
  • reads, updates, and deletes filter by both record and organization identifiers;
  • relationships use tenant-safe database constraints where one tenant-owned record references another.

This prevents a valid member from reading or mutating another organization’s record by guessing its identifier.

An upload intent that mutates an organization-owned asset must perform the corresponding organization policy check before generating a key or presigned URL. Organization logos require organization:update, require an organization identifier, and store the organization identifier in the object-key prefix.

  1. Reuse a permission from ORGANIZATION_PERMISSIONS; add new vocabulary in packages/auth first when required.
  2. Apply @RequireOrganizationPermission to the controller method.
  3. Read the active organization with @ActiveOrganizationId().
  4. Carry organizationId through the use case and repository port.
  5. Add organization predicates to every repository query and tenant-safe constraints to related tables.
  6. Test permission behavior and verify that repository calls receive the organization identifier.

See ADR 0003 for the role model and architectural rationale.