@elevasis/sdk 1.8.2 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/cli.cjs +289 -105
  2. package/dist/index.d.ts +90 -39
  3. package/dist/types/worker/adapters/lead.d.ts +1 -1
  4. package/dist/worker/index.js +2 -0
  5. package/package.json +2 -2
  6. package/reference/_navigation.md +7 -1
  7. package/reference/_reference-manifest.json +14 -0
  8. package/reference/claude-config/logs/scaffold-registry-reminder.log +3 -0
  9. package/reference/claude-config/rules/agent-start-here.md +254 -254
  10. package/reference/claude-config/rules/frontend.md +43 -43
  11. package/reference/claude-config/rules/operations.md +64 -64
  12. package/reference/claude-config/rules/organization-model.md +42 -43
  13. package/reference/claude-config/rules/organization-os.md +107 -107
  14. package/reference/claude-config/rules/shared-types.md +2 -2
  15. package/reference/claude-config/rules/task-tracking.md +1 -1
  16. package/reference/claude-config/rules/ui.md +202 -202
  17. package/reference/claude-config/rules/vibe.md +202 -202
  18. package/reference/claude-config/skills/configure/SKILL.md +98 -98
  19. package/reference/claude-config/skills/configure/operations/codify-level-a.md +100 -100
  20. package/reference/claude-config/skills/configure/operations/codify-level-b.md +158 -158
  21. package/reference/claude-config/skills/configure/operations/customers.md +150 -150
  22. package/reference/claude-config/skills/configure/operations/features.md +162 -162
  23. package/reference/claude-config/skills/configure/operations/goals.md +147 -147
  24. package/reference/claude-config/skills/configure/operations/identity.md +133 -133
  25. package/reference/claude-config/skills/configure/operations/labels.md +128 -128
  26. package/reference/claude-config/skills/configure/operations/offerings.md +159 -159
  27. package/reference/claude-config/skills/configure/operations/roles.md +153 -153
  28. package/reference/claude-config/skills/configure/operations/techStack.md +139 -139
  29. package/reference/claude-config/skills/explore/SKILL.md +78 -78
  30. package/reference/claude-config/skills/git-sync/SKILL.md +126 -0
  31. package/reference/claude-config/skills/save/SKILL.md +183 -183
  32. package/reference/claude-config/skills/setup/SKILL.md +275 -275
  33. package/reference/claude-config/skills/sync/SKILL.md +10 -44
  34. package/reference/claude-config/sync-notes/2026-04-22-git-sync-and-sync-notes.md +27 -0
  35. package/reference/claude-config/sync-notes/2026-04-22-lead-gen-deliverability-removal.md +30 -0
  36. package/reference/claude-config/sync-notes/2026-04-24-ui-consolidation-and-sdk-cli-train.md +86 -0
  37. package/reference/claude-config/sync-notes/README.md +43 -0
  38. package/reference/deployment/index.mdx +42 -7
  39. package/reference/examples/organization-model.ts +689 -0
  40. package/reference/index.mdx +6 -5
  41. package/reference/packages/core/src/README.md +39 -36
  42. package/reference/packages/core/src/business/README.md +52 -52
  43. package/reference/packages/core/src/organization-model/README.md +97 -97
  44. package/reference/packages/core/src/test-utils/README.md +42 -0
  45. package/reference/scaffold/core/organization-graph.mdx +272 -272
  46. package/reference/scaffold/core/organization-model.mdx +320 -320
  47. package/reference/scaffold/index.mdx +64 -64
  48. package/reference/scaffold/operations/propagation-pipeline.md +125 -104
  49. package/reference/scaffold/operations/scaffold-maintenance.md +122 -122
  50. package/reference/scaffold/operations/workflow-recipes.md +436 -436
  51. package/reference/scaffold/recipes/add-a-feature.md +158 -158
  52. package/reference/scaffold/recipes/add-a-resource.md +158 -158
  53. package/reference/scaffold/recipes/customize-organization-model.md +400 -400
  54. package/reference/scaffold/recipes/extend-a-base-entity.md +140 -140
  55. package/reference/scaffold/recipes/gate-by-feature-or-admin.md +158 -158
  56. package/reference/scaffold/recipes/index.md +32 -32
  57. package/reference/scaffold/reference/contracts.md +608 -607
  58. package/reference/scaffold/reference/feature-registry.md +2 -0
  59. package/reference/scaffold/reference/glossary.md +105 -105
  60. package/reference/scaffold/ui/composition-extensibility.mdx +1 -1
  61. package/reference/scaffold/ui/feature-flags-and-gating.md +1 -1
  62. package/reference/claude-config/commands/submit-request.md +0 -11
