spine-framework 0.4.4 → 0.5.0-rc.5

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 (305) 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/install-app.ts +183 -13
  8. package/.framework/cli/commands/migrate.ts +36 -30
  9. package/.framework/cli/commands/migrations.ts +16 -56
  10. package/.framework/cli/commands/upgrade.ts +35 -0
  11. package/.framework/cli/commands/validate-app.ts +127 -1
  12. package/.framework/cli/commands/validate-schema.ts +147 -0
  13. package/.framework/cli/context.ts +223 -110
  14. package/.framework/cli/index.ts +6 -0
  15. package/.framework/cli/lib/project-files.ts +241 -1
  16. package/.framework/functions/_shared/app-manifest.ts +2 -0
  17. package/.framework/functions/_shared/hash-api-key.ts +183 -79
  18. package/.framework/functions/_shared/logging.ts +562 -224
  19. package/.framework/functions/_shared/middleware.ts +47 -44
  20. package/.framework/functions/_shared/permissions.ts +4 -0
  21. package/.framework/functions/admin-data.ts +51 -28
  22. package/.framework/functions/ai-agents.ts +10 -6
  23. package/.framework/functions/api-keys.ts +301 -122
  24. package/.framework/functions/apps.ts +63 -29
  25. package/.framework/functions/cil-ingest.ts +48 -15
  26. package/.framework/functions/debug-auth.ts +14 -8
  27. package/.framework/functions/deliveries.ts +39 -12
  28. package/.framework/functions/embeddings.ts +149 -78
  29. package/.framework/functions/integration-outbound.ts +67 -27
  30. package/.framework/functions/integrations.ts +254 -127
  31. package/.framework/functions/logs.ts +273 -133
  32. package/.framework/functions/observability.ts +278 -148
  33. package/.framework/functions/pipeline-executions.ts +1 -1
  34. package/.framework/functions/pipelines.ts +14 -9
  35. package/.framework/functions/prompt-configs.ts +7 -4
  36. package/.framework/functions/roles.ts +8 -5
  37. package/.framework/functions/security.ts +30 -17
  38. package/.framework/functions/system-cron.ts +240 -127
  39. package/.framework/functions/tests.ts +7 -6
  40. package/.framework/functions/timers.ts +310 -107
  41. package/.framework/functions/triggers.ts +28 -13
  42. package/.framework/functions/types.ts +13 -10
  43. package/.framework/migrations/069_log_stats_window.sql +54 -0
  44. package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
  45. package/.framework/migrations/071_timer_interval_keys.sql +96 -0
  46. package/.framework/migrations/072_slug_autofill.sql +77 -0
  47. package/.framework/migrations/073_seed_member_roles.sql +26 -0
  48. package/.framework/migrations/074_app_route_prefix_domains.sql +168 -0
  49. package/.framework/migrations/075_security_definer_grants.sql +157 -0
  50. package/.framework/src/apps/admin/index.tsx +1141 -775
  51. package/.framework/src/components/ErrorBoundary.tsx +136 -0
  52. package/.framework/src/components/admin/AdminListPage.tsx +81 -73
  53. package/.framework/src/components/admin/AdminSidebar.tsx +88 -79
  54. package/.framework/src/components/admin/EntitySelect.tsx +181 -0
  55. package/.framework/src/components/admin/PageInstructions.tsx +86 -127
  56. package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
  57. package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
  58. package/.framework/src/components/layout/Sidebar.tsx +300 -177
  59. package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
  60. package/.framework/src/components/runtime/DataHeader.tsx +96 -48
  61. package/.framework/src/components/runtime/DataListPage.tsx +3 -26
  62. package/.framework/src/components/runtime/DataStats.tsx +10 -10
  63. package/.framework/src/components/runtime/DataTable.tsx +12 -10
  64. package/.framework/src/components/ui/sheet.tsx +24 -26
  65. package/.framework/src/components/ui/textarea.tsx +74 -48
  66. package/.framework/src/contexts/AppContext.tsx +5 -12
  67. package/.framework/src/contexts/AuthContext.tsx +17 -9
  68. package/.framework/src/hooks/useApps.ts +18 -22
  69. package/.framework/src/hooks/useEntityList.ts +12 -9
  70. package/.framework/src/hooks/useListSchema.ts +108 -58
  71. package/.framework/src/instructions/README.md +74 -0
  72. package/.framework/src/instructions/accounts.md +97 -0
  73. package/.framework/src/instructions/ai-agents.md +69 -0
  74. package/.framework/src/instructions/alerts.md +80 -0
  75. package/.framework/src/instructions/api-keys.md +87 -0
  76. package/.framework/src/instructions/apps.md +77 -0
  77. package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
  78. package/.framework/src/instructions/concepts/apps-and-renderers.md +228 -0
  79. package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
  80. package/.framework/src/instructions/concepts/automation.md +186 -0
  81. package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
  82. package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
  83. package/.framework/src/instructions/concepts/design-schema.md +152 -0
  84. package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
  85. package/.framework/src/instructions/concepts/observability.md +116 -0
  86. package/.framework/src/instructions/concepts/permissions.md +150 -0
  87. package/.framework/src/instructions/concepts/runtime-records.md +111 -0
  88. package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
  89. package/.framework/src/instructions/concepts/spine-way.md +172 -0
  90. package/.framework/src/instructions/deliveries.md +59 -0
  91. package/.framework/src/instructions/guide/00-orientation.md +106 -0
  92. package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
  93. package/.framework/src/instructions/guide/02-project-map.md +147 -0
  94. package/.framework/src/instructions/guide/03-domain-model.md +223 -0
  95. package/.framework/src/instructions/guide/04-first-app.md +227 -0
  96. package/.framework/src/instructions/guide/05-glossary.md +183 -0
  97. package/.framework/src/instructions/integrations.md +96 -0
  98. package/.framework/src/instructions/internal/cil-markers.md +83 -0
  99. package/.framework/src/instructions/internal/docs-contract.md +158 -0
  100. package/.framework/src/instructions/invites.md +60 -0
  101. package/.framework/src/instructions/kb.md +80 -0
  102. package/.framework/src/instructions/logs.md +77 -0
  103. package/.framework/src/instructions/notifications.md +77 -0
  104. package/.framework/src/instructions/observability.md +61 -0
  105. package/.framework/src/instructions/operations/deployment.md +115 -0
  106. package/.framework/src/instructions/operations/migrations.md +125 -0
  107. package/.framework/src/instructions/operations/security-checklist.md +170 -0
  108. package/.framework/src/instructions/operations/troubleshooting.md +170 -0
  109. package/.framework/src/instructions/operations/upgrade.md +116 -0
  110. package/.framework/src/instructions/people.md +91 -0
  111. package/.framework/src/instructions/pipeline-executions.md +62 -0
  112. package/.framework/src/instructions/pipelines.md +78 -0
  113. package/.framework/src/instructions/prompts.md +95 -0
  114. package/.framework/src/instructions/reference/api/admin-data.md +162 -0
  115. package/.framework/src/instructions/reference/api/auth.md +113 -0
  116. package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
  117. package/.framework/src/instructions/reference/app-manifest.md +184 -0
  118. package/.framework/src/instructions/reference/cli.md +124 -0
  119. package/.framework/src/instructions/reference/database.md +132 -0
  120. package/.framework/src/instructions/reference/design-schema.md +280 -0
  121. package/.framework/src/instructions/reference/env-vars.md +133 -0
  122. package/.framework/src/instructions/reference/frontend-sdk.md +207 -0
  123. package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
  124. package/.framework/src/instructions/reference/functions/admin-data.md +197 -0
  125. package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
  126. package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
  127. package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
  128. package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
  129. package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
  130. package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
  131. package/.framework/src/instructions/reference/functions/apps.md +140 -0
  132. package/.framework/src/instructions/reference/functions/auth.md +99 -0
  133. package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
  134. package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
  135. package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
  136. package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
  137. package/.framework/src/instructions/reference/functions/index.md +103 -0
  138. package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
  139. package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
  140. package/.framework/src/instructions/reference/functions/integrations.md +130 -0
  141. package/.framework/src/instructions/reference/functions/invites.md +182 -0
  142. package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
  143. package/.framework/src/instructions/reference/functions/kb.md +166 -0
  144. package/.framework/src/instructions/reference/functions/logs.md +141 -0
  145. package/.framework/src/instructions/reference/functions/observability.md +105 -0
  146. package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
  147. package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
  148. package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
  149. package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
  150. package/.framework/src/instructions/reference/functions/roles.md +121 -0
  151. package/.framework/src/instructions/reference/functions/security.md +140 -0
  152. package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
  153. package/.framework/src/instructions/reference/functions/system.md +106 -0
  154. package/.framework/src/instructions/reference/functions/tests.md +97 -0
  155. package/.framework/src/instructions/reference/functions/timers.md +117 -0
  156. package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
  157. package/.framework/src/instructions/reference/functions/triggers.md +140 -0
  158. package/.framework/src/instructions/reference/functions/types.md +146 -0
  159. package/.framework/src/instructions/reference/seeds.md +126 -0
  160. package/.framework/src/instructions/reference/shared-modules.md +127 -0
  161. package/.framework/src/instructions/reference/spine-config.md +114 -0
  162. package/.framework/src/instructions/roles.md +80 -0
  163. package/.framework/src/instructions/runtime-collaboration.md +106 -0
  164. package/.framework/src/instructions/runtime-detail.md +70 -0
  165. package/.framework/src/instructions/runtime-list.md +77 -0
  166. package/.framework/src/instructions/security.md +67 -0
  167. package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
  168. package/.framework/src/instructions/tasks/add-a-role.md +96 -0
  169. package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
  170. package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
  171. package/.framework/src/instructions/tasks/add-a-type.md +151 -0
  172. package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
  173. package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
  174. package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
  175. package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
  176. package/.framework/src/instructions/tasks/document-your-app.md +117 -0
  177. package/.framework/src/instructions/tasks/package-and-install-an-app.md +136 -0
  178. package/.framework/src/instructions/tasks/register-an-app.md +137 -0
  179. package/.framework/src/instructions/tasks/test-your-code.md +108 -0
  180. package/.framework/src/instructions/tasks/write-a-function.md +132 -0
  181. package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
  182. package/.framework/src/instructions/testing.md +59 -0
  183. package/.framework/src/instructions/timers.md +84 -0
  184. package/.framework/src/instructions/triggers.md +69 -0
  185. package/.framework/src/instructions/types.md +93 -0
  186. package/.framework/src/lib/app-mount.ts +88 -0
  187. package/.framework/src/pages/NotFoundPage.tsx +62 -31
  188. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
  189. package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
  190. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
  191. package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
  192. package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
  193. package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
  194. package/.framework/src/pages/admin/AppsPage.tsx +104 -66
  195. package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
  196. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
  197. package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
  198. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
  199. package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
  200. package/.framework/src/pages/admin/LogsPage.tsx +144 -76
  201. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
  202. package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
  203. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
  204. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
  205. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
  206. package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
  207. package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
  208. package/.framework/src/pages/admin/RolesPage.tsx +86 -43
  209. package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
  210. package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
  211. package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
  212. package/.framework/src/pages/admin/TimersPage.tsx +90 -62
  213. package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
  214. package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
  215. package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
  216. package/.framework/src/pages/admin/TypesPage.tsx +90 -48
  217. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
  218. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
  219. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
  220. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
  221. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
  222. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
  223. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
  224. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
  225. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
  226. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
  227. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
  228. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
  229. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
  230. package/.framework/src/pages/auth/LoginPage.tsx +3 -2
  231. package/.framework/src/pages/auth/RegisterPage.tsx +16 -8
  232. package/.framework/templates/agent/AGENTS.md +167 -0
  233. package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
  234. package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
  235. package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
  236. package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
  237. package/.framework/templates/agent/skills/spine-install-app/SKILL.md +79 -0
  238. package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
  239. package/.framework/templates/agent/skills/spine-new-app/SKILL.md +79 -0
  240. package/.framework/templates/agent/skills/spine-new-function/SKILL.md +71 -0
  241. package/.framework/templates/agent/skills/spine-new-type/SKILL.md +72 -0
  242. package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
  243. package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
  244. package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
  245. package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
  246. package/.framework/templates/project.json +4 -1
  247. package/.framework/tests/unit/account-scoping-gates.test.ts +394 -0
  248. package/.framework/tests/unit/agent-files.test.ts +355 -0
  249. package/.framework/tests/unit/agent-runner.test.ts +479 -44
  250. package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
  251. package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
  252. package/.framework/tests/unit/app-mount.test.ts +80 -0
  253. package/.framework/tests/unit/bootstrap.test.ts +242 -0
  254. package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
  255. package/.framework/tests/unit/debug-auth.test.ts +38 -0
  256. package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
  257. package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
  258. package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
  259. package/.framework/tests/unit/logging-app-id.test.ts +75 -0
  260. package/.framework/tests/unit/security-definer-grants.test.ts +154 -0
  261. package/.framework/tests/unit/system-context-denial.test.ts +145 -0
  262. package/.framework/tests/unit/system-cron.test.ts +240 -0
  263. package/.framework/tests/unit/timers.test.ts +159 -0
  264. package/README.md +125 -166
  265. package/config/tsconfig.cli.json +2 -2
  266. package/package.json +15 -4
  267. package/templates/project/package-lock.json +2 -2
  268. package/.framework/docs/admin-routes.md +0 -76
  269. package/.framework/docs/api-docs-progress.md +0 -38
  270. package/.framework/docs/api-governance.md +0 -146
  271. package/.framework/docs/api-testing-results.md +0 -212
  272. package/.framework/docs/assembly-launch-guide.md +0 -138
  273. package/.framework/docs/audit-results.md +0 -590
  274. package/.framework/docs/db-api-inventory.md +0 -95
  275. package/.framework/docs/gap-remediation-backlog.md +0 -103
  276. package/.framework/docs/ui-adoption-verification.md +0 -111
  277. package/.framework/docs/ui-api-coverage.md +0 -84
  278. package/.framework/docs/v2-compatibility-audit.md +0 -228
  279. package/.framework/mcp/index.ts +0 -133
  280. package/.framework/mcp/load-env.ts +0 -60
  281. package/.framework/mcp/shared/app-manifest.ts +0 -275
  282. package/.framework/mcp/shared/db.ts +0 -477
  283. package/.framework/mcp/shared/kb.ts +0 -118
  284. package/.framework/mcp/tools/architect.ts +0 -431
  285. package/.framework/mcp/tools/cil/context.ts +0 -345
  286. package/.framework/mcp/tools/cil/graph.ts +0 -242
  287. package/.framework/mcp/tools/cil/ingest.ts +0 -1217
  288. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
  289. package/.framework/mcp/tools/cil/pipeline.ts +0 -767
  290. package/.framework/mcp/tools/cil/project.ts +0 -791
  291. package/.framework/mcp/tools/cil/retrieval.ts +0 -336
  292. package/.framework/mcp/tools/cil/search-core.ts +0 -454
  293. package/.framework/mcp/tools/cil/search.ts +0 -97
  294. package/.framework/mcp/tools/cil/suggest.ts +0 -186
  295. package/.framework/mcp/tools/cil/validate.ts +0 -1369
  296. package/.framework/mcp/tools/kb/delete.ts +0 -139
  297. package/.framework/mcp/tools/kb/index.ts +0 -52
  298. package/.framework/mcp/tools/kb/ingest.ts +0 -84
  299. package/.framework/mcp/tools/kb/list-types.ts +0 -102
  300. package/.framework/mcp/tools/kb/publish.ts +0 -100
  301. package/.framework/mcp/tools/kb/reindex.ts +0 -86
  302. package/.framework/mcp/tools/kb/revoke.ts +0 -85
  303. package/.framework/mcp/tools/kb/search.ts +0 -101
  304. package/.framework/mcp/tools/kb/upsert.ts +0 -125
  305. package/.framework/mcp/tools/observability.ts +0 -510
@@ -0,0 +1,133 @@
1
+ ---
2
+ title: "Auth: sessions, API keys, and invites"
3
+ audience: developers, agents
4
+ kind: concept
5
+ verified_against: f69c37d
6
+ verified_on: 2026-10-29
7
+ source_files:
8
+ - .framework/functions/_shared/principal.ts
9
+ - .framework/functions/_shared/middleware.ts
10
+ - .framework/functions/_shared/hash-api-key.ts
11
+ - .framework/functions/_shared/rate-limit.ts
12
+ - .framework/functions/api-keys.ts
13
+ - .framework/functions/invites.ts
14
+ - .framework/functions/integration-routes.ts
15
+ - .framework/migrations/035_rate_limits.sql
16
+ - .framework/migrations/052_registration_security.sql
17
+ ---
18
+
19
+ # Auth: sessions, API keys, and invites
20
+
21
+ Every request to a Spine function resolves to one **Principal**
22
+ (`_shared/principal.ts`): either `type: 'human'` (a `people` row + roles) or
23
+ `type: 'machine'` (an `api_keys` row + scopes), plus `ANONYMOUS_PRINCIPAL` and
24
+ `SYSTEM_PRINCIPAL` sentinels. `resolvePrincipal` is the single dispatch point,
25
+ run by `createHandler` / `createPublicHandler` in `_shared/middleware.ts`:
26
+
27
+ 1. `x-api-key` header → **machine** principal.
28
+ 2. A valid `INTERNAL_INVOKE_SECRET` (function-to-function) → `x-cron-id` /
29
+ `x-trigger-id` impersonation, else `SYSTEM_PRINCIPAL`. These headers are
30
+ honored only alongside the secret — a bare id is ignored.
31
+ 3. `Authorization: Bearer <jwt>` → **human** principal via Supabase auth.
32
+ 4. Nothing → `ANONYMOUS_PRINCIPAL` (machine, no scopes, no account).
33
+
34
+ The principal also selects the database client (`getPrincipalDb`): humans get a
35
+ JWT-scoped client (row-level security applies); machines and anonymous get the
36
+ service-role client, so endpoint code is responsible for checking machine
37
+ scopes — `machineHasScope` honors `resource:action`, `resource:*`, and `*:*`,
38
+ and `admin-data` treats a machine with `*:*` as authorized (with role
39
+ `system_admin`) while lesser keys still pass through record permissions.
40
+
41
+ ## Browser sessions (humans)
42
+
43
+ `Bearer <supabase-jwt>` → `supabase.auth.getUser` → `resolveInternalPersonId`
44
+ binds the auth user to a `people` row by `auth_uid` (email fallback never
45
+ binds unclaimed invite stubs), then loads the person with its role join. The
46
+ principal carries `roles: [role.slug]`, `accountId`, and the raw JWT in
47
+ `authContext` (never serialized — `formatPrincipalForAudit` omits it). Results
48
+ cache for 60 seconds per warm instance, so role/account changes take up to a
49
+ minute to propagate; a person with neither `account_id` nor `role_id`
50
+ (registration not yet placed) is never cached.
51
+
52
+ A human's effective rights come from `roles` → `isSystemAdmin` /
53
+ `humanHasRole` → the `design_schema.record_permissions` on each type. An
54
+ invalid or expired JWT silently becomes anonymous — endpoints then fail with
55
+ "Authentication required" / "Account context required" rather than 401s.
56
+
57
+ ## API keys (machines)
58
+
59
+ Mint a key with `POST /api/api-keys` (`.framework/functions/api-keys.ts`):
60
+ `{ name, key_type?, scopes?, rate_limit?, expires_at?, integration_id?,
61
+ app_id?, machine_type?, is_internal? }`. The response returns `raw_key` —
62
+ `sk_<32 hex>` — **once**, with a "store this now" warning. Only the first 8
63
+ characters (`key_prefix`, the lookup index) and a PBKDF2-SHA256 hash
64
+ (100k iterations, `pbkdf2$iterations$salt$hash`) are stored; `omitSecretFields`
65
+ strips secret material from every read response.
66
+
67
+ Use it as `x-api-key: sk_...` on any function endpoint. Resolution:
68
+
69
+ - Prefix-matched candidate keys must be `is_active` and unexpired, then the
70
+ hash is verified with a timing-safe compare.
71
+ - Per-key rate limit: `rate_limit` (default 100) requests per
72
+ `config.rate_limit_window_seconds` (default 60), counted in
73
+ `api_key_usage_logs`.
74
+ - Per-IP ceiling: `rate_limit_hit` RPC on `ip:<ip>:<path>`, default 300/minute
75
+ (`SPINE_IP_RATE_LIMIT`), fail-open.
76
+ - Every request writes an `api_key_usage_logs` row (method, path, IP,
77
+ user-agent) — `action=usage-logs` on the same endpoint reads them back.
78
+
79
+ The resulting machine principal inherits `key.scopes`, `key.machine_type`
80
+ (default `'integration'`), `key.is_internal`, and `key.account_id`. A `*:*`
81
+ scope acts as `system_admin`. `integration-routes` (inbound webhooks)
82
+ *requires* a machine principal — a JWT cannot deliver webhooks — and keys
83
+ pinned to `integration_id`/`app_id` are restricted to those integrations.
84
+ Related actions: `action=validate` (verify a key + check permissions),
85
+ `action=revoke` (sets `is_active=false`), `action=usage-logs`.
86
+
87
+ Shared throttling also applies: `checkRateLimit(ctx, action)`
88
+ (`_shared/rate-limit.ts`) resolves named policies from
89
+ `accounts.data.rate_limits[action]` (account override) or the calling app's
90
+ `config.rate_limits` (with `rate_limit_profile` indirection), and enforces
91
+ them through the atomic `rate_limit_hit` RPC on `public.rate_limits`
92
+ (migration `035`). Over-limit → 429 (`RateLimitError`); a `fail_closed`
93
+ policy → 503 when the counter RPC is unavailable.
94
+
95
+ ## First admin and invites
96
+
97
+ Bootstrap is a Postgres trigger, not an endpoint: on the first `auth.users`
98
+ signup, `handle_new_auth_user` (migrations `010`, hardened in `052`) takes an
99
+ advisory lock, creates the `people` row, and — only when **no** `people` rows
100
+ exist — grants the `system_admin` role under the `spine-system` account.
101
+ Serialization means two concurrent first signups cannot both become
102
+ system-admin; `system_admin` can never be assigned through registration.
103
+
104
+ Everyone after that arrives by invite (`.framework/functions/invites.ts`):
105
+
106
+ - `action=create-invite` — requires `system_admin` or an `*_admin` role on the
107
+ target account; `role_slug: 'system_admin'` is refused. Creates a stub
108
+ `people` row holding `data.invite_token` (16-char) and
109
+ `invite_expires_at` (7 days), returns
110
+ `{APP_BASE_URL}/register?t=<token>`, and can send the email through a
111
+ configured email integration.
112
+ - `action=validate-token` (public) — checks the token exists, is unblocked,
113
+ and unexpired.
114
+ - `action=complete-registration` — after Supabase signup, calls the
115
+ service-role-only `complete_registration_placement` RPC, which atomically
116
+ stamps `account_id` + `role_id` from the app's server-side registration
117
+ config (`new_account`, `existing_account`, `join_request` paths). Client
118
+ claims about role or target are never trusted — the RPC is the only
119
+ placement writer.
120
+ - Admin management: `list-invites`, `revoke-invite`, `reinvite`,
121
+ `block-invite`, `approve`, `deny`, `list-members`, `update-member`,
122
+ `deactivate-member`. `check-first-user` and `check-domain` are public
123
+ read-only probes used by the registration UI.
124
+
125
+ ## What each surface can reach
126
+
127
+ | | Browser JWT | API key | Internal secret |
128
+ |---|---|---|---|
129
+ | Principal | human (roles) | machine (scopes) | system / cron / trigger |
130
+ | DB client | JWT-scoped (RLS) | service role | service role |
131
+ | `admin-data`, entity APIs | role + record permissions | scopes; `*:*` = system admin | full |
132
+ | `integration-routes` (inbound) | no | required | — |
133
+ | `system-cron` / `trigger-scheduler` | no | `SCHEDULER_API_KEY` or internal machine | yes |
@@ -0,0 +1,186 @@
1
+ ---
2
+ title: "Automation: pipelines, triggers, and timers"
3
+ audience: developers, agents
4
+ kind: concept
5
+ verified_against: f69c37d
6
+ verified_on: 2026-10-29
7
+ source_files:
8
+ - .framework/functions/pipelines.ts
9
+ - .framework/functions/pipeline-executions.ts
10
+ - .framework/functions/pipeline-runner-background.ts
11
+ - .framework/functions/_shared/pipeline-runner.ts
12
+ - .framework/functions/_shared/trigger-engine.ts
13
+ - .framework/functions/timers.ts
14
+ - .framework/functions/triggers.ts
15
+ - .framework/functions/trigger-scheduler.ts
16
+ - .framework/functions/system-cron.ts
17
+ - .framework/functions/admin-data.ts
18
+ - .framework/migrations/001_core_schema.sql
19
+ - .framework/migrations/018_runtime_tables.sql
20
+ - .framework/migrations/020_schema_reconciliation.sql
21
+ - .framework/migrations/068_cron_schedule_timer_rpcs.sql
22
+ - netlify.toml
23
+ ---
24
+
25
+ # Automation: pipelines, triggers, and timers
26
+
27
+ Spine automation is three primitives plus a ledger:
28
+
29
+ - A **pipeline** (`public.pipelines`) is an ordered JSON `stages` array — the
30
+ definition of *what runs*.
31
+ - A **trigger** (`public.triggers`) maps a record event
32
+ (`<kind>_created` / `<kind>_updated` / `<kind>_deleted`) to a pipeline — *when
33
+ data changes*.
34
+ - A **timer** (`public.timers`) points at a pipeline and carries
35
+ `next_execution` — *when the clock says so*.
36
+ - A **pipeline execution** (`public.pipeline_executions`) is the durable record
37
+ of one run — *what actually happened*.
38
+
39
+ There is no separate "workflow engine" service. Everything runs inside the
40
+ Netlify functions, coordinated through Postgres rows.
41
+
42
+ ## Pipelines and the runner
43
+
44
+ `runPipeline` in `.framework/functions/_shared/pipeline-runner.ts` is the single
45
+ executor. It loads the pipeline row (inactive or missing pipelines throw),
46
+ creates or claims a `pipeline_executions` row, then walks `pipeline.stages` in
47
+ array order.
48
+
49
+ Each stage names a `stage_type`. That value is **not** a function name — it is a
50
+ slug looked up in `public.actions` (`eq('slug', stage.stage_type)`,
51
+ `is_active = true`). The action row supplies `handler_module` and `handler`;
52
+ the stage's `config` is merged over `action.config` along with internal keys
53
+ `_pipelineId`, `_executionId`, `_stageIndex`, and `_triggerData`. The runner
54
+ then routes on `handler_module`:
55
+
56
+ - `functions` — dispatch to a named handler in the runner's `stageHandlers`
57
+ map. Built-ins include `update_item`, `create_record`, `http_request`,
58
+ `send_notification`, `send_email`, `run_pipeline`, `query_items`,
59
+ `search_knowledge`, `agent_inference`, `generate_embeddings`,
60
+ `revoke_embeddings`, `kb_ingest`, `kb_revoke`, and `create_interaction`.
61
+ - `integrations` — dispatch through `integration-outbound` (the same path
62
+ outbound webhooks use).
63
+ - `webhook` — POST/GET to `action.config.url` via `integration-outbound`.
64
+
65
+ Stage config strings support `{{trigger.*}}` and `{{record.*}}` placeholders,
66
+ resolved recursively against the trigger envelope and the firing record's data
67
+ (`record` is `triggerData.entity.data`). Timer- and manual-fired runs have **no
68
+ record context** — `{{record.*}}` placeholders pass through unresolved and
69
+ appear literally in stage output (e.g. notification text). Author timer-driven
70
+ stages against `{{trigger.*}}` or fixed text.
71
+
72
+ A stage that throws is recorded as `failed` in the stage results; the run then
73
+ aborts **unless** the stage sets `continue_on_error: true`. Completed and
74
+ failed runs both write terminal status, stage results, and duration back to the
75
+ execution row — `pipeline-executions` is therefore the ground truth for "did it
76
+ work", and `status: 'cancelled'` is available for operator-initiated stops.
77
+
78
+ ## Triggers: record events
79
+
80
+ Writes that go through `admin-data` call into
81
+ `.framework/functions/_shared/trigger-engine.ts`: creates fire
82
+ `fireCreateTriggers`, updates fire `fireUpdateTriggers` with the previous row
83
+ snapshot. (The delete path currently does not invoke `fireDeleteTriggers` —
84
+ `*_deleted` event types exist in the union, but nothing emits them from
85
+ `admin-data`.) Trigger fires are caught-and-logged, so a failing trigger never
86
+ rolls back the write that caused it.
87
+
88
+ `checkAndFireTriggers` loads active triggers for the event type and evaluates
89
+ each against `config`:
90
+
91
+ - `config.entity_type` must match the runtime table's kind (`items` → `item`,
92
+ `people` → `person`, and so on).
93
+ - `config.type_slug` restricts to one `types.slug`; the engine resolves the
94
+ slug from the record's `type_id` when needed.
95
+ - `config.filters` is a map of path → expected value. Paths support dot
96
+ notation and Postgres-style `data->>'field'` accessors; expected values may
97
+ be literals, arrays (membership), or operator objects using `$eq`, `$ne`,
98
+ `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$exists`.
99
+ - `config.fire_on_transition` makes an update trigger fire only when the record
100
+ *moves into* the matching state — the previous snapshot must not already
101
+ match. This is what prevents "every save to a published record re-fires the
102
+ pipeline".
103
+
104
+ A matching trigger does **not** run the pipeline inline. It:
105
+
106
+ 1. Inserts a `pipeline_executions` row with `status: 'queued'` (a stable
107
+ execution id exists before any work starts).
108
+ 2. POSTs to `/.netlify/functions/pipeline-runner-background` with an
109
+ `X-Internal-Secret` header, awaiting only the dispatch ack (bounded by a 5s
110
+ timeout) — the caller's request never waits for the pipeline to finish.
111
+ 3. Records `record_trigger_fire` bookkeeping and emits a `trigger.fired` audit
112
+ event (or `trigger.failed` on error).
113
+
114
+ The background function verifies `INTERNAL_INVOKE_SECRET`, reconstructs a
115
+ machine context, and calls `runPipeline` with the pre-created `execution_id`,
116
+ which claims the row `queued → running` instead of inserting a duplicate.
117
+
118
+ ## Timers and the scheduler
119
+
120
+ A timer row carries `timer_type`, `pipeline_id`, `config`, and
121
+ `next_execution`. The `get_due_timers` RPC returns active timers whose
122
+ `pipeline_id` is set and `next_execution <= now()`; `update_timer_after_run`
123
+ stamps `last_execution`, increments `execution_count`/`success_count`/
124
+ `failure_count`, and reschedules `next_execution` from
125
+ `config.interval_minutes` — a timer with no positive `interval_minutes` gets
126
+ `next_execution = NULL` and never runs again.
127
+
128
+ The clock is `trigger-scheduler`, a Netlify scheduled function configured for
129
+ `* * * * *` (every minute) in `netlify.toml`. It accepts genuine Netlify
130
+ Clockwork invocations (schedule header or `Netlify Clockwork` user-agent *plus*
131
+ a `{"next_run": ...}` body) and fabricates a system-principal context;
132
+ non-Clockwork calls fall through to the cron handler's own auth, which accepts
133
+ either `?api_key=$SCHEDULER_API_KEY` or an internal machine principal — anything
134
+ else gets 403 and an audit event.
135
+
136
+ Each tick, `system-cron` does all of the following:
137
+
138
+ - **Sweeps stale executions** — `sweepStaleExecutions` marks
139
+ `running`/`pending`/`queued` rows untouched for over 10 minutes as `failed`.
140
+ This is the safety net for a runner that died mid-flight or a dispatch that
141
+ never arrived.
142
+ - **Runs due schedules** — `get_due_schedules` → validate the creator → load
143
+ the `public.actions` row → execute as the schedule's delegated machine
144
+ principal (`schedules`/`actions`/`schedule_executions` are the runtime
145
+ scheduling tables).
146
+ - **Runs due timers** — each due timer runs its pipeline as machine principal
147
+ `timer:<timer_id>` with scope `pipelines:execute`, then
148
+ `update_timer_after_run` reschedules it.
149
+ - **Evaluates threshold alerts** — `threshold_alerts` rows compare
150
+ `error_rate`, `latency_p95`, or `pipeline_failure_rate` metrics against a
151
+ value and fire the alert's `pipeline_id` on breach.
152
+ - **Retries webhook deliveries** — `retryPendingDeliveries` runs every tick.
153
+ - **Cleans logs** — `cleanup_old_logs` runs on the 00:00 UTC tick.
154
+
155
+ ## Observing executions
156
+
157
+ - `pipeline-executions` (`.framework/functions/pipeline-executions.ts`):
158
+ `GET` list/get, `action=running`, `action=stats`, `action=cleanup`
159
+ (`days_to_keep` defaults to 30), plus `start`/`complete`/`cancel`.
160
+ - `pipelines` (`.framework/functions/pipelines.ts`): CRUD plus
161
+ `action=by-trigger`, `action=executions`, `action=toggle`.
162
+ - `triggers` and `timers` (`.framework/functions/triggers.ts`,
163
+ `.framework/functions/timers.ts`): CRUD plus `toggle`; triggers also expose
164
+ `listByEvent` and `getExecutions`.
165
+ - CLI: `spine-framework pipelines run <id>` executes a pipeline;
166
+ `spine-framework pipelines executions <id>` lists recent runs
167
+ (`.framework/cli/commands/pipelines.ts`).
168
+
169
+ An execution stuck in `queued` means dispatch failed before the background
170
+ function claimed it — the sweeper fails it after 10 minutes. An execution stuck
171
+ in `running` means the runner died — same sweeper, same outcome.
172
+
173
+ ## How the pieces connect
174
+
175
+ 1. A client writes a record through `admin-data`; `fireCreateTriggers` /
176
+ `fireUpdateTriggers` run in-process after the write.
177
+ 2. The trigger engine matches the event to active `triggers` rows, inserts a
178
+ `queued` execution, and dispatches to `pipeline-runner-background`.
179
+ 3. Independently, Netlify Clockwork invokes `trigger-scheduler` every minute;
180
+ `system-cron` runs due timers and schedules through the same `runPipeline`,
181
+ sweeps dead executions, retries deliveries, and evaluates alert thresholds.
182
+ 4. `runPipeline` resolves each `stage_type` through `public.actions` and runs
183
+ the stage handlers — which is also where outbound integrations, AI agent
184
+ calls, and embedding jobs enter the system.
185
+ 5. Every step lands in `pipeline_executions`, `logs`, and the audit trail,
186
+ which is what the admin UI and `pipeline-executions` API read.
@@ -0,0 +1,125 @@
1
+ ---
2
+ title: "Concurrency and idempotency"
3
+ audience: developers, agents
4
+ kind: concept
5
+ verified_against: f69c37d
6
+ verified_on: 2026-10-29
7
+ source_files:
8
+ - .framework/functions/_shared/message-sequence.ts
9
+ - .framework/functions/_shared/agent-runner.ts
10
+ - .framework/functions/_shared/pipeline-runner.ts
11
+ - .framework/functions/_shared/trigger-engine.ts
12
+ - .framework/functions/_shared/retry-deliveries.ts
13
+ - .framework/functions/integration-outbound.ts
14
+ - .framework/functions/_shared/kb.ts
15
+ - .framework/cli/commands/migrate.ts
16
+ - .framework/migrations/001_core_schema.sql
17
+ - .framework/migrations/035_rate_limits.sql
18
+ - .framework/migrations/054_kb_ingest_lease.sql
19
+ - .framework/migrations/064_episode_leases.sql
20
+ - .framework/migrations/066_episode_steal_thread_merge.sql
21
+ - .framework/migrations/067_episode_lease_stale_steal.sql
22
+ ---
23
+
24
+ # Concurrency and idempotency
25
+
26
+ Spine runs on serverless functions: an invocation can die at the response
27
+ flush, a client can retry a request that already succeeded, and two instances
28
+ can race the same resource. Spine's answer is consistent: **the database
29
+ decides**. Ordering, single-flight, and deduplication are enforced by
30
+ constraints and atomic RPCs — never by reading state and deciding in memory.
31
+
32
+ ## Ordered writes: message sequencing
33
+
34
+ `public.messages` has `UNIQUE(thread_id, sequence)`
35
+ (migration `001_core_schema.sql`) — the hard invariant for conversation order.
36
+ Two writers honor it differently:
37
+
38
+ - `_shared/message-sequence.ts` (`insertMessageSequenced`) is the simple path:
39
+ read the max `sequence` for the thread, insert `max + 1`, retry up to 3
40
+ times on a `23505` duplicate-key error. The retry exists because a racing
41
+ writer may take the same number; the constraint makes the conflict visible.
42
+ - The agent runner keeps a per-instance counter (`threadSeqCounter`) seeded by
43
+ one `COUNT(*)` per thread, then allocates locally — a batch of tool-result
44
+ messages gets consecutive numbers up front so `Promise.all` inserts stay in
45
+ order. A `23505` triggers a reseed + retry. The counter is an optimization;
46
+ the unique constraint remains the backstop.
47
+
48
+ ## Pipeline executions: claim, don't create-twice
49
+
50
+ Trigger dispatches reserve a `pipeline_executions` row with
51
+ `status: 'queued'` *before* calling the background runner. `runPipeline` then
52
+ **claims** that row (`queued → running` on `id = executionId AND status =
53
+ 'queued'`) rather than inserting a second execution — a retried or duplicated
54
+ dispatch converges on the same row. If the claim finds nothing, the runner
55
+ falls back to inserting, so an execution record always exists. Rows stuck in
56
+ `queued`/`running`/`pending` for over 10 minutes are swept to `failed` — a
57
+ runner that died mid-flight can never write its own terminal status.
58
+
59
+ Individual stages can also be idempotent: `create_record` accepts
60
+ `config.dedupe { field, value, account_scoped? }` — when a record matching
61
+ `field = value` already exists, the stage returns the existing row
62
+ (`deduped: true`) instead of inserting a duplicate.
63
+
64
+ ## Webhook deliveries: retry in place
65
+
66
+ An outbound delivery is one `webhook_deliveries` row for its whole life:
67
+ `pending → processing → delivered | pending | failed`. Retries — automatic
68
+ (`retryPendingDeliveries` each cron tick) or manual
69
+ (`deliveries?action=retry`) — call `integration-outbound` with the existing
70
+ `delivery_id`, which reloads the row, reuses its payload, and increments
71
+ `attempt_count` in place; they never create a second delivery. Backoff is
72
+ exponential (`30s × 2^attempt`, 1h cap, ±20% jitter) until `max_attempts`
73
+ turns the row `failed`; the sweep is bounded (`attempt_count < 10`, 100 rows
74
+ per pass) so one poisoned integration can't monopolize the tick. Inbound
75
+ deliveries get the same row lifecycle, but inbound retry is not implemented —
76
+ a failed inbound delivery stays visible for manual handling.
77
+
78
+ ## Leases: single-flight with fencing
79
+
80
+ Long-running agent episodes coordinate through `public.episode_leases`
81
+ (migration `064`; service-role only). `acquire_episode_lease` is atomic: an
82
+ `INSERT … ON CONFLICT (thread_id) DO NOTHING` fast path, then a
83
+ `FOR UPDATE` conflict path returning `claimed` + a fresh `lease_token`, or
84
+ `held` + the live holder's `driver_id`. The same driver re-acquiring renews;
85
+ an expired lease is taken over with a new token.
86
+
87
+ The token is a **fence**, not just a lock:
88
+
89
+ - `heartbeat_episode_lease(thread_id, lease_token)` extends the TTL only when
90
+ the token matches — `false` means "you were displaced; stop writing now."
91
+ - `release_episode_lease` is token-gated for the same reason.
92
+ - `merge_thread_data(thread_id, patch, lease_token)` (migration `066`) is the
93
+ only way episode checkpoints are written: an atomic jsonb merge whose
94
+ `WHERE` clause refuses the write when a different token holds the lease —
95
+ so a fenced driver's late write is dropped instead of resurrecting stale
96
+ state.
97
+
98
+ Migration `067` adds `p_steal_detached` / `p_stale_seconds`: request-bound
99
+ drivers may steal a lease from a `detached:`-prefixed holder **only** when its
100
+ heartbeat is older than `p_stale_seconds` (default 30). Detached drivers are a
101
+ liveness fallback, never an owner that blocks live work. The same lease shape
102
+ guards knowledge ingest: `document_leases` with
103
+ `acquire_document_lease`/`release_document_lease` (migration `054`), claimed
104
+ by content hash in `_shared/kb.ts`.
105
+
106
+ ## Atomic counters and bookkeeping
107
+
108
+ - `rate_limit_hit` (migration `035`) upsert-increments the `rate_limits`
109
+ window counter in one statement — concurrent invocations can't double-spend
110
+ a window.
111
+ - `record_trigger_fire` (migration `064`) bumps trigger bookkeeping atomically
112
+ on each fire.
113
+
114
+ ## Migrations: ordered, transactional, ledgered
115
+
116
+ `spine-framework migrate` (`.framework/cli/commands/migrate.ts`) applies
117
+ `.framework/migrations/*.sql` (plus `custom/migrations/` unless
118
+ `--framework-only`) in **filename order** — the numeric prefix is the order.
119
+ Each file runs inside one transaction together with its ledger insert into
120
+ `_spine_migrations` (`filename UNIQUE`, `applied_at`): a file either lands
121
+ complete with its ledger row or rolls back, and the run stops at the first
122
+ failure. Re-runs skip anything already in the ledger (`--dry-run` lists
123
+ pending without touching the DB). The connection is derived from
124
+ `SUPABASE_URL` (a hosted `*.supabase.co` URL only — no credentials, ports, or
125
+ paths) plus `SUPABASE_DB_PASSWORD`.
@@ -0,0 +1,124 @@
1
+ ---
2
+ title: Core vs Custom Boundary
3
+ audience: developers, agents
4
+ kind: concept
5
+ verified_against: f69c37d
6
+ verified_on: 2026-10-29
7
+ source_files:
8
+ - .framework/templates/project.json
9
+ - .framework/cli/commands/upgrade.ts
10
+ - .framework/cli/lib/project-files.ts
11
+ - .framework/cli/commands/migrate.ts
12
+ - scripts/boundary-check.sh
13
+ - scripts/assemble-frontend.sh
14
+ - scripts/assemble-functions.sh
15
+ - config/vite.config.ts
16
+ - config/tsconfig.json
17
+ - config/tsconfig.functions.json
18
+ - netlify.toml
19
+ ---
20
+
21
+ # Core vs Custom Boundary
22
+
23
+ Spine projects are two trees with different owners: `.framework/` is the
24
+ sealed, upgradeable package; `custom/` is project-owned code and data the
25
+ package never writes. Everything about the boundary — the alias config, the
26
+ assembly, the upgrade command, the lint script — exists to keep one promise:
27
+ **upgrading the framework can never destroy your work.**
28
+
29
+ ## What lives where
30
+
31
+ **`.framework/` (core, framework-owned)** — the React app shell and admin UI
32
+ (`.framework/src/`), Netlify functions and shared middleware
33
+ (`.framework/functions/`), ordered SQL migrations (`.framework/migrations/`),
34
+ the CLI (`.framework/cli/`), this instruction book
35
+ (`.framework/src/instructions/`), schemas, seeds, tests, and templates
36
+ (`.framework/templates/`). The shipped file set is declared in
37
+ `.framework/templates/project.json` under `framework_dirs`, `framework_files`,
38
+ `config_files`, `root_scripts`, and `bin`.
39
+
40
+ **`custom/` (project-owned)** — apps under `custom/apps/<slug>/` (each with a
41
+ `manifest.json` and `index.tsx`), migrations under `custom/migrations/`,
42
+ function handlers under `custom/functions/` and `custom/apps/<slug>/functions`,
43
+ plus tests and vendored packages. This tree is created by `init` / `create-app`
44
+ / `install-app`; it is not part of the npm package and `upgrade` never writes
45
+ into it.
46
+
47
+ **Shared but owned:** `config/` (Vite + tsconfig), `netlify.toml`, and the
48
+ `scripts/` assembly-and-integrity scripts ship with the framework and are
49
+ synced on upgrade, with one carve-out — `netlify.toml`
50
+ `[functions."custom_*"]` sections belong to apps and are re-appended verbatim
51
+ by `renderNetlifyToml` in `.framework/cli/lib/project-files.ts`.
52
+
53
+ ## Why the boundary exists: upgrade safety
54
+
55
+ `npx spine-framework upgrade` is a file sync, not a git operation:
56
+
57
+ 1. It reads `.spine-manifest.json` — frozen per-file hashes written at install
58
+ — and **aborts if any framework file was locally modified or deleted**
59
+ unless `--force` is passed (`.framework/cli/commands/upgrade.ts`,
60
+ `runUpgrade`).
61
+ 2. It copies the new package's framework files over yours and deletes only
62
+ `.framework/` entries that disappeared upstream. Files outside
63
+ `.framework/` that upstream dropped are warned about, never removed.
64
+ 3. It re-merges `package.json` (framework scripts + dependency pin), re-renders
65
+ `netlify.toml` preserving `custom_*` sections, and regenerates the manifest.
66
+ New migrations are reported, never run — `npx spine-framework migrate`
67
+ applies them.
68
+
69
+ Because overwrite-on-upgrade is guaranteed, the only way a local change inside
70
+ `.framework/` survives is `verify`/`upgrade` treating the tree as sealed. The
71
+ frozen manifest is the integrity contract: `scripts/verify-integrity.sh`
72
+ re-hashes the tree and compares.
73
+
74
+ ## How the boundary is enforced
75
+
76
+ - **`scripts/boundary-check.sh`** — fails the build if `.framework/` code
77
+ imports `custom/` or `custom_*` modules, if core source references
78
+ known app slugs, or if `custom/` code reaches `.framework/` via relative
79
+ imports instead of the `@core` alias.
80
+ - **Aliases** — `config/tsconfig.json` maps `@core/*` → `.framework/src/*` and
81
+ `@custom/*` → `custom/*`; `config/vite.config.ts` maps `@core` to the
82
+ assembled `src` tree. `config/tsconfig.functions.json` maps `@shared/*` to
83
+ the assembled `_shared/` directory — custom functions import `./_shared/...`
84
+ at runtime.
85
+ - **Assembly** — `scripts/assemble-frontend.sh` copies `.framework/src` as the
86
+ base layer and each `custom/apps/<slug>` verbatim into `.assembled/src/<slug>/`;
87
+ `scripts/assemble-functions.sh` flattens framework + custom handlers into
88
+ `.assembled/netlify/functions/` and treats duplicate function basenames as a
89
+ hard error, while letting `custom/` overlay `_shared/` helpers.
90
+ - **Manifest** — `.spine-manifest.json` + `scripts/verify-integrity.sh` seal
91
+ the `.framework/` tree between upgrades.
92
+
93
+ ## How to extend core without crossing the boundary
94
+
95
+ Custom code reaches core through three sanctioned channels, in order of
96
+ preference:
97
+
98
+ 1. **Data** — `public.apps` rows, `public.types` `design_schema`, roles,
99
+ link types, triggers, pipelines. Most "customization" is data.
100
+ 2. **Custom apps** — `custom/apps/<slug>/` with a `manifest.json`, mounted by
101
+ `route_prefix`/`renderer` and loaded dynamically by
102
+ `.framework/src/components/CustomAppLoader.tsx`.
103
+ 3. **Custom functions** — new handler files alongside the assembled
104
+ `_shared/` layer, for behavior admin-data can't express. Never wrappers
105
+ for runtime CRUD — that is `/api/admin-data`'s job.
106
+
107
+ ## What breaks if you cross it
108
+
109
+ - **Core → custom import:** `boundary-check.sh` fails; even if merged, the next
110
+ `upgrade` overwrites or refuses, and the core now fails in projects lacking
111
+ that app.
112
+ - **Custom → `.framework/` relative import:** `boundary-check.sh` check 2b
113
+ fails; the path also breaks under the assembled layout Vite actually builds.
114
+ - **Edit inside `.framework/`:** `verify` reports drift and `upgrade` aborts —
115
+ or `--force` silently reverts your change. Either way the change was never
116
+ yours to keep.
117
+ - **Custom runtime-CRUD function:** compiles fine and quietly bypasses
118
+ permissions, validation, audit, and triggers — the worst kind of violation
119
+ because nothing fails.
120
+
121
+ ## See also
122
+
123
+ - [The Spine Way](spine-way.md) — the ten rules this boundary is part of
124
+ - [Apps and Renderers](apps-and-renderers.md) — how `custom/` apps register and mount