You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: Docs/Requirements/apps.md
+28Lines changed: 28 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -184,6 +184,22 @@ Kill switches belong to an App. A kill switch entry defines a version range and
184
184
185
185
iOS apps provide `versionName` (CFBundleShortVersionString) and `buildNumber` (CFBundleVersion). Android apps provide `versionName` and `versionCode`. The kill switch entry specifies which field to compare against.
186
186
187
+
### Kill switch entry management API
188
+
189
+
All endpoints require org `admin` or `owner` UOA role (domain-hash auth + config JWT verification).
190
+
191
+
```
192
+
POST /org/:orgId/apps/:appId/killswitches — create a kill switch entry
193
+
GET /org/:orgId/apps/:appId/killswitches — list all entries for an App (paginated, cursor-based)
194
+
GET /org/:orgId/apps/:appId/killswitches/:id — get a single entry
DELETE /org/:orgId/apps/:appId/killswitches/:id — delete an entry
197
+
```
198
+
199
+
`POST` body accepts all kill switch entry fields (`platform`, `type`, `versionField`, `versionValue`, `versionMax`, `versionScheme`, `name`, `titleKey`, `title`, `messageKey`, `message`, `primaryButtonKey`, `primaryButton`, `secondaryButtonKey`, `secondaryButton`, `latestVersion`, `active`, `activateAt`, `deactivateAt`, `priority`, `testUserIds`, `cacheTtl`). `platform`, `type`, `versionField`, `versionValue`, and `type` are required; all others are optional. Returns HTTP 201 on creation with the full entry.
200
+
201
+
`DELETE` is idempotent — deleting a non-existent entry returns HTTP 404.
202
+
187
203
### Kill switch query API
188
204
189
205
**Authentication:** This endpoint is intentionally public (no bearer token required). It identifies the app by `appIdentifier` (a registered, non-secret identifier). Optionally the SDK may attach the domain-hash bearer token for the org's domain if available, but it is not required. The `userId` param, if provided, must be a valid user ID — it is not authenticated here (used only for test mode targeting).
@@ -220,6 +236,18 @@ Response when blocked:
220
236
}
221
237
```
222
238
239
+
Response when a kill switch has a future `activateAt` (not yet blocking, but approaching):
240
+
241
+
```json
242
+
{
243
+
"status": "ok",
244
+
"activatesIn": 540,
245
+
"cacheTtl": 540
246
+
}
247
+
```
248
+
249
+
`activatesIn` is the number of seconds until the nearest pending kill switch activates. When `activatesIn ≤ 900` (15 minutes), `cacheTtl` is capped to `activatesIn` so the SDK re-polls before activation. The kill switch entry is not present in the response until it activates.
Copy file name to clipboardExpand all lines: Docs/Requirements/feature-flags.md
+31-2Lines changed: 31 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -45,7 +45,7 @@ When a consuming app queries a flag for a user, resolution proceeds in order and
45
45
46
46
The global missing-flag default means consuming apps never get an error for an undefined flag — they always get a boolean.
47
47
48
-
**Multi-team context:** A user may have different `customRole` values on different teams within the same org, producing different role flag values. The flag query must specify a team context (`teamId` param) when a user has more than one team membership. If no `teamId` is provided and the user has exactly one team membership, that team's role is used. If no `teamId` is provided and the user has multiple memberships, the tiebreaker is applied in this order: (1) highest UOA system role on that team (`owner > admin > plain-member`), (2) if equal, earliest `createdAt` on the `TeamMember` record (the team the user joined first). This tiebreaker uses the effective UOA role (including org-level inheritance).
48
+
**Multi-team context:** A user may have different `customRole` values on different teams within the same org, producing different role flag values. The flag query must specify a team context (`teamId` param) when a user has more than one team membership. If no `teamId` is provided and the user has exactly one team membership, that team's role is used. If no `teamId` is provided and the user has multiple memberships, the tiebreaker is applied in this order: (1) highest UOA system role on that team (`owner > admin`, with users having no named UOA role ranking lowest), (2) if equal, earliest `createdAt` on the `TeamMember` record (the team the user joined first). This tiebreaker uses the effective UOA role (including org-level inheritance). Note: "no named UOA role" (plain member) is not a valid enum value — it simply means neither `owner` nor `admin` is assigned.
49
49
50
50
### Flag query API
51
51
@@ -174,6 +174,20 @@ PATCH /apps/:appId/flags/definitions/:flagKey — update a flag (body: { d
174
174
DELETE /apps/:appId/flags/definitions/:flagKey — delete a flag (cascades: role assignments, user overrides)
`POST` response (HTTP 201): same shape as a single element from the `GET` response array. Returns HTTP 400 with `{ "error": "Request failed" }` if the key is invalid format, already exists, or the App's `max_flags_per_app` cap is reached.
186
+
187
+
`PATCH` response (HTTP 200): updated flag object. `PATCH` with an empty body `{}` returns HTTP 400. Unknown fields in body return HTTP 400.
188
+
189
+
`DELETE` response: HTTP 204 (no body).
190
+
177
191
### Role matrix API endpoints
178
192
179
193
```
@@ -192,6 +206,21 @@ DELETE /apps/:appId/flags/overrides/:userId/:flagKey — remove a specific over
192
206
DELETE /apps/:appId/flags/overrides/:userId — remove all overrides for a user
Source values: `"override"` (explicit per-user assignment), `"role"` (from role matrix), `"default"` (flag's `defaultState`). An unknown `userId` or a `userId` not belonging to this org's App returns HTTP 200 with `{}` (no overrides, no information leak).
219
+
220
+
`PUT` response (HTTP 200): the updated resolved flag map for the user, same shape as above.
Feature flags are enabled per **App** (not per-org globally). Two fields control availability:
@@ -207,7 +236,7 @@ Feature flags are enabled per **App** (not per-org globally). Two fields control
207
236
208
237
## Resolved decisions
209
238
210
-
1.**Flag key format** — **decided: lowercase letters, digits, and underscores only; must start with a letter** (`[a-z][a-z0-9_]*`). Examples: `dark_mode`, `can_publish`, `beta_access`. Enforced at creation. Validation rejects any other format with a 400 error. This applies to all flags across all Apps. Leading underscores are not allowed.
239
+
1.**Flag key format** — **decided: must start with a lowercase letter, followed by lowercase letters, digits, or underscores** (regex: `[a-z][a-z0-9_]*`). Examples: `dark_mode`, `can_publish`, `beta_access`. Enforced at creation. Validation rejects any other format with HTTP 400. This applies to all flags across all Apps.
211
240
2.**Token flag inclusion** — **decided: all flags for the App are included in the token at login time**, regardless of whether they differ from the default. This keeps token consumption simple (no server-side re-resolution needed on read). Orgs with many flags should use `max_flags_per_app` config to cap token size (default 100; see `org_features` in brief.md).
212
241
3.**Real-time flag changes** — **decided: poll only**. The token embeds flag state at login. Mid-session changes are visible only via `/apps/:appId/flags` query endpoint calls. The SDK polls on foreground resume (default interval: 5 minutes, minimum 60 seconds, configurable per App). No push mechanism.
213
242
4.**SCIM flag sync / override retention** — **decided: soft-deprovision by default**. When a SCIM user is deprovisioned (`active: false`), their per-user flag overrides are **retained** and are re-linked when the user is re-provisioned (matched by email or SCIM `externalId`). Overrides are only deleted when a user is hard-deleted (`DELETE /scim/v2/Users/:id` with `?hardDelete=true` query param, or when the org is deleted). This is the default; orgs can configure `scim_override_retention: "retain" | "clear"` in their `org_features`.
Copy file name to clipboardExpand all lines: Docs/Requirements/roles-and-acl.md
+15-4Lines changed: 15 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -15,7 +15,7 @@ These control who can administer the UOA backend itself — the org and team str
15
15
16
16
Users who are neither `owner` nor `admin` have no named UOA system role — they are plain members whose significance is defined entirely by their custom role.
17
17
18
-
`system_admin` is a separate global role for UOA's own admin panel operators and is not visible to org/team users.
18
+
`system_admin` is a separate global role for UOA's own admin panel operators and is not visible to org/team users. It is stored as the string value `"system_admin"` on the internal `AdminUser` model (separate from `OrgMember`/`TeamMember`). System admin identity is verified via domain-hash auth against the admin domain + the UOA `SHARED_SECRET` (same mechanism as other internal routes). Endpoints under `/internal/admin/` require system admin auth and bypass all org/team permission middleware.
19
19
20
20
### 2. Consumer-defined roles (external, custom)
21
21
@@ -140,10 +140,10 @@ On rename: existing `TeamMember.customRole` records with the old name are update
140
140
-`method` — the authentication method used: `"email"`, `"google"`, `"github"`, `"microsoft"`
141
141
-`uoaRole` — `owner`, `admin`, or omitted if neither (plain member has no named UOA role)
142
142
-`uoaRoleInherited` — `true` if derived from org-level role rather than explicit team assignment; omitted if `false`
143
-
-`customRole` (org level) — the consuming app's role label for this user at the org level. Since org-level custom roles are independent of team roles, this reflects any role explicitly set at the orgscope. Omitted if none assigned at org level.
143
+
-`customRole` (org level) — a convenience field. Derived from the user's primary team `customRole` using the same tiebreaker as flag resolution (highest UOA system role → earliest `TeamMember.createdAt`). There is no separately stored org-level custom role — this field is computed, not read from a separate org-scope data model. Omitted if no team has a `customRole` set for this user.
144
144
-`customRole` (team level) — the consuming app's single role label for this user on this specific team membership. Omitted if no custom role is assigned on that team.
145
145
146
-
**`org.customRole` when the user has multiple team memberships:**The org-level `customRole` is set explicitly (not derived from team roles). If the user has no explicit org-level custom role, `org.customRole`is omitted. Team-level `customRole` values are always per-team in the `teams[]` array and are never aggregated to the org level. See decision in the role model section below.
146
+
**`org.customRole` when the user has multiple team memberships:**Always derived from the primary team's `customRole`using the standard multi-team tiebreaker. It is never independently set or stored at org scope. Custom roles are team-only constructs.
147
147
148
148
---
149
149
@@ -220,14 +220,15 @@ GET /scim/v2/Users/:id — read user
220
220
PATCH /scim/v2/Users/:id — update user attributes / active status
221
221
DELETE /scim/v2/Users/:id — deprovision user (see deprovisioning behavior below)
222
222
GET /scim/v2/Groups — list groups/teams (paginated, cursor-based)
223
+
GET /scim/v2/Groups/:id — read a single group/team
223
224
POST /scim/v2/Groups — create team via IdP
224
225
PATCH /scim/v2/Groups/:id — add/remove members, rename team
225
226
DELETE /scim/v2/Groups/:id — delete team
226
227
```
227
228
228
229
**SCIM bearer token:** An opaque UUID token issued per org via the admin panel. It has no expiry by default (long-lived). A single org may have multiple active tokens (for rolling rotation or multiple IdP integrations). Tokens are stored hashed; plain value shown only at creation. Revoked via admin panel (`DELETE /internal/admin/orgs/:orgId/scim-tokens/:tokenId`). Scoped to a single org — cannot be used across orgs.
229
230
230
-
**Group → team mapping:** SCIM Groups map to UOA teams. Mapping is stored explicitly: a `ScimGroupMapping` record links an IdP `externalGroupId` to a UOA `teamId`. Endpoints for managing mappings: `GET/POST/DELETE /org/:orgId/scim/group-mappings`. If a SCIM Group arrives with no mapping, UOA auto-creates a new team with the group `displayName` as the team name, and creates a mapping automatically. Deleting a mapping does not delete the team — only severs the auto-sync link.
231
+
**Group → team mapping:** SCIM Groups map to UOA teams. Mapping is stored explicitly: a `ScimGroupMapping` record links an IdP `externalGroupId` to a UOA `teamId`. Endpoints for managing mappings (system admin auth required): `GET/POST/DELETE /internal/admin/orgs/:orgId/scim/group-mappings` (see also `api-changes-rebac.md §6`). If a SCIM Group arrives with no mapping, UOA auto-creates a new team with the group `displayName` as the team name (if a team with that name already exists in the org, auto-creation is skipped and the SCIM operation continues without team assignment for that group), and creates a mapping automatically. When a SCIM group auto-creates a team, the first member added in the provisioning request receives `admin` UOA team role; all subsequent members receive `member`. Deleting a mapping does not delete the team — only severs the auto-sync link.
231
232
232
233
**User attribute mapping:**
233
234
-`userName` → UOA `email` (UPN format like `alice@ford.com`)
@@ -289,8 +290,18 @@ SCIM endpoints are authenticated with the per-org SCIM bearer token (see above).
289
290
290
291
**SCIM `GET /scim/v2/Users` pagination:** Uses SCIM standard `startIndex` (1-based, default 1) and `count` (page size, default 100, max 200) params. Supports `filter=userName eq "alice@acme.com"` and `filter=externalId eq "<idp-id>"` per RFC 7644 §3.4.2.2. Response includes `totalResults`, `startIndex`, `itemsPerPage`, and a `Resources` array of User objects.
291
292
293
+
**SCIM `GET /scim/v2/Groups/:id` response:** Returns a SCIM Group object including `id`, `displayName`, `externalId` (IdP group ID), and a `members[]` array of `{ "value": "<scimUserId>", "display": "<userName>" }` objects for current team members.
- Rename team: `{ "op": "Replace", "path": "displayName", "value": "New Team Name" }`
299
+
Multiple operations may appear in a single PATCH body. Operations are applied atomically. Team rename via SCIM is authoritative — if a UOA admin renamed the team, the next SCIM PATCH with a `displayName` replace will overwrite it.
300
+
292
301
**SCIM `GET /scim/v2/Groups` pagination:** Uses SCIM standard `startIndex` (1-based, default 1) and `count` (page size, default 100, max 200) params. Supports `filter=displayName eq "Engineering"` per RFC 7644 §3.4.2.2. Response includes `totalResults`, `startIndex`, `itemsPerPage`.
293
302
303
+
**Override retention on soft-deprovision:** Per-user flag overrides are **always retained** on soft-deprovision (`PATCH { active: false }` or `DELETE` without `?hardDelete=true`), regardless of the `scim_override_retention` org config. The `scim_override_retention` config only controls what happens on hard-delete (`DELETE?hardDelete=true`).
Copy file name to clipboardExpand all lines: Docs/Research/api-changes-rebac.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -315,7 +315,7 @@ interface OrgClaim {
315
315
id:string;
316
316
slug:string;
317
317
uoaRole?:'owner'|'admin'; // omitted if plain member
318
-
customRole?:string; //consuming app's role label; omitted if none
318
+
customRole?:string; //the customRole from the user's primary team (tiebreaker: highest UOA system role → earliest TeamMember.createdAt); omitted if no team customRole is set. This is a convenience field derived from the teams[] array — it is NOT a separately stored org-level role. There is no org-scoped custom role data model.
|`max_flags_per_app`| integer |`100`| Maximum feature flag definitions per App (max 500). Enforced at creation; existing flags unaffected if cap is lowered. |
603
+
|`scim_override_retention`|`"retain"`\|`"clear"`|`"retain"`| Controls per-user flag override retention on SCIM hard-delete (`DELETE /scim/v2/Users/:id?hardDelete=true`). `"retain"` keeps overrides; `"clear"` deletes them. Soft-deprovision always retains overrides regardless of this setting. |
600
604
601
605
*`enabled = false` (or omitted): all `/org/*` and `/internal/org/*` endpoints return `404`, access tokens omit `org` claims.
602
606
*`groups_enabled = false`: group read/write paths return `404`.
0 commit comments