@@ -1,140 +1,140 @@
1
- ---
2
- title: Extend a Base Entity
3
- description: Add project-specific metadata to the canonical entity shapes (Project, Deal, Company, etc.) from @elevasis/core/entities using the TMeta extension slot.
4
- ---
5
-
6
- # Extend a Base Entity
7
-
8
- Workflows and UI features often operate on domain entities such as projects, deals, companies, or contacts. Rather than each project declaring its own shape from scratch, `@elevasis/core/entities` provides typed base interfaces generic over a `<TMeta>` slot. External projects extend these to add project-specific fields while keeping the canonical shape stable and interoperable with platform tooling.
9
-
10
- The canonical demo lives in `foundations/types/entities.ts` of any scaffold project.
11
-
12
- ---
13
-
14
- ## Available Base Entities
15
-
16
- Each entity ships as a TypeScript interface, a Zod schema, and an Input type:
17
-
18
- | Set | Description |
19
- | -------------------------------------------------------------- | ------------------------------------------- |
20
- | `BaseProject` / `BaseProjectSchema` / `BaseProjectInput` | Client or internal project record |
21
- | `BaseMilestone` / `BaseMilestoneSchema` / `BaseMilestoneInput` | Milestone within a project |
22
- | `BaseTask` / `BaseTaskSchema` / `BaseTaskInput` | Discrete task within a project or milestone |
23
- | `BaseDeal` / `BaseDealSchema` / `BaseDealInput` | Sales or partnership deal record |
24
- | `BaseCompany` / `BaseCompanySchema` / `BaseCompanyInput` | Company / account record |
25
- | `BaseContact` / `BaseContactSchema` / `BaseContactInput` | Individual contact record |
26
-
27
- All imports come from `@elevasis/core/entities`.
28
-
29
- ---
30
-
31
- ## Recipe 1 -- Extend a base entity with custom metadata
32
-
33
- Place this in `foundations/types/entities.ts`. This is the primary pattern -- the scaffold template ships this exact example.
34
-
35
- ```ts
36
- import { z } from 'zod'
37
- import type { BaseProject, BaseDeal } from '@elevasis/core/entities'
38
- import { BaseProjectSchema, BaseDealSchema } from '@elevasis/core/entities'
39
-
40
- // -- Project: extending metadata on a base entity --
41
-
42
- export const ProjectMetaSchema = z.object({
43
- budget: z.number().nonnegative(),
44
- clientPriority: z.enum(['low', 'medium', 'high'])
45
- })
46
-
47
- export type ProjectMeta = z.infer<typeof ProjectMetaSchema>
48
-
49
- export const ProjectSchema = BaseProjectSchema.extend({ metadata: ProjectMetaSchema })
50
-
51
- export type Project = BaseProject<ProjectMeta>
52
- ```
53
-
54
- Key points:
55
-
56
- - `BaseProjectSchema.extend({ metadata: ... })` merges your metadata Zod schema into the validated shape. Zod handles the rest.
57
- - `BaseProject<ProjectMeta>` infers the TypeScript type with your metadata typed correctly.
58
- - Only the `metadata` field is extended -- all other base fields (`id`, `organizationId`, `name`, `status`, `createdAt`, `updatedAt`, etc.) are inherited unchanged.
59
- - This file is the single source of truth for entity shapes across `foundations/`, `operations/`, and `ui/`.
60
-
61
- ---
62
-
63
- ## Recipe 2 -- Use a base entity as-is
64
-
65
- When the base shape covers everything the project needs, skip the extension entirely:
66
-
67
- ```ts
68
- import type { BaseDeal } from '@elevasis/core/entities'
69
- import { BaseDealSchema } from '@elevasis/core/entities'
70
-
71
- export const DealSchema = BaseDealSchema
72
-
73
- export type Deal = BaseDeal
74
- ```
75
-
76
- `BaseDeal` without a type argument defaults `TMeta` to an empty object (`{}`), which Zod validates as `z.object({})`. Use this path when the project has no per-deal custom fields -- you still get the canonical shape and full Zod validation for free.
77
-
78
- ---
79
-
80
- ## Recipe 3 -- Reference entity types from a workflow input schema
81
-
82
- Workflows that operate on projects or deals should reference the project-local entity types rather than redeclaring the shape. Add this to `operations/src/<workflow>/workflow.ts`:
83
-
84
- ```ts
85
- import { z } from 'zod'
86
- import type { WorkflowDefinition } from '@elevasis/sdk'
87
- import { ProjectSchema } from '@foundation/types/entities'
88
-
89
- export const myWorkflow: WorkflowDefinition = {
90
- id: 'my-workflow',
91
- inputSchema: z.object({
92
- project: ProjectSchema,
93
- notes: z.string().optional()
94
- }),
95
- // ...
96
- }
97
- ```
98
-
99
- The `@foundation/*` alias maps to the `foundations/` workspace package. This keeps entity contracts in one place and ensures the workflow input type matches whatever the UI passes as the execution payload.
100
-
101
- ---
102
-
103
- ## Recipe 4 -- Reference entity types from the UI
104
-
105
- UI components accept entity types directly in props. The entity type flows from the `foundations` package through the component into the workflow execution payload:
106
-
107
- ```tsx
108
- import type { Project } from '@foundation/types/entities'
109
-
110
- interface ProjectCardProps {
111
- project: Project
112
- onExecute?: (project: Project) => void
113
- }
114
-
115
- export function ProjectCard({ project, onExecute }: ProjectCardProps) {
116
- return (
117
- <button onClick={() => onExecute?.(project)}>
118
- Run workflow for {project.name}
119
- </button>
120
- )
121
- }
122
- ```
123
-
124
- When wiring this to a `RunResourceButton`, the entity instance becomes the workflow input. See UI Recipes recipe 6 (`Execute a Resource from a Surface`) at `../ui/recipes.md` for the end-to-end pattern where the entity type flows into a resource execution via `RunResourceButton`.
125
-
126
- ---
127
-
128
- ## Verification
129
-
130
- - **Type-check foundations:** `pnpm -C foundations test` runs the Vitest suite. The template ships `foundations/types/entities.test.ts` as a working smoke-check -- it `safeParse`s a valid project (expects success) and an invalid one (expects failure). Run this after any schema change.
131
- - **Round-trip safeParse:** Call `ProjectSchema.safeParse(candidateObject)` in a test or REPL to confirm the Zod shape accepts the data your workflows and UI will produce.
132
- - **Cross-package type check:** `pnpm -C ui build` will surface any TypeScript errors if a UI component or hook passes a mismatched entity type to a workflow input.
133
-
134
- ---
135
-
136
- ## Cross-references
137
-
138
- - [./add-a-resource.md](./add-a-resource.md) -- resource authoring that consumes entity types in `inputSchema`
139
- - [../ui/recipes.md](../ui/recipes.md) recipe 6 -- execute a resource from a surface, with entity-typed input via `RunResourceButton`
140
- - [../reference/contracts.md](../reference/contracts.md) -- auto-generated TypeScript contract shapes for all Organization OS types
1
+ ---
2
+ title: Extend a Base Entity
3
+ description: Add project-specific metadata to the canonical entity shapes (Project, Deal, Company, etc.) from @elevasis/core/entities using the TMeta extension slot.
4
+ ---
5
+
6
+ # Extend a Base Entity
7
+
8
+ Workflows and UI features often operate on domain entities such as projects, deals, companies, or contacts. Rather than each project declaring its own shape from scratch, `@elevasis/core/entities` provides typed base interfaces generic over a `<TMeta>` slot. External projects extend these to add project-specific fields while keeping the canonical shape stable and interoperable with platform tooling.
9
+
10
+ The canonical demo lives in `foundations/types/entities.ts` of any scaffold project.
11
+
12
+ ---
13
+
14
+ ## Available Base Entities
15
+
16
+ Each entity ships as a TypeScript interface, a Zod schema, and an Input type:
17
+
18
+ | Set | Description |
19
+ | -------------------------------------------------------------- | ------------------------------------------- |
20
+ | `BaseProject` / `BaseProjectSchema` / `BaseProjectInput` | Client or internal project record |
21
+ | `BaseMilestone` / `BaseMilestoneSchema` / `BaseMilestoneInput` | Milestone within a project |
22
+ | `BaseTask` / `BaseTaskSchema` / `BaseTaskInput` | Discrete task within a project or milestone |
23
+ | `BaseDeal` / `BaseDealSchema` / `BaseDealInput` | Sales or partnership deal record |
24
+ | `BaseCompany` / `BaseCompanySchema` / `BaseCompanyInput` | Company / account record |
25
+ | `BaseContact` / `BaseContactSchema` / `BaseContactInput` | Individual contact record |
26
+
27
+ All imports come from `@elevasis/core/entities`.
28
+
29
+ ---
30
+
31
+ ## Recipe 1 -- Extend a base entity with custom metadata
32
+
33
+ Place this in `foundations/types/entities.ts`. This is the primary pattern -- the scaffold template ships this exact example.
34
+
35
+ ```ts
36
+ import { z } from 'zod'
37
+ import type { BaseProject, BaseDeal } from '@elevasis/core/entities'
38
+ import { BaseProjectSchema, BaseDealSchema } from '@elevasis/core/entities'
39
+
40
+ // -- Project: extending metadata on a base entity --
41
+
42
+ export const ProjectMetaSchema = z.object({
43
+ budget: z.number().nonnegative(),
44
+ clientPriority: z.enum(['low', 'medium', 'high'])
45
+ })
46
+
47
+ export type ProjectMeta = z.infer<typeof ProjectMetaSchema>
48
+
49
+ export const ProjectSchema = BaseProjectSchema.extend({ metadata: ProjectMetaSchema })
50
+
51
+ export type Project = BaseProject<ProjectMeta>
52
+ ```
53
+
54
+ Key points:
55
+
56
+ - `BaseProjectSchema.extend({ metadata: ... })` merges your metadata Zod schema into the validated shape. Zod handles the rest.
57
+ - `BaseProject<ProjectMeta>` infers the TypeScript type with your metadata typed correctly.
58
+ - Only the `metadata` field is extended -- all other base fields (`id`, `organizationId`, `name`, `status`, `createdAt`, `updatedAt`, etc.) are inherited unchanged.
59
+ - This file is the single source of truth for entity shapes across `foundations/`, `operations/`, and `ui/`.
60
+
61
+ ---
62
+
63
+ ## Recipe 2 -- Use a base entity as-is
64
+
65
+ When the base shape covers everything the project needs, skip the extension entirely:
66
+
67
+ ```ts
68
+ import type { BaseDeal } from '@elevasis/core/entities'
69
+ import { BaseDealSchema } from '@elevasis/core/entities'
70
+
71
+ export const DealSchema = BaseDealSchema
72
+
73
+ export type Deal = BaseDeal
74
+ ```
75
+
76
+ `BaseDeal` without a type argument defaults `TMeta` to an empty object (`{}`), which Zod validates as `z.object({})`. Use this path when the project has no per-deal custom fields -- you still get the canonical shape and full Zod validation for free.
77
+
78
+ ---
79
+
80
+ ## Recipe 3 -- Reference entity types from a workflow input schema
81
+
82
+ Workflows that operate on projects or deals should reference the project-local entity types rather than redeclaring the shape. Add this to `operations/src/<workflow>/workflow.ts`:
83
+
84
+ ```ts
85
+ import { z } from 'zod'
86
+ import type { WorkflowDefinition } from '@elevasis/sdk'
87
+ import { ProjectSchema } from '@foundation/types/entities'
88
+
89
+ export const myWorkflow: WorkflowDefinition = {
90
+ id: 'my-workflow',
91
+ inputSchema: z.object({
92
+ project: ProjectSchema,
93
+ notes: z.string().optional()
94
+ }),
95
+ // ...
96
+ }
97
+ ```
98
+
99
+ The `@foundation/*` alias maps to the `foundations/` workspace package. This keeps entity contracts in one place and ensures the workflow input type matches whatever the UI passes as the execution payload.
100
+
101
+ ---
102
+
103
+ ## Recipe 4 -- Reference entity types from the UI
104
+
105
+ UI components accept entity types directly in props. The entity type flows from the `foundations` package through the component into the workflow execution payload:
106
+
107
+ ```tsx
108
+ import type { Project } from '@foundation/types/entities'
109
+
110
+ interface ProjectCardProps {
111
+ project: Project
112
+ onExecute?: (project: Project) => void
113
+ }
114
+
115
+ export function ProjectCard({ project, onExecute }: ProjectCardProps) {
116
+ return (
117
+ <button onClick={() => onExecute?.(project)}>
118
+ Run workflow for {project.name}
119
+ </button>
120
+ )
121
+ }
122
+ ```
123
+
124
+ When wiring this to a `RunResourceButton`, the entity instance becomes the workflow input. See UI Recipes recipe 6 (`Execute a Resource from a Surface`) at `../ui/recipes.md` for the end-to-end pattern where the entity type flows into a resource execution via `RunResourceButton`.
125
+
126
+ ---
127
+
128
+ ## Verification
129
+
130
+ - **Type-check foundations:** `pnpm -C foundations test` runs the Vitest suite. The template ships `foundations/types/entities.test.ts` as a working smoke-check -- it `safeParse`s a valid project (expects success) and an invalid one (expects failure). Run this after any schema change.
131
+ - **Round-trip safeParse:** Call `ProjectSchema.safeParse(candidateObject)` in a test or REPL to confirm the Zod shape accepts the data your workflows and UI will produce.
132
+ - **Cross-package type check:** `pnpm -C ui build` will surface any TypeScript errors if a UI component or hook passes a mismatched entity type to a workflow input.
133
+
134
+ ---
135
+
136
+ ## Cross-references
137
+
138
+ - [./add-a-resource.md](./add-a-resource.md) -- resource authoring that consumes entity types in `inputSchema`
139
+ - [../ui/recipes.md](../ui/recipes.md) recipe 6 -- execute a resource from a surface, with entity-typed input via `RunResourceButton`
140
+ - [../reference/contracts.md](../reference/contracts.md) -- auto-generated TypeScript contract shapes for all Organization OS types
@@ -1,158 +1,158 @@
1
- ---
2
- title: Gate by Feature or Admin
3
- description: Decision table and step-by-step recipes for gating routes, nav items, and UI elements by feature flag or admin role.
4
- ---
5
-
6
- # Gate by Feature or Admin
7
-
8
- End-to-end: gate a route, nav item, or UI element. Steps are sequential.
9
-
10
- See [glossary.md](../reference/glossary.md) under **FeatureGuard**, **AdminGuard**, **accessFeatureKey**, **featureKey**, **Settings asymmetry**, and **MembershipFeatureConfig**. See [contracts.md](../reference/contracts.md) for `MembershipFeatureConfig` shape and the settings-asymmetry callout.
11
-
12
- For the three-concept model in full detail, see [feature-flags-and-gating.md](../ui/feature-flags-and-gating.md).
13
-
14
- ---
15
-
16
- ## 1. Decide: feature-level or admin-level?
17
-
18
- | Scenario | Gate to use |
19
- | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
20
- | Surface should be off by default for all members, toggled org-wide or per member | `FeatureGuard` + `featureKey` on nav entry |
21
- | Surface is always visible to authenticated members but restricted to admins only | `AdminGuard` + `requiresAdmin` on nav entry |
22
- | Surface is both feature-gated and admin-only | Both: `FeatureGuard` wrapping `AdminGuard`, plus both nav-entry fields |
23
-
24
- Do not substitute one for the other. `FeatureGuard` reads feature flags; `AdminGuard` reads admin role. They are independent.
25
-
26
- ---
27
-
28
- ## 2. Feature gate -- org level
29
-
30
- Ensure a feature object with the matching `id` exists in the org model. File: `foundations/config/organization-model.ts`.
31
-
32
- ```ts
33
- import { defineOrganizationModel, OPERATIONS_FEATURE_ID, SALES_FEATURE_ID } from '@elevasis/core/organization-model'
34
-
35
- // In the features[] array inside defineOrganizationModel:
36
- features: [
37
- // ... existing features
38
- { id: 'analytics', label: 'Analytics', enabled: false, entityIds: [], surfaceIds: [], resourceIds: [], capabilityIds: [] }
39
- ]
40
- ```
41
-
42
- Use typed constants for the 7 platform features (`SALES_FEATURE_ID`, `OPERATIONS_FEATURE_ID`, etc.); use string literals for project-specific features you invent. In this example, `'analytics'` is a project-local feature and stays as a string literal.
43
-
44
- See [add-a-feature.md](add-a-feature.md) step 2 for the full feature object shape.
45
-
46
- Set `enabled: true` to enable for all members by default.
47
-
48
- ---
49
-
50
- ## 3. Feature gate -- route level
51
-
52
- Wrap the route layout with `FeatureGuard` inside `ProtectedRoute`. The template-local `FeatureGuard` resolves key aliases automatically.
53
-
54
- ```tsx
55
- import { ProtectedRoute } from '@/features/auth'
56
- import { FeatureGuard } from '@/features/auth/guards/FeatureGuard'
57
- import { createFileRoute, Outlet } from '@tanstack/react-router'
58
-
59
- export const Route = createFileRoute('/analytics')({ component: AnalyticsLayout })
60
-
61
- function AnalyticsLayout() {
62
- return (
63
- <ProtectedRoute>
64
- <FeatureGuard featureKey="analytics">
65
- <Outlet />
66
- </FeatureGuard>
67
- </ProtectedRoute>
68
- )
69
- }
70
- ```
71
-
72
- `FeatureGuard` redirects to `/` with a Mantine notification when the feature is off. All child routes under this layout are automatically protected -- no need to repeat the guard in children.
73
-
74
- Import path: `@/features/auth/guards/FeatureGuard` (template-local). Do not import `FeatureGuard` from `@elevasis/ui/features` -- that version does not close over the local org model. See [feature-flags-and-gating.md](../ui/feature-flags-and-gating.md) for the import-paths table.
75
-
76
- ---
77
-
78
- ## 4. Admin gate -- route level
79
-
80
- Wrap with `AdminGuard` inside `ProtectedRoute`. `AdminGuard` redirects non-admins to `redirectTo` (default `/`).
81
-
82
- ```tsx
83
- import { ProtectedRoute } from '@/features/auth'
84
- import { AdminGuard } from '@elevasis/ui/auth'
85
- import { createFileRoute, Outlet } from '@tanstack/react-router'
86
-
87
- export const Route = createFileRoute('/admin')({ component: AdminLayout })
88
-
89
- function AdminLayout() {
90
- return (
91
- <ProtectedRoute>
92
- <AdminGuard>
93
- <Outlet />
94
- </AdminGuard>
95
- </ProtectedRoute>
96
- )
97
- }
98
- ```
99
-
100
- Import path: `@elevasis/ui/auth` (published). Always nest inside `ProtectedRoute` -- the guard depends on the user profile being loaded.
101
-
102
- ---
103
-
104
- ## 5. Nav-item visibility
105
-
106
- Declare `featureKey` on the nav entry to have the shell auto-hide it when the feature is off. Declare `requiresAdmin: true` to hide it for non-admin members.
107
-
108
- ```ts
109
- // In FeatureModule.navEntry (manifest-backed features):
110
- navEntry: {
111
- label: 'Analytics',
112
- icon: IconChartBar,
113
- link: '/analytics',
114
- featureKey: 'analytics' // optional -- featureId already gates the whole module
115
- }
116
-
117
- // In nav-items.ts (app-local nav):
118
- { label: 'Analytics', icon: IconChartBar, link: '/analytics', featureKey: 'analytics' }
119
- { label: 'Admin', icon: IconShield, link: '/admin', requiresAdmin: true }
120
- ```
121
-
122
- `featureKey` on a nav entry is a display hint only -- it does not protect the route. Always pair with a route-level `FeatureGuard`. See [glossary.md](../reference/glossary.md) under **accessFeatureKey vs featureKey** for the distinction. When the `featureKey` is one of the 7 platform features, prefer the typed constant (e.g., `featureKey: SALES_FEATURE_ID`) over a string literal to catch renames at compile time.
123
-
124
- ---
125
-
126
- ## 6. Per-member override
127
-
128
- `MembershipFeatureConfig.features` (stored in `org_memberships.config`) overrides org-level defaults per member. Full shape is in [contracts.md](../reference/contracts.md#membershipfeatureconfig).
129
-
130
- ```ts
131
- // Disable analytics for a specific member (stored in their membership record):
132
- {
133
- features: { analytics: false }
134
- }
135
- ```
136
-
137
- `MembershipFeatureConfig.features` is `Record<string, boolean>` -- a dynamic map keyed by feature ID. Any feature ID from the org model can be overridden per member without schema changes.
138
-
139
- **Settings asymmetry -- read this before writing membership config.** The `settings` feature is intentionally excluded from per-member overrides: settings access is controlled by `requiresAdmin` and `AdminGuard`, not per-member feature flags. Writing a `settings` key to `org_memberships.config` has no effect. See [contracts.md](../reference/contracts.md#membershipfeatureconfig) for the full callout, and [glossary.md](../reference/glossary.md) under **Settings asymmetry**.
140
-
141
- Membership config is read/written directly via the Supabase client (`org_memberships.config` column). There is no dedicated API route for bulk updates.
142
-
143
- ---
144
-
145
- ## 7. Verify
146
-
147
- State matrix -- confirm each combination:
148
-
149
- | Org feature enabled | Member override | Admin | Expected result |
150
- | ------------------- | --------------- | --------- | --------------------------------------------- |
151
- | true | (absent) | any | Route accessible, nav visible |
152
- | true | false | any | Route redirects, nav hidden |
153
- | false | (absent) | any | Route redirects, nav hidden |
154
- | false | true | any | Route redirects (org gate wins) |
155
- | true | (absent) | non-admin | Admin-gated route redirects, admin nav hidden |
156
- | true | (absent) | admin | Admin-gated route accessible, nav visible |
157
-
158
- Check each gate independently: feature toggle in `foundations/config/organization-model.ts`, membership config in `org_memberships.config`, and admin status in the user profile.
1
+ ---
2
+ title: Gate by Feature or Admin
3
+ description: Decision table and step-by-step recipes for gating routes, nav items, and UI elements by feature flag or admin role.
4
+ ---
5
+
6
+ # Gate by Feature or Admin
7
+
8
+ End-to-end: gate a route, nav item, or UI element. Steps are sequential.
9
+
10
+ See [glossary.md](../reference/glossary.md) under **FeatureGuard**, **AdminGuard**, **featureId**, **featureKey**, **Settings asymmetry**, and **MembershipFeatureConfig**. See [contracts.md](../reference/contracts.md) for `MembershipFeatureConfig` shape and the settings-asymmetry callout.
11
+
12
+ For the three-concept model in full detail, see [feature-flags-and-gating.md](../ui/feature-flags-and-gating.md).
13
+
14
+ ---
15
+
16
+ ## 1. Decide: feature-level or admin-level?
17
+
18
+ | Scenario | Gate to use |
19
+ | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
20
+ | Surface should be off by default for all members, toggled org-wide or per member | `FeatureGuard` + `featureKey` on nav entry |
21
+ | Surface is always visible to authenticated members but restricted to admins only | `AdminGuard` + `requiresAdmin` on nav entry |
22
+ | Surface is both feature-gated and admin-only | Both: `FeatureGuard` wrapping `AdminGuard`, plus both nav-entry fields |
23
+
24
+ Do not substitute one for the other. `FeatureGuard` reads feature flags; `AdminGuard` reads admin role. They are independent.
25
+
26
+ ---
27
+
28
+ ## 2. Feature gate -- org level
29
+
30
+ Ensure a feature object with the matching `id` exists in the org model. File: `foundations/config/organization-model.ts`.
31
+
32
+ ```ts
33
+ import { defineOrganizationModel, OPERATIONS_FEATURE_ID, SALES_FEATURE_ID } from '@elevasis/core/organization-model'
34
+
35
+ // In the features[] array inside defineOrganizationModel:
36
+ features: [
37
+ // ... existing features
38
+ { id: 'analytics', label: 'Analytics', enabled: false, entityIds: [], surfaceIds: [], resourceIds: [], capabilityIds: [] }
39
+ ]
40
+ ```
41
+
42
+ Use typed constants for the 7 platform features (`SALES_FEATURE_ID`, `OPERATIONS_FEATURE_ID`, etc.); use string literals for project-specific features you invent. In this example, `'analytics'` is a project-local feature and stays as a string literal.
43
+
44
+ See [add-a-feature.md](add-a-feature.md) step 2 for the full feature object shape.
45
+
46
+ Set `enabled: true` to enable for all members by default.
47
+
48
+ ---
49
+
50
+ ## 3. Feature gate -- route level
51
+
52
+ Wrap the route layout with `FeatureGuard` inside `ProtectedRoute`. The template-local `FeatureGuard` resolves key aliases automatically.
53
+
54
+ ```tsx
55
+ import { ProtectedRoute } from '@/features/auth'
56
+ import { FeatureGuard } from '@/features/auth/guards/FeatureGuard'
57
+ import { createFileRoute, Outlet } from '@tanstack/react-router'
58
+
59
+ export const Route = createFileRoute('/analytics')({ component: AnalyticsLayout })
60
+
61
+ function AnalyticsLayout() {
62
+ return (
63
+ <ProtectedRoute>
64
+ <FeatureGuard featureKey="analytics">
65
+ <Outlet />
66
+ </FeatureGuard>
67
+ </ProtectedRoute>
68
+ )
69
+ }
70
+ ```
71
+
72
+ `FeatureGuard` redirects to `/` with a Mantine notification when the feature is off. All child routes under this layout are automatically protected -- no need to repeat the guard in children.
73
+
74
+ Import path: `@/features/auth/guards/FeatureGuard` (template-local). Do not import `FeatureGuard` from `@elevasis/ui/features` -- that version does not close over the local org model. See [feature-flags-and-gating.md](../ui/feature-flags-and-gating.md) for the import-paths table.
75
+
76
+ ---
77
+
78
+ ## 4. Admin gate -- route level
79
+
80
+ Wrap with `AdminGuard` inside `ProtectedRoute`. `AdminGuard` redirects non-admins to `redirectTo` (default `/`).
81
+
82
+ ```tsx
83
+ import { ProtectedRoute } from '@/features/auth'
84
+ import { AdminGuard } from '@elevasis/ui/auth'
85
+ import { createFileRoute, Outlet } from '@tanstack/react-router'
86
+
87
+ export const Route = createFileRoute('/admin')({ component: AdminLayout })
88
+
89
+ function AdminLayout() {
90
+ return (
91
+ <ProtectedRoute>
92
+ <AdminGuard>
93
+ <Outlet />
94
+ </AdminGuard>
95
+ </ProtectedRoute>
96
+ )
97
+ }
98
+ ```
99
+
100
+ Import path: `@elevasis/ui/auth` (published). Always nest inside `ProtectedRoute` -- the guard depends on the user profile being loaded.
101
+
102
+ ---
103
+
104
+ ## 5. Nav-item visibility
105
+
106
+ Declare `featureKey` on the nav entry to have the shell auto-hide it when the feature is off. Declare `requiresAdmin: true` to hide it for non-admin members.
107
+
108
+ ```ts
109
+ // In FeatureModule.navEntry (manifest-backed features):
110
+ navEntry: {
111
+ label: 'Analytics',
112
+ icon: IconChartBar,
113
+ link: '/analytics',
114
+ featureKey: 'analytics' // optional -- featureId already gates the whole module
115
+ }
116
+
117
+ // In nav-items.ts (app-local nav):
118
+ { label: 'Analytics', icon: IconChartBar, link: '/analytics', featureKey: 'analytics' }
119
+ { label: 'Admin', icon: IconShield, link: '/admin', requiresAdmin: true }
120
+ ```
121
+
122
+ `featureKey` on a nav entry is a display hint only -- it does not protect the route. Always pair with a route-level `FeatureGuard`. See [glossary.md](../reference/glossary.md) under **featureId vs featureKey** for the distinction. When the `featureKey` is one of the 7 platform features, prefer the typed constant (e.g., `featureKey: SALES_FEATURE_ID`) over a string literal to catch renames at compile time.
123
+
124
+ ---
125
+
126
+ ## 6. Per-member override
127
+
128
+ `MembershipFeatureConfig.features` (stored in `org_memberships.config`) overrides org-level defaults per member. Full shape is in [contracts.md](../reference/contracts.md#membershipfeatureconfig).
129
+
130
+ ```ts
131
+ // Disable analytics for a specific member (stored in their membership record):
132
+ {
133
+ features: { analytics: false }
134
+ }
135
+ ```
136
+
137
+ `MembershipFeatureConfig.features` is `Record<string, boolean>` -- a dynamic map keyed by feature ID. Any feature ID from the org model can be overridden per member without schema changes.
138
+
139
+ **Settings asymmetry -- read this before writing membership config.** The `settings` feature is intentionally excluded from per-member overrides: settings access is controlled by `requiresAdmin` and `AdminGuard`, not per-member feature flags. Writing a `settings` key to `org_memberships.config` has no effect. See [contracts.md](../reference/contracts.md#membershipfeatureconfig) for the full callout, and [glossary.md](../reference/glossary.md) under **Settings asymmetry**.
140
+
141
+ Membership config is read/written directly via the Supabase client (`org_memberships.config` column). There is no dedicated API route for bulk updates.
142
+
143
+ ---
144
+
145
+ ## 7. Verify
146
+
147
+ State matrix -- confirm each combination:
148
+
149
+ | Org feature enabled | Member override | Admin | Expected result |
150
+ | ------------------- | --------------- | --------- | --------------------------------------------- |
151
+ | true | (absent) | any | Route accessible, nav visible |
152
+ | true | false | any | Route redirects, nav hidden |
153
+ | false | (absent) | any | Route redirects, nav hidden |
154
+ | false | true | any | Route redirects (org gate wins) |
155
+ | true | (absent) | non-admin | Admin-gated route redirects, admin nav hidden |
156
+ | true | (absent) | admin | Admin-gated route accessible, nav visible |
157
+
158
+ Check each gate independently: feature toggle in `foundations/config/organization-model.ts`, membership config in `org_memberships.config`, and admin status in the user profile.