Organization Activation and Routes
Organization Activation and Routes
Zeeq treats organization activation as a server-side authorization precondition for organization-scoped state changes. An organization is active when Organization.ActivatedAtUtc is set and Organization.DisabledAtUtc is not set.
New organizations are activated at creation time. Existing non-disabled organizations are backfilled with activated_at_utc = created_at_utc during the activation migration. Disabled organizations remain inactive.
Route Shape
Organization-scoped APIs use /api/v1/orgs/{orgId}/....
The route orgId must match the active organization ID carried by the authenticated cookie. The route/cookie consistency filter runs before active-state checks:
| Condition | Result |
|---|---|
Missing route orgId | 400 Bad Request |
| Missing cookie organization claim | 401 Unauthorized |
Route orgId does not match the cookie organization | 403 Forbidden |
Route orgId matches the cookie organization | The request continues |
Do not accept organization scope only through query string parameters for new org-scoped APIs. If an endpoint needs organization context, put it in the route unless it is an auth bootstrap or activation-adjacent flow that deliberately has no active organization yet.
Protected Routes
Mutation routes under /api/v1/orgs/{orgId}/... require an active organization before they can mutate state. This includes:
- Organization profile updates.
- Member role changes and member removal.
- Organization invitation create and cancel.
- LLM settings, key management, and provider tests.
- Code-review manual requests, organization settings, reviewer agents, and repository review configuration.
- GitHub repository mapping create, update, and disable.
- User-owned client credentials and API tokens under the active organization route.
GET /api/v1/me uses the current organization from the cookie instead of a route parameter. It also requires that current organization to be active so the app shell can redirect inactive sessions consistently.
Exempt Routes
These routes intentionally do not use the active-organization filter:
- Organization creation and slug checks.
- Login, logout, OAuth callbacks, and dynamic auth bootstrap flows.
- GitHub App install and callback routes.
POST /api/v1/me/invitations/{id}/acceptand/decline.
Invitation accept and decline are activation-adjacent. A user may need to accept an invitation into an active organization while their current organization is inactive or unset, so those handlers validate the invitation transition directly instead of depending on the current organization activation filter.
Inactive Behavior
When the active-organization filter finds an inactive organization, it returns a redirect to /login?inactiveOrg=true. The frontend Kubb fetch transport handles redirected responses before JSON parsing and navigates the browser to the login page. The login view uses the inactiveOrg=true page state to render the inactive-organization notice below the OAuth login card.
The redirect is intentional for browser callers. API code should not treat inactive-organization responses as ordinary JSON error bodies.
Cache Window
The active-state lookup uses HybridCache with a 30-second expiration and local expiration. Within that window, repeated endpoint-filter checks can reuse the cached OrganizationActivationState projection instead of loading the full organization row.
The cache is a short consistency window. Activation or disable changes may take up to 30 seconds to be reflected by filters on every process.
Implementation Notes
The filters live in Zeeq.Core.Identity.Organizations:
RequireRouteOrganizationMatchesCookieFilterRequireActiveOrganizationFilterRequireActiveCurrentOrganizationFilter
Endpoint groups attach them through RequireRouteOrganizationMatchesCookie(), RequireActiveOrganization(), and RequireActiveCurrentOrganization(). Prefer applying the route/cookie filter at the route group and the active filter only to the mutation handlers that need it.