spine-framework 0.4.4 → 0.5.0-rc.4

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 (283) hide show
  1. package/.env.example +13 -3
  2. package/.framework/cli/commands/bootstrap.ts +392 -0
  3. package/.framework/cli/commands/dev.ts +91 -218
  4. package/.framework/cli/commands/doctor.ts +132 -26
  5. package/.framework/cli/commands/generate.ts +38 -298
  6. package/.framework/cli/commands/init.ts +20 -1
  7. package/.framework/cli/commands/migrate.ts +36 -30
  8. package/.framework/cli/commands/migrations.ts +16 -56
  9. package/.framework/cli/commands/upgrade.ts +35 -0
  10. package/.framework/cli/commands/validate-schema.ts +147 -0
  11. package/.framework/cli/context.ts +223 -110
  12. package/.framework/cli/index.ts +6 -0
  13. package/.framework/cli/lib/project-files.ts +241 -1
  14. package/.framework/functions/_shared/hash-api-key.ts +183 -79
  15. package/.framework/functions/_shared/logging.ts +562 -224
  16. package/.framework/functions/_shared/middleware.ts +47 -44
  17. package/.framework/functions/admin-data.ts +39 -28
  18. package/.framework/functions/ai-agents.ts +10 -6
  19. package/.framework/functions/api-keys.ts +301 -122
  20. package/.framework/functions/apps.ts +43 -26
  21. package/.framework/functions/cil-ingest.ts +48 -15
  22. package/.framework/functions/debug-auth.ts +14 -8
  23. package/.framework/functions/deliveries.ts +39 -12
  24. package/.framework/functions/embeddings.ts +145 -76
  25. package/.framework/functions/integration-outbound.ts +67 -27
  26. package/.framework/functions/integrations.ts +254 -127
  27. package/.framework/functions/logs.ts +273 -133
  28. package/.framework/functions/observability.ts +277 -147
  29. package/.framework/functions/pipeline-executions.ts +1 -1
  30. package/.framework/functions/pipelines.ts +14 -9
  31. package/.framework/functions/prompt-configs.ts +7 -4
  32. package/.framework/functions/roles.ts +8 -5
  33. package/.framework/functions/security.ts +30 -17
  34. package/.framework/functions/system-cron.ts +240 -127
  35. package/.framework/functions/tests.ts +7 -6
  36. package/.framework/functions/timers.ts +310 -107
  37. package/.framework/functions/triggers.ts +28 -13
  38. package/.framework/functions/types.ts +13 -10
  39. package/.framework/migrations/069_log_stats_window.sql +54 -0
  40. package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
  41. package/.framework/migrations/071_timer_interval_keys.sql +96 -0
  42. package/.framework/migrations/072_slug_autofill.sql +77 -0
  43. package/.framework/migrations/073_seed_member_roles.sql +26 -0
  44. package/.framework/src/apps/admin/index.tsx +1141 -775
  45. package/.framework/src/components/ErrorBoundary.tsx +136 -0
  46. package/.framework/src/components/admin/AdminListPage.tsx +81 -73
  47. package/.framework/src/components/admin/AdminSidebar.tsx +88 -79
  48. package/.framework/src/components/admin/EntitySelect.tsx +181 -0
  49. package/.framework/src/components/admin/PageInstructions.tsx +86 -127
  50. package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
  51. package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
  52. package/.framework/src/components/layout/Sidebar.tsx +300 -177
  53. package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
  54. package/.framework/src/components/runtime/DataHeader.tsx +96 -48
  55. package/.framework/src/components/runtime/DataListPage.tsx +3 -26
  56. package/.framework/src/components/runtime/DataStats.tsx +10 -10
  57. package/.framework/src/components/runtime/DataTable.tsx +12 -10
  58. package/.framework/src/components/ui/sheet.tsx +24 -26
  59. package/.framework/src/components/ui/textarea.tsx +74 -48
  60. package/.framework/src/hooks/useEntityList.ts +12 -9
  61. package/.framework/src/hooks/useListSchema.ts +108 -58
  62. package/.framework/src/instructions/README.md +57 -0
  63. package/.framework/src/instructions/ai-agents.md +69 -0
  64. package/.framework/src/instructions/alerts.md +80 -0
  65. package/.framework/src/instructions/api-keys.md +87 -0
  66. package/.framework/src/instructions/apps.md +77 -0
  67. package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
  68. package/.framework/src/instructions/concepts/apps-and-renderers.md +162 -0
  69. package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
  70. package/.framework/src/instructions/concepts/automation.md +186 -0
  71. package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
  72. package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
  73. package/.framework/src/instructions/concepts/design-schema.md +152 -0
  74. package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
  75. package/.framework/src/instructions/concepts/observability.md +116 -0
  76. package/.framework/src/instructions/concepts/permissions.md +150 -0
  77. package/.framework/src/instructions/concepts/runtime-records.md +111 -0
  78. package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
  79. package/.framework/src/instructions/concepts/spine-way.md +172 -0
  80. package/.framework/src/instructions/deliveries.md +59 -0
  81. package/.framework/src/instructions/guide/00-orientation.md +106 -0
  82. package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
  83. package/.framework/src/instructions/guide/02-project-map.md +147 -0
  84. package/.framework/src/instructions/guide/03-domain-model.md +223 -0
  85. package/.framework/src/instructions/guide/04-first-app.md +227 -0
  86. package/.framework/src/instructions/guide/05-glossary.md +183 -0
  87. package/.framework/src/instructions/integrations.md +96 -0
  88. package/.framework/src/instructions/internal/cil-markers.md +83 -0
  89. package/.framework/src/instructions/invites.md +60 -0
  90. package/.framework/src/instructions/kb.md +80 -0
  91. package/.framework/src/instructions/logs.md +77 -0
  92. package/.framework/src/instructions/observability.md +61 -0
  93. package/.framework/src/instructions/operations/deployment.md +115 -0
  94. package/.framework/src/instructions/operations/migrations.md +125 -0
  95. package/.framework/src/instructions/operations/security-checklist.md +148 -0
  96. package/.framework/src/instructions/operations/troubleshooting.md +170 -0
  97. package/.framework/src/instructions/operations/upgrade.md +116 -0
  98. package/.framework/src/instructions/pipeline-executions.md +62 -0
  99. package/.framework/src/instructions/pipelines.md +78 -0
  100. package/.framework/src/instructions/prompts.md +95 -0
  101. package/.framework/src/instructions/reference/api/admin-data.md +162 -0
  102. package/.framework/src/instructions/reference/api/auth.md +113 -0
  103. package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
  104. package/.framework/src/instructions/reference/app-manifest.md +150 -0
  105. package/.framework/src/instructions/reference/cli.md +124 -0
  106. package/.framework/src/instructions/reference/database.md +132 -0
  107. package/.framework/src/instructions/reference/design-schema.md +280 -0
  108. package/.framework/src/instructions/reference/env-vars.md +133 -0
  109. package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
  110. package/.framework/src/instructions/reference/functions/admin-data.md +195 -0
  111. package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
  112. package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
  113. package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
  114. package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
  115. package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
  116. package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
  117. package/.framework/src/instructions/reference/functions/apps.md +140 -0
  118. package/.framework/src/instructions/reference/functions/auth.md +99 -0
  119. package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
  120. package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
  121. package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
  122. package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
  123. package/.framework/src/instructions/reference/functions/index.md +103 -0
  124. package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
  125. package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
  126. package/.framework/src/instructions/reference/functions/integrations.md +130 -0
  127. package/.framework/src/instructions/reference/functions/invites.md +182 -0
  128. package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
  129. package/.framework/src/instructions/reference/functions/kb.md +166 -0
  130. package/.framework/src/instructions/reference/functions/logs.md +141 -0
  131. package/.framework/src/instructions/reference/functions/observability.md +105 -0
  132. package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
  133. package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
  134. package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
  135. package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
  136. package/.framework/src/instructions/reference/functions/roles.md +121 -0
  137. package/.framework/src/instructions/reference/functions/security.md +140 -0
  138. package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
  139. package/.framework/src/instructions/reference/functions/system.md +106 -0
  140. package/.framework/src/instructions/reference/functions/tests.md +97 -0
  141. package/.framework/src/instructions/reference/functions/timers.md +117 -0
  142. package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
  143. package/.framework/src/instructions/reference/functions/triggers.md +140 -0
  144. package/.framework/src/instructions/reference/functions/types.md +146 -0
  145. package/.framework/src/instructions/reference/seeds.md +126 -0
  146. package/.framework/src/instructions/reference/shared-modules.md +127 -0
  147. package/.framework/src/instructions/reference/spine-config.md +114 -0
  148. package/.framework/src/instructions/roles.md +80 -0
  149. package/.framework/src/instructions/runtime-detail.md +70 -0
  150. package/.framework/src/instructions/runtime-list.md +77 -0
  151. package/.framework/src/instructions/security.md +67 -0
  152. package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
  153. package/.framework/src/instructions/tasks/add-a-role.md +96 -0
  154. package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
  155. package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
  156. package/.framework/src/instructions/tasks/add-a-type.md +151 -0
  157. package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
  158. package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
  159. package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
  160. package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
  161. package/.framework/src/instructions/tasks/package-and-install-an-app.md +127 -0
  162. package/.framework/src/instructions/tasks/test-your-code.md +108 -0
  163. package/.framework/src/instructions/tasks/write-a-function.md +132 -0
  164. package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
  165. package/.framework/src/instructions/testing.md +59 -0
  166. package/.framework/src/instructions/timers.md +82 -0
  167. package/.framework/src/instructions/triggers.md +69 -0
  168. package/.framework/src/instructions/types.md +93 -0
  169. package/.framework/src/pages/NotFoundPage.tsx +62 -31
  170. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
  171. package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
  172. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
  173. package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
  174. package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
  175. package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
  176. package/.framework/src/pages/admin/AppsPage.tsx +104 -66
  177. package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
  178. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
  179. package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
  180. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
  181. package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
  182. package/.framework/src/pages/admin/LogsPage.tsx +144 -76
  183. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
  184. package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
  185. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
  186. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
  187. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
  188. package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
  189. package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
  190. package/.framework/src/pages/admin/RolesPage.tsx +86 -43
  191. package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
  192. package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
  193. package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
  194. package/.framework/src/pages/admin/TimersPage.tsx +90 -62
  195. package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
  196. package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
  197. package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
  198. package/.framework/src/pages/admin/TypesPage.tsx +90 -48
  199. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
  200. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
  201. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
  202. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
  203. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
  204. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
  205. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
  206. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
  207. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
  208. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
  209. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
  210. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
  211. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
  212. package/.framework/templates/agent/AGENTS.md +150 -0
  213. package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
  214. package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
  215. package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
  216. package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
  217. package/.framework/templates/agent/skills/spine-install-app/SKILL.md +72 -0
  218. package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
  219. package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
  220. package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
  221. package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -0
  222. package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
  223. package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
  224. package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
  225. package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
  226. package/.framework/templates/project.json +4 -1
  227. package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
  228. package/.framework/tests/unit/agent-files.test.ts +355 -0
  229. package/.framework/tests/unit/agent-runner.test.ts +479 -44
  230. package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
  231. package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
  232. package/.framework/tests/unit/bootstrap.test.ts +242 -0
  233. package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
  234. package/.framework/tests/unit/debug-auth.test.ts +38 -0
  235. package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
  236. package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
  237. package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
  238. package/.framework/tests/unit/logging-app-id.test.ts +75 -0
  239. package/.framework/tests/unit/system-context-denial.test.ts +145 -0
  240. package/.framework/tests/unit/system-cron.test.ts +240 -0
  241. package/.framework/tests/unit/timers.test.ts +159 -0
  242. package/README.md +125 -166
  243. package/config/tsconfig.cli.json +2 -2
  244. package/package.json +15 -4
  245. package/templates/project/package-lock.json +2 -2
  246. package/.framework/docs/admin-routes.md +0 -76
  247. package/.framework/docs/api-docs-progress.md +0 -38
  248. package/.framework/docs/api-governance.md +0 -146
  249. package/.framework/docs/api-testing-results.md +0 -212
  250. package/.framework/docs/assembly-launch-guide.md +0 -138
  251. package/.framework/docs/audit-results.md +0 -590
  252. package/.framework/docs/db-api-inventory.md +0 -95
  253. package/.framework/docs/gap-remediation-backlog.md +0 -103
  254. package/.framework/docs/ui-adoption-verification.md +0 -111
  255. package/.framework/docs/ui-api-coverage.md +0 -84
  256. package/.framework/docs/v2-compatibility-audit.md +0 -228
  257. package/.framework/mcp/index.ts +0 -133
  258. package/.framework/mcp/load-env.ts +0 -60
  259. package/.framework/mcp/shared/app-manifest.ts +0 -275
  260. package/.framework/mcp/shared/db.ts +0 -477
  261. package/.framework/mcp/shared/kb.ts +0 -118
  262. package/.framework/mcp/tools/architect.ts +0 -431
  263. package/.framework/mcp/tools/cil/context.ts +0 -345
  264. package/.framework/mcp/tools/cil/graph.ts +0 -242
  265. package/.framework/mcp/tools/cil/ingest.ts +0 -1217
  266. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
  267. package/.framework/mcp/tools/cil/pipeline.ts +0 -767
  268. package/.framework/mcp/tools/cil/project.ts +0 -791
  269. package/.framework/mcp/tools/cil/retrieval.ts +0 -336
  270. package/.framework/mcp/tools/cil/search-core.ts +0 -454
  271. package/.framework/mcp/tools/cil/search.ts +0 -97
  272. package/.framework/mcp/tools/cil/suggest.ts +0 -186
  273. package/.framework/mcp/tools/cil/validate.ts +0 -1369
  274. package/.framework/mcp/tools/kb/delete.ts +0 -139
  275. package/.framework/mcp/tools/kb/index.ts +0 -52
  276. package/.framework/mcp/tools/kb/ingest.ts +0 -84
  277. package/.framework/mcp/tools/kb/list-types.ts +0 -102
  278. package/.framework/mcp/tools/kb/publish.ts +0 -100
  279. package/.framework/mcp/tools/kb/reindex.ts +0 -86
  280. package/.framework/mcp/tools/kb/revoke.ts +0 -85
  281. package/.framework/mcp/tools/kb/search.ts +0 -101
  282. package/.framework/mcp/tools/kb/upsert.ts +0 -125
  283. package/.framework/mcp/tools/observability.ts +0 -510
@@ -0,0 +1,150 @@
1
+ ---
2
+ title: Permission Model
3
+ audience: developers, agents
4
+ kind: concept
5
+ verified_against: f69c37d
6
+ verified_on: 2026-10-29
7
+ source_files:
8
+ - .framework/functions/_shared/permissions.ts
9
+ - .framework/functions/_shared/principal.ts
10
+ - .framework/functions/_shared/db.ts
11
+ - .framework/functions/_shared/middleware.ts
12
+ - .framework/functions/admin-data.ts
13
+ - .framework/migrations/001_core_schema.sql
14
+ - .framework/migrations/002_core_rls_and_functions.sql
15
+ - .framework/migrations/003_core_system_data.sql
16
+ ---
17
+
18
+ # Permission Model
19
+
20
+ Spine authorization is two independent layers, and the code says so verbatim:
21
+ **"RLS controls row access; design_schema controls what the principal can do.
22
+ No permissions granted = none given."** (`.framework/functions/_shared/permissions.ts`)
23
+
24
+ - **Row visibility (RLS):** Postgres policies scope every tenant table to the
25
+ accounts the caller can reach — see [Scope and Accounts](scope-and-accounts.md).
26
+ - **Action authorization (`record_permissions`):** per request, the permission
27
+ engine evaluates the caller's role against the schema **stamped on the
28
+ record** to decide create/read/update/delete and per-field read/write.
29
+
30
+ ## Principals
31
+
32
+ `.framework/functions/_shared/principal.ts` resolves two principal types:
33
+
34
+ - **Human** — Supabase JWT → `people` row → `role_id` join to `roles`; the
35
+ principal carries `roles: [slug]` and gets a JWT-scoped DB client (`ctx.db`),
36
+ so RLS applies.
37
+ - **Machine** — `x-api-key` header → `api_keys` row → `scopes` array and the
38
+ key's `account_id`. Machine principals get `adminDb` (service role), which
39
+ **bypasses RLS entirely**; their only authorization is the scope check.
40
+
41
+ ## The three surfaces
42
+
43
+ `detectSurface` in `.framework/functions/_shared/permissions.ts` buckets the
44
+ target by table name:
45
+
46
+ | Surface | Covers | Rule |
47
+ |---|---|---|
48
+ | First (default) | runtime records — items, threads, messages, people, accounts, … | `record_permissions` on the record's stamped `design_schema` |
49
+ | Second | config objects — `apps`, `pipelines`, `triggers`, `ai_agents`, `embeddings`, `timers`, `integrations`, `roles`, `types`, `prompt_configs` | `system_admin` full access; machine principals read-only; everyone else denied |
50
+ | Third | system metadata — `logs`, `pipeline_executions`, `trigger_executions`, `link_types`, `links` | `system_admin`/machine full; humans read only their own/account rows |
51
+
52
+ ## First-surface evaluation
53
+
54
+ `resolveFirstSurfacePermissions` runs on every runtime-record access:
55
+
56
+ 1. **Schema source is the record.** The stamped `design_schema` on the record
57
+ is authoritative; there is deliberately no fallback to the type row — a
58
+ record with no schema or no `record_permissions` is denied.
59
+ 2. **Role resolution.** The caller's role slug comes from
60
+ `people.role_id → roles.slug` (one role per person, cached ~60s). No role,
61
+ no permissions.
62
+ 3. **Record grant.** `record_permissions[roleSlug]` is an array of actions
63
+ (`create`, `read`, `update`, `delete`). The special key `'all'` is a
64
+ wildcard evaluated for every authenticated principal — grants to `all`
65
+ apply to anyone who passed RLS.
66
+ 4. **Field merge.** For each declared field, `fields.<name>.permissions[role]`
67
+ — an array drawn from `read`/`write` only — replaces the default. Fields
68
+ **without** an entry default to `{ read: canRead, write: canUpdate }` from
69
+ the record grant. That is why `record_permissions` is the base, most
70
+ restrictive grant and field entries are *overrides*: an explicit `["read"]`
71
+ on a field removes write access that `update` would otherwise give.
72
+
73
+ > **Role keys are matched verbatim.** `record_permissions` keys must be real
74
+ > `roles.slug` values — nothing normalizes them. The seeded admin slug is
75
+ > `system_admin` (underscore, `.framework/migrations/003_core_system_data.sql`);
76
+ > a `system-admin` key silently matches nobody and denies the admin.
77
+
78
+ ## Enforcement points
79
+
80
+ - **Read** (`sanitizeFirstSurfaceRecordData`): `canRead` gates the record;
81
+ `data` is filtered per field by `fieldPermissions[name].read`; values are
82
+ formatted through `validation_schema` (`formatFieldData`); legacy `metadata`
83
+ is stripped from responses. Records with an empty `fields` map pass through
84
+ whole at the record grant.
85
+ - **Update** (`validateFirstSurfaceUpdatePermissions`): `canUpdate` gates the
86
+ record, then **every key inside `data` must have `write`** — one denied field
87
+ fails the whole update with a per-field error. Values are sanitized through
88
+ `sanitizeFieldData` (type coercion + constraint checks; violations are
89
+ errors, not silent drops). Top-level non-`data` keys pass through the gate.
90
+ Writes therefore require both the record action *and* field-level write —
91
+ undeclared fields have no `fieldPermissions` entry and 403.
92
+ - **Create** (`admin-data` `create`): permission is checked against the type's
93
+ `design_schema` (which becomes the record's stamp) before insert;
94
+ `type_id`/`type_slug` is required. A machine principal with `*:*` scope
95
+ bypasses the create check.
96
+
97
+ ## `system_admin` bypass
98
+
99
+ `isSystemAdmin` (`roles` contains `system_admin`) short-circuits record
100
+ access, sanitization, and update validation in `permissions.ts`, and gates the
101
+ `types.ts`/`roles.ts`/`apps.ts` write APIs entirely. The same slug drives the
102
+ RLS bypass inside `get_accessible_accounts` — system admins see all accounts.
103
+
104
+ ## Machine (API key) scopes
105
+
106
+ `checkMachineScope` grants an action when the key's `scopes` contain
107
+ `resource:action` exactly, `resource:*`, or `*:*` (e.g. `items:read`,
108
+ `items:*`, `*:*`). Scopes are the *only* thing standing between a leaked API
109
+ key and `adminDb`, so issue the narrowest scope that works — `*:*` also
110
+ bypasses the create permission check in `admin-data`.
111
+
112
+ ## Canonical example
113
+
114
+ The seeded pattern from the framework migrations — base grant first, field
115
+ override only where needed:
116
+
117
+ ```json
118
+ {
119
+ "record_permissions": {
120
+ "system_admin": ["create", "read", "update", "delete"],
121
+ "member_admin": ["create", "read", "update"],
122
+ "member": ["create", "read", "update"]
123
+ },
124
+ "fields": {
125
+ "title": { "data_type": "text", "label": "Title", "required": true, "system": true },
126
+ "status": { "data_type": "select", "label": "Status", "required": true,
127
+ "options": ["open", "closed"],
128
+ "permissions": { "member": ["read"] } },
129
+ "internal_notes": { "data_type": "textarea", "label": "Internal Notes",
130
+ "required": false,
131
+ "permissions": { "member": [] } }
132
+ }
133
+ }
134
+ ```
135
+
136
+ Reading it: `member` can create and update records, so every declared field
137
+ defaults to read+write — except `status`, whose `["read"]` override strips
138
+ `member`'s write, and `internal_notes`, whose empty override hides it from
139
+ `member` entirely. `member_admin` touches neither override, so it inherits
140
+ record-level read+write on both. That is the whole discipline: **add a
141
+ `fields.*.permissions` entry only where the base grant is wrong for that
142
+ field** — usually to restrict a sensitive or `system: true` field, occasionally
143
+ to loosen one the base grant closes.
144
+
145
+ ## See also
146
+
147
+ - [Design Schema](design-schema.md) — where `record_permissions` lives
148
+ - [Scope and Accounts](scope-and-accounts.md) — the RLS layer beneath this one
149
+ - [../roles.md](../roles.md) — the role entity (`permissions` on roles bound
150
+ the *subject* side at the app/UI level)
@@ -0,0 +1,111 @@
1
+ ---
2
+ title: Runtime Records
3
+ audience: developers, agents
4
+ kind: concept
5
+ verified_against: f69c37d
6
+ verified_on: 2026-10-29
7
+ source_files:
8
+ - .framework/functions/admin-data.ts
9
+ - .framework/functions/_shared/resolve-ids.ts
10
+ - .framework/functions/_shared/permissions.ts
11
+ - .framework/migrations/001_core_schema.sql
12
+ - .framework/src/hooks/useEntityRecord.ts
13
+ - .framework/src/components/app-shell/GenericDetailPage.tsx
14
+ ---
15
+
16
+ # Runtime Records
17
+
18
+ Runtime records are the rows your users and apps actually create — as opposed
19
+ to config entities (`types`, `apps`, `roles`, `pipelines`, …) that describe
20
+ the system itself. They share one storage contract: a type, an account, a
21
+ stamped schema, and a split between framework-owned columns and app-owned
22
+ `data`.
23
+
24
+ ## The record kinds
25
+
26
+ `admin-data` maps entity names to type `kind`s via `ENTITY_TO_KIND`
27
+ (`.framework/functions/admin-data.ts`):
28
+
29
+ | Entity (table) | `kind` | What it is |
30
+ |---|---|---|
31
+ | `items` | `item` | The workhorse — tickets, tasks, articles, any typed business object |
32
+ | `threads` | `thread` | A conversation bound to a target (`target_type` + `target_id`); unique per `(target_type, target_id, visibility)` |
33
+ | `messages` | `message` | An entry in a thread (`thread_id` + `sequence`, unique together) |
34
+ | `links` | `link` | A polymorphic edge between two records (`source_type`/`source_id` → `target_type`/`target_id`, optional `link_type`) |
35
+ | `attachments` | `attachment` | A stored file reference (filename, size, mime, `storage_path`) |
36
+ | `watchers` | `watcher` | A person subscribed to a target (unique per target+person) |
37
+ | `people` | `person` | Users/contacts — also carry `auth_uid`, `role_id`, `email` |
38
+ | `accounts` | `account` | Tenants — the only table without `account_id` (it *is* the account) |
39
+ | `item_progress` | `item_progress` | Progress/status snapshots for items |
40
+
41
+ `VALID_ENTITIES` also accepts `logs`, `test_runs`, `observability`, and
42
+ `test_cases` — the `RAW_ENTITIES` path: whitelisted raw-column inserts with no
43
+ type requirement and no `design_schema`. `types` and `link_types` are accepted
44
+ too but are config, not runtime records.
45
+
46
+ ## The shared row shape
47
+
48
+ Every runtime table in `.framework/migrations/001_core_schema.sql` carries the
49
+ same spine:
50
+
51
+ - `id`, `created_at`, `updated_at`, `created_by` (+ `updated_by` on most)
52
+ - `type_id uuid NOT NULL` — every record must reference a type
53
+ - `account_id` — the owning tenant (all except `accounts`)
54
+ - `app_id` — optional attribution to the app that created it
55
+ - `data jsonb` — app-defined field values
56
+ - `design_schema jsonb`, `validation_schema jsonb` — **stamped at creation**
57
+ from the type; the runtime source of truth for permissions and validation
58
+ (see [Design Schema](design-schema.md))
59
+ - `is_active` — soft-delete flag; `remove` flips it rather than deleting
60
+
61
+ `admin-data` create requires `type_id` *or* `type_slug` — `resolveTypeRecord`
62
+ in `.framework/functions/_shared/resolve-ids.ts` resolves `(kind, slug)` to
63
+ the type row, and the record is stamped before permission evaluation and
64
+ insert.
65
+
66
+ ## `system: true` columns vs `record.data`
67
+
68
+ The field declaration's `system` flag decides where a value lives:
69
+
70
+ - **`system: true`** → a top-level record column (`record.title`,
71
+ `record.status`, `record.email`). These are framework-owned or
72
+ schema-critical fields.
73
+ - **`system: false` / omitted** → inside `record.data.<field>`.
74
+
75
+ Consumers split on the flag — this is the authoritative convention:
76
+
77
+ - `useEntityRecord.save` partitions form data into `systemData` (top-level
78
+ keys) and `customData` (packed into `data`) by `fieldDef.system`
79
+ (`.framework/src/hooks/useEntityRecord.ts`).
80
+ - `GenericDetailPage` initializes each field from `record[field]` when
81
+ `fieldDef.system` and `record.data[field]` otherwise
82
+ (`.framework/src/components/app-shell/GenericDetailPage.tsx`).
83
+ - The permission engine's write gate evaluates every key inside `data`
84
+ against `fieldPermissions[name].write` and sanitizes through
85
+ `validation_schema` (`.framework/functions/_shared/permissions.ts`).
86
+
87
+ **Fact:** send system fields as top-level keys and non-system fields inside a
88
+ `data` object. A non-system field sent top-level is stored but invisible to
89
+ schema-driven readers; a system field sent inside `data` never reaches its
90
+ column.
91
+
92
+ ## Reading records back
93
+
94
+ Reads pass through `sanitizeRecordData`: fields the caller can't read are
95
+ dropped from `data`, the legacy `metadata` key is stripped from responses, and
96
+ remaining values are formatted through `validation_schema`
97
+ (`formatFieldData` — e.g. currency, multiselect, dates). A record the caller
98
+ can't read at all collapses to `{id, created_at, updated_at}` — so "record
99
+ exists but looks empty" is a permission answer, not missing data.
100
+
101
+ ## Writing rules of thumb
102
+
103
+ - `type_slug` + `entity` on create; `id` on update/delete.
104
+ - Never write `design_schema`, `validation_schema`, `type_id`, `account_id`,
105
+ or `id` in the body — create strips them before insert (`account_id` in the
106
+ body is honored as a *targeting* hint, see
107
+ [Scope and Accounts](scope-and-accounts.md)).
108
+ - Deletes are soft (`is_active = false`) for the standard entities; only a few
109
+ entities hard-delete.
110
+ - Creates fire triggers (`fireCreateTriggers`) and per-type rate limits after
111
+ permission checks — a custom insert path loses both.
@@ -0,0 +1,112 @@
1
+ ---
2
+ title: Scope and Accounts
3
+ audience: developers, agents
4
+ kind: concept
5
+ verified_against: f69c37d
6
+ verified_on: 2026-10-29
7
+ source_files:
8
+ - .framework/migrations/001_core_schema.sql
9
+ - .framework/migrations/002_core_rls_and_functions.sql
10
+ - .framework/migrations/011_spine_system_account.sql
11
+ - .framework/migrations/013_get_account_apps_role_filter.sql
12
+ - .framework/migrations/015_types_kind_slug_unique.sql
13
+ - .framework/functions/admin-data.ts
14
+ - .framework/functions/apps.ts
15
+ - .framework/functions/_shared/principal.ts
16
+ - .framework/functions/_shared/db.ts
17
+ ---
18
+
19
+ # Scope and Accounts
20
+
21
+ Accounts are Spine's tenancy primitive: every runtime record belongs to
22
+ exactly one `account_id`, and that column is what both RLS and the app layer
23
+ use to partition tenants. `design_schema.scope` is the small lever that decides
24
+ *which* account a record belongs to.
25
+
26
+ ## The tenancy model
27
+
28
+ - **`public.accounts`** — the tenant (organization, workspace, customer).
29
+ Accounts form a hierarchy through `parent_id`; the `account_paths` closure
30
+ table stores every ancestor→descendant pair so subtree lookups are one join.
31
+ Accounts have no `account_id` of their own — the table is the source of
32
+ truth.
33
+ - **`public.people`** — a person belongs to exactly one account
34
+ (`people.account_id NOT NULL`) and holds one `role_id`. A person is *also* a
35
+ runtime record: `type_id`, `data`, and a stamped `design_schema`.
36
+ - **Everything else** — `items`, `threads`, `messages`, `links`,
37
+ `attachments`, `watchers`, `item_progress` — carries
38
+ `account_id NOT NULL` (or via its parent thread for `messages`).
39
+
40
+ ## How scope decides `account_id`
41
+
42
+ `admin-data` create resolves the record's account in order
43
+ (`.framework/functions/admin-data.ts`, `create`):
44
+
45
+ 1. An explicit `account_id` in the request body, else the caller's
46
+ `ctx.accountId`.
47
+ 2. If the record references a parent (`thread_id`, `target_id`, `parent_id`),
48
+ the **parent's** `account_id` wins — and the parent's stamped `scope` is
49
+ inherited into the new record's `design_schema`. A child can never drift
50
+ into a different tenant than its parent.
51
+ 3. `design_schema.scope` then applies: `'account'` (the default) keeps the
52
+ resolved account; `'platform'` replaces it with the **spine-system**
53
+ account's id.
54
+
55
+ So `scope` on a type is really "which tenant owns these records": your
56
+ account's rows, or the platform's shared rows.
57
+
58
+ ## The `spine-system` account
59
+
60
+ Seeded by `.framework/migrations/011_spine_system_account.sql`:
61
+ `accounts.slug = 'spine-system'` — "Master system account for internal Spine
62
+ operations." It is the landing account for `scope: 'platform'` records. The
63
+ `accounts` table itself is the exception to the scoping rule: it has no
64
+ `account_id` column for scope to land on, so for `accounts` records
65
+ `admin-data` instead sets `parent_id` to the creator's account when none is
66
+ given — new accounts are born as *children* of the account that created them.
67
+
68
+ ## How scope affects queries
69
+
70
+ - **Human callers** query through `ctx.db` — a Supabase client created with the
71
+ caller's JWT (`.framework/functions/_shared/principal.ts`,
72
+ `getPrincipalDb`). RLS policies on the tenant tables filter rows to
73
+ `account_id IN (SELECT get_accessible_accounts(current_actor_id()))`
74
+ (`.framework/migrations/002_core_rls_and_functions.sql`).
75
+ `current_actor_id()` maps `auth.uid()` to the `people` row;
76
+ `get_accessible_accounts` returns that person's account **plus every
77
+ descendant** via `account_paths` — a parent-account user sees the whole
78
+ subtree. If the person holds `system_admin`, the function short-circuits and
79
+ returns *all* accounts.
80
+ - **Machine callers** (API keys) use `adminDb`, which bypasses RLS; tenancy is
81
+ enforced only by the key's `account_id` and `scopes` — see
82
+ [Permissions](permissions.md). A `*:*` key reads every tenant.
83
+ - **Platform-scoped rows** carry the `spine-system` `account_id`, so under
84
+ account-based RLS they are visible to the spine-system account subtree and
85
+ to `system_admin` (who sees all accounts) — not to ordinary tenant users
86
+ querying through `ctx.db`. Machine principals on `adminDb` reach them
87
+ regardless, subject to key scopes.
88
+
89
+ ## What is *not* scoped by account
90
+
91
+ - **Types are global.** `public.types` uniqueness is `(kind, slug)`
92
+ (`.framework/migrations/015_types_kind_slug_unique.sql`) — deliberately not
93
+ per-app or per-account, so `resolveTypeRecord(kind, slug)` `.single()`
94
+ lookups stay deterministic and every account shares the same type registry.
95
+ - **Apps are global** rows in `public.apps`, linked to accounts only through
96
+ `owner_account_id` and the `get_account_apps` RPC, which filters by the
97
+ caller's role slugs (`.framework/functions/apps.ts`;
98
+ `.framework/migrations/013_get_account_apps_role_filter.sql`).
99
+ - **Roles** are global slugs matched verbatim by the permission engine.
100
+
101
+ ## Practical consequences
102
+
103
+ - Put `scope: 'platform'` only on types whose records should exist once for the
104
+ whole installation (system config, shared registries) — never on tenant
105
+ data.
106
+ - Child records inherit the parent's account and scope; you cannot re-tenant a
107
+ record by pointing it at a parent in another account — the parent's account
108
+ wins, silently.
109
+ - Because types are global, pick type slugs as if they were API names:
110
+ `kind + slug` must be unique across every app installed in the deployment.
111
+ - Account-scoped tests and seeds must set `account_id` on every record or RLS
112
+ will make them invisible to human-scoped queries.
@@ -0,0 +1,172 @@
1
+ ---
2
+ title: The Spine Way
3
+ audience: developers, agents
4
+ kind: concept
5
+ verified_against: f69c37d
6
+ verified_on: 2026-10-29
7
+ source_files:
8
+ - scripts/boundary-check.sh
9
+ - config/vite.config.ts
10
+ - config/tsconfig.json
11
+ - .framework/functions/admin-data.ts
12
+ - .framework/functions/_shared/permissions.ts
13
+ - .framework/functions/_shared/principal.ts
14
+ - .framework/functions/_shared/db.ts
15
+ - .framework/cli/commands/migrate.ts
16
+ - .framework/cli/commands/upgrade.ts
17
+ - .framework/migrations/001_core_schema.sql
18
+ - .framework/migrations/002_core_rls_and_functions.sql
19
+ - .framework/migrations/011_spine_system_account.sql
20
+ - .framework/src/hooks/useEntityRecord.ts
21
+ - .framework/src/components/app-shell/GenericDetailPage.tsx
22
+ ---
23
+
24
+ # The Spine Way
25
+
26
+ Ten rules every app, function, migration, and type must satisfy to stay aligned
27
+ with the framework. Each rule is stated as a quotable invariant, followed by
28
+ why it exists and what actually breaks when you violate it. Deeper treatment of
29
+ each topic lives in the sibling concept pages.
30
+
31
+ ## 1. Core never imports from custom
32
+
33
+ `.framework/` must not import from `custom/`, `custom_*` modules, or hardcode
34
+ custom app slugs.
35
+
36
+ **Why:** `.framework/` is a sealed, upgradeable npm package — it must run
37
+ identically in every project.
38
+
39
+ **Breaks:** `scripts/boundary-check.sh` fails the build (checks 1–3), and the
40
+ next `upgrade` either aborts on locally modified framework files or silently
41
+ overwrites the coupling.
42
+
43
+ ## 2. Custom code imports core through `@core/*`, never relative paths
44
+
45
+ Custom frontend code uses `@core/components/...`, `@core/hooks/...`; custom
46
+ functions import `./_shared/...` after assembly (`@shared/*` in
47
+ `config/tsconfig.functions.json`).
48
+
49
+ **Why:** `@core` maps to `.framework/src` in `config/tsconfig.json` and to the
50
+ assembled tree in `config/vite.config.ts` — the two roots custom code is
51
+ actually built against.
52
+
53
+ **Breaks:** `scripts/boundary-check.sh` check 2b flags relative `../.framework`
54
+ imports in `custom/`, and paths that resolve today break the moment the
55
+ assembly layout changes.
56
+
57
+ ## 3. Apps are registered data, not static imports
58
+
59
+ An app is a `manifest.json` plus a row in `public.apps`; the shell mounts it at
60
+ runtime by `route_prefix` and `renderer`.
61
+
62
+ **Why:** registration is what makes an app discoverable, permissionable
63
+ (`min_role` / `required_roles`), and toggleable (`is_active`) without a core
64
+ redeploy.
65
+
66
+ **Breaks:** statically importing an app component into `.framework/src/`
67
+ violates rule 1 and bypasses the role gate in
68
+ `.framework/src/components/AppWrapper.tsx` — the app is always visible and
69
+ always mounted.
70
+
71
+ ## 4. Runtime record CRUD goes through `/api/admin-data`
72
+
73
+ All creates, reads, updates, and deletes of `items`, `threads`, `messages`,
74
+ `links`, `attachments`, `watchers`, `people`, `accounts` go through the single
75
+ `admin-data` endpoint.
76
+
77
+ **Why:** that endpoint is where type resolution, `design_schema` stamping,
78
+ permission evaluation, audit logging, and trigger dispatch actually live — see
79
+ `.framework/functions/admin-data.ts`.
80
+
81
+ **Breaks:** a custom function wrapper skips record permissions, field-level
82
+ write gates, `validation_schema` sanitization, audit rows, and create/update
83
+ triggers — the record lands in the table but nothing that should have run ran.
84
+
85
+ ## 5. Behavior lives in `design_schema`, not handler branches
86
+
87
+ Fields, views, record permissions, scope, and functionality bindings belong on
88
+ the type's `design_schema`; the runtime stamps a copy onto every record at
89
+ creation.
90
+
91
+ **Why:** the permission engine and generic renderers read the schema off the
92
+ record — `.framework/functions/_shared/permissions.ts` denies outright when a
93
+ record has no stamped `record_permissions`.
94
+
95
+ **Breaks:** hardcoded per-type branches can't be permissioned, can't render in
96
+ the generic UI, and drift from the stamped schema records already carry.
97
+
98
+ ## 6. `record_permissions` is the base grant; field permissions are overrides
99
+
100
+ Start with the most restrictive `record_permissions` per role. A field without
101
+ a `fields.<name>.permissions` entry inherits `read` from `canRead` and `write`
102
+ from `canUpdate`; only add a field-level entry to change that default.
103
+
104
+ **Why:** the merge in `resolveFirstSurfacePermissions` applies the override
105
+ verbatim — `["read"]` on a field removes `write` for that role even when the
106
+ record grant allows it.
107
+
108
+ **Breaks:** re-stating record-level grants per field silently locks fields, and
109
+ leaving `system: true` fields un-overridden lets any record-writer modify
110
+ columns like `status` or `slug`.
111
+
112
+ ## 7. RLS decides which rows you see; design_schema decides what you can do
113
+
114
+ Every tenant table has RLS enabled and policies scope rows through
115
+ `get_accessible_accounts(current_actor_id())`. Human requests query through
116
+ `ctx.db` (JWT-scoped); `adminDb` bypasses RLS and is reserved for service-role
117
+ work.
118
+
119
+ **Why:** this is the second, independent layer of authorization — RLS cannot
120
+ express per-type, per-role, per-field grants.
121
+
122
+ **Breaks:** a table without RLS leaks every tenant's rows to every caller; a
123
+ handler that uses `adminDb` "for convenience" turns your permission model into
124
+ decoration.
125
+
126
+ ## 8. Migrations are idempotent and ordered
127
+
128
+ Filenames are `NNN_description.sql`; every statement must be safe to re-run
129
+ (`CREATE ... IF NOT EXISTS`, `DROP POLICY IF EXISTS`, `ON CONFLICT DO NOTHING`,
130
+ guarded `DO $$` blocks). `migrate` applies `.framework/migrations` first, then
131
+ `custom/migrations`, each sorted by name.
132
+
133
+ **Why:** the same migration set runs against fresh and existing databases, and
134
+ later migrations legitimately depend on earlier objects.
135
+
136
+ **Breaks:** a non-idempotent migration fails on second run and wedges every
137
+ environment that already applied it; unordered migrations crash on missing
138
+ dependencies.
139
+
140
+ ## 9. `system: true` fields live on the record; everything else lives in `data`
141
+
142
+ Fields declared `system: true` read and write top-level record columns
143
+ (`record.title`); all other fields live inside `record.data.<field>`.
144
+
145
+ **Why:** the split is how the platform separates framework-owned columns from
146
+ app-defined data — `.framework/src/hooks/useEntityRecord.ts` splits form data
147
+ by `fieldDef.system` on every save, and
148
+ `.framework/src/components/app-shell/GenericDetailPage.tsx` reads the same way.
149
+
150
+ **Breaks:** sending a non-system field as a top-level key writes it outside the
151
+ field-permission gate's `data` object (invisible to renderers); sending a
152
+ system field inside `data` never reaches its column.
153
+
154
+ ## 10. Secrets and service keys stay on the backend
155
+
156
+ Browser code may use `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` only.
157
+ `SUPABASE_SERVICE_ROLE_KEY`, `SUPABASE_DB_PASSWORD`, and app secrets appear
158
+ only in `.framework/functions/`, `.framework/cli/`, and uncommitted env files.
159
+
160
+ **Why:** `adminDb` is a service-role client — whoever holds the service key
161
+ holds an RLS bypass for the entire database.
162
+
163
+ **Breaks:** a service key bundled into frontend code is a credential leak with
164
+ full read/write on every tenant's data; committed env files leak it to every
165
+ clone of the repo.
166
+
167
+ ## Applying the rules
168
+
169
+ Before shipping a change, walk the list and check each rule that could apply.
170
+ When a rule genuinely must bend, prefer the weakest violation (data over code,
171
+ config over core) and document it — most "exceptions" turn out to be a missing
172
+ `design_schema` section or a missing app manifest entry.
@@ -0,0 +1,59 @@
1
+ ---
2
+ title: Deliveries
3
+ audience: both
4
+ kind: entity
5
+ verified_against: e13127d
6
+ verified_on: 2026-10-30
7
+ source_files:
8
+ - .framework/functions/deliveries.ts
9
+ - .framework/functions/integration-outbound.ts
10
+ - .framework/functions/_shared/retry-deliveries.ts
11
+ - .framework/src/pages/admin/DeliveriesPage.tsx
12
+ ---
13
+
14
+ ## What this is for
15
+
16
+ **Deliveries** is the outbound delivery log — every webhook/integration delivery attempt, its status, retries, and response. It is primarily a diagnostic, read-mostly page: use it to confirm an event left Spine, to debug a failing endpoint, and to see retry behavior.
17
+
18
+ ## Fields and settings
19
+
20
+ - **Direction** — `inbound` (external webhook into Spine) or `outbound` (Spine calling an integration).
21
+ - **Status** — `pending`, `processing`, `delivered`, or `failed`. Pending rows with a future `next_attempt_at` are scheduled retries.
22
+ - **Integration** — the `integration_id` the delivery ran through.
23
+ - **Handler / Action** — `handler_name` (inbound webhook handler) or `action_slug` that produced it.
24
+ - **Attempts** — `attempt_count / max_attempts`. `max_attempts` defaults to 3, overridable per call (`action_config.max_attempts`) or per integration (`config.retry_policy.max_attempts`).
25
+ - **Next Attempt / Created** — scheduled retry time and queue time. `response_status` and `last_error` on the row capture the last attempt's result.
26
+
27
+ **Downstream effects**
28
+
29
+ - A `failed` row means the endpoint rejected or timed out and `attempt_count` reached `max_attempts` — fix the endpoint or credentials, then retry via the API (`deliveries?action=retry`, outbound only) or re-fire the event.
30
+ - Retries reuse the same row: `attempt_count` increments and `next_attempt_at` follows exponential backoff (30s × 2^attempt, capped at 1h, +20% jitter). `system-cron` sweeps due `pending` rows each tick.
31
+ - Deliveries are produced by `integration-outbound` (outbound) and `integration-routes` (inbound) — an empty list with active webhooks usually means no events matched, not a broken delivery layer.
32
+ - Rows are append-only diagnostics; there is nothing to create or edit on this page.
33
+
34
+ ## Minimal configuration
35
+
36
+ N/A — deliveries are generated by outbound calls, not authored. To produce one: create an `integration-routes`/webhook integration (or a trigger with an outbound stage), then emit its event.
37
+
38
+ ## Full configuration
39
+
40
+ N/A — see `integrations` for endpoint/credential configuration, `triggers`/`pipelines` for what fires deliveries.
41
+
42
+ ## Example
43
+
44
+ A support app's `customer-webhooks` integration posts every `ticket.closed` event to the customer's endpoint; a `failed` delivery with response `401` points at an expired token — rotate it in the integration's `credentials`, not here.
45
+
46
+ ## Core files
47
+
48
+ - `.framework/functions/deliveries.ts` — API (list/get/`action=retry`)
49
+ - `.framework/functions/integration-outbound.ts` — outbound broker that writes delivery rows
50
+ - `.framework/functions/integration-routes.ts` — inbound webhook path that writes inbound rows
51
+ - `.framework/functions/_shared/retry-deliveries.ts` — scheduled sweep of due `pending` rows
52
+ - `.framework/src/pages/admin/DeliveriesPage.tsx` — admin UI
53
+ - Table: `public.webhook_deliveries`
54
+
55
+ ## Agent notes
56
+
57
+ - Debug order: delivery row → integration config/credentials → the event producer (trigger/pipeline). The delivery row is the *symptom*, not the cause.
58
+ - Repeated failures across one endpoint usually mean credentials or URL drift; failures across many endpoints point at the outbound broker or networking.
59
+ - Delivery volume doubles as an activity signal for integrations — a silent integration shows up here first.