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.
- package/.env.example +13 -3
- package/.framework/cli/commands/bootstrap.ts +392 -0
- package/.framework/cli/commands/dev.ts +91 -218
- package/.framework/cli/commands/doctor.ts +132 -26
- package/.framework/cli/commands/generate.ts +38 -298
- package/.framework/cli/commands/init.ts +20 -1
- package/.framework/cli/commands/install-app.ts +183 -13
- package/.framework/cli/commands/migrate.ts +36 -30
- package/.framework/cli/commands/migrations.ts +16 -56
- package/.framework/cli/commands/upgrade.ts +35 -0
- package/.framework/cli/commands/validate-app.ts +127 -1
- package/.framework/cli/commands/validate-schema.ts +147 -0
- package/.framework/cli/context.ts +223 -110
- package/.framework/cli/index.ts +6 -0
- package/.framework/cli/lib/project-files.ts +241 -1
- package/.framework/functions/_shared/app-manifest.ts +2 -0
- package/.framework/functions/_shared/hash-api-key.ts +183 -79
- package/.framework/functions/_shared/logging.ts +562 -224
- package/.framework/functions/_shared/middleware.ts +47 -44
- package/.framework/functions/_shared/permissions.ts +4 -0
- package/.framework/functions/admin-data.ts +51 -28
- package/.framework/functions/ai-agents.ts +10 -6
- package/.framework/functions/api-keys.ts +301 -122
- package/.framework/functions/apps.ts +63 -29
- package/.framework/functions/cil-ingest.ts +48 -15
- package/.framework/functions/debug-auth.ts +14 -8
- package/.framework/functions/deliveries.ts +39 -12
- package/.framework/functions/embeddings.ts +149 -78
- package/.framework/functions/integration-outbound.ts +67 -27
- package/.framework/functions/integrations.ts +254 -127
- package/.framework/functions/logs.ts +273 -133
- package/.framework/functions/observability.ts +278 -148
- package/.framework/functions/pipeline-executions.ts +1 -1
- package/.framework/functions/pipelines.ts +14 -9
- package/.framework/functions/prompt-configs.ts +7 -4
- package/.framework/functions/roles.ts +8 -5
- package/.framework/functions/security.ts +30 -17
- package/.framework/functions/system-cron.ts +240 -127
- package/.framework/functions/tests.ts +7 -6
- package/.framework/functions/timers.ts +310 -107
- package/.framework/functions/triggers.ts +28 -13
- package/.framework/functions/types.ts +13 -10
- package/.framework/migrations/069_log_stats_window.sql +54 -0
- package/.framework/migrations/070_app_type_schema_rpcs.sql +166 -0
- package/.framework/migrations/071_timer_interval_keys.sql +96 -0
- package/.framework/migrations/072_slug_autofill.sql +77 -0
- package/.framework/migrations/073_seed_member_roles.sql +26 -0
- package/.framework/migrations/074_app_route_prefix_domains.sql +168 -0
- package/.framework/migrations/075_security_definer_grants.sql +157 -0
- package/.framework/src/apps/admin/index.tsx +1141 -775
- package/.framework/src/components/ErrorBoundary.tsx +136 -0
- package/.framework/src/components/admin/AdminListPage.tsx +81 -73
- package/.framework/src/components/admin/AdminSidebar.tsx +88 -79
- package/.framework/src/components/admin/EntitySelect.tsx +181 -0
- package/.framework/src/components/admin/PageInstructions.tsx +86 -127
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
- package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
- package/.framework/src/components/layout/Sidebar.tsx +300 -177
- package/.framework/src/components/runtime/DataDetailPage.tsx +67 -35
- package/.framework/src/components/runtime/DataHeader.tsx +96 -48
- package/.framework/src/components/runtime/DataListPage.tsx +3 -26
- package/.framework/src/components/runtime/DataStats.tsx +10 -10
- package/.framework/src/components/runtime/DataTable.tsx +12 -10
- package/.framework/src/components/ui/sheet.tsx +24 -26
- package/.framework/src/components/ui/textarea.tsx +74 -48
- package/.framework/src/contexts/AppContext.tsx +5 -12
- package/.framework/src/contexts/AuthContext.tsx +17 -9
- package/.framework/src/hooks/useApps.ts +18 -22
- package/.framework/src/hooks/useEntityList.ts +12 -9
- package/.framework/src/hooks/useListSchema.ts +108 -58
- package/.framework/src/instructions/README.md +74 -0
- package/.framework/src/instructions/accounts.md +97 -0
- package/.framework/src/instructions/ai-agents.md +69 -0
- package/.framework/src/instructions/alerts.md +80 -0
- package/.framework/src/instructions/api-keys.md +87 -0
- package/.framework/src/instructions/apps.md +77 -0
- package/.framework/src/instructions/concepts/ai-and-prompts.md +150 -0
- package/.framework/src/instructions/concepts/apps-and-renderers.md +228 -0
- package/.framework/src/instructions/concepts/auth-and-api-keys.md +133 -0
- package/.framework/src/instructions/concepts/automation.md +186 -0
- package/.framework/src/instructions/concepts/concurrency-and-idempotency.md +125 -0
- package/.framework/src/instructions/concepts/core-vs-custom.md +124 -0
- package/.framework/src/instructions/concepts/design-schema.md +152 -0
- package/.framework/src/instructions/concepts/integrations-and-webhooks.md +127 -0
- package/.framework/src/instructions/concepts/observability.md +116 -0
- package/.framework/src/instructions/concepts/permissions.md +150 -0
- package/.framework/src/instructions/concepts/runtime-records.md +111 -0
- package/.framework/src/instructions/concepts/scope-and-accounts.md +112 -0
- package/.framework/src/instructions/concepts/spine-way.md +172 -0
- package/.framework/src/instructions/deliveries.md +59 -0
- package/.framework/src/instructions/guide/00-orientation.md +106 -0
- package/.framework/src/instructions/guide/01-install-and-first-run.md +253 -0
- package/.framework/src/instructions/guide/02-project-map.md +147 -0
- package/.framework/src/instructions/guide/03-domain-model.md +223 -0
- package/.framework/src/instructions/guide/04-first-app.md +227 -0
- package/.framework/src/instructions/guide/05-glossary.md +183 -0
- package/.framework/src/instructions/integrations.md +96 -0
- package/.framework/src/instructions/internal/cil-markers.md +83 -0
- package/.framework/src/instructions/internal/docs-contract.md +158 -0
- package/.framework/src/instructions/invites.md +60 -0
- package/.framework/src/instructions/kb.md +80 -0
- package/.framework/src/instructions/logs.md +77 -0
- package/.framework/src/instructions/notifications.md +77 -0
- package/.framework/src/instructions/observability.md +61 -0
- package/.framework/src/instructions/operations/deployment.md +115 -0
- package/.framework/src/instructions/operations/migrations.md +125 -0
- package/.framework/src/instructions/operations/security-checklist.md +170 -0
- package/.framework/src/instructions/operations/troubleshooting.md +170 -0
- package/.framework/src/instructions/operations/upgrade.md +116 -0
- package/.framework/src/instructions/people.md +91 -0
- package/.framework/src/instructions/pipeline-executions.md +62 -0
- package/.framework/src/instructions/pipelines.md +78 -0
- package/.framework/src/instructions/prompts.md +95 -0
- package/.framework/src/instructions/reference/api/admin-data.md +162 -0
- package/.framework/src/instructions/reference/api/auth.md +113 -0
- package/.framework/src/instructions/reference/api/errors-and-envelopes.md +113 -0
- package/.framework/src/instructions/reference/app-manifest.md +184 -0
- package/.framework/src/instructions/reference/cli.md +124 -0
- package/.framework/src/instructions/reference/database.md +132 -0
- package/.framework/src/instructions/reference/design-schema.md +280 -0
- package/.framework/src/instructions/reference/env-vars.md +133 -0
- package/.framework/src/instructions/reference/frontend-sdk.md +207 -0
- package/.framework/src/instructions/reference/functions/account-nodes.md +75 -0
- package/.framework/src/instructions/reference/functions/admin-data.md +197 -0
- package/.framework/src/instructions/reference/functions/admin-funnel.md +90 -0
- package/.framework/src/instructions/reference/functions/admin-install-funnel.md +107 -0
- package/.framework/src/instructions/reference/functions/admin-signals.md +102 -0
- package/.framework/src/instructions/reference/functions/ai-agents.md +148 -0
- package/.framework/src/instructions/reference/functions/ai-interactions.md +156 -0
- package/.framework/src/instructions/reference/functions/api-keys.md +130 -0
- package/.framework/src/instructions/reference/functions/apps.md +140 -0
- package/.framework/src/instructions/reference/functions/auth.md +99 -0
- package/.framework/src/instructions/reference/functions/debug-auth.md +91 -0
- package/.framework/src/instructions/reference/functions/deliveries.md +110 -0
- package/.framework/src/instructions/reference/functions/embeddings.md +166 -0
- package/.framework/src/instructions/reference/functions/eval-harness.md +149 -0
- package/.framework/src/instructions/reference/functions/index.md +103 -0
- package/.framework/src/instructions/reference/functions/integration-outbound.md +200 -0
- package/.framework/src/instructions/reference/functions/integration-routes.md +151 -0
- package/.framework/src/instructions/reference/functions/integrations.md +130 -0
- package/.framework/src/instructions/reference/functions/invites.md +182 -0
- package/.framework/src/instructions/reference/functions/item-progress.md +107 -0
- package/.framework/src/instructions/reference/functions/kb.md +166 -0
- package/.framework/src/instructions/reference/functions/logs.md +141 -0
- package/.framework/src/instructions/reference/functions/observability.md +105 -0
- package/.framework/src/instructions/reference/functions/pipeline-executions.md +132 -0
- package/.framework/src/instructions/reference/functions/pipeline-runner-background.md +121 -0
- package/.framework/src/instructions/reference/functions/pipelines.md +136 -0
- package/.framework/src/instructions/reference/functions/prompt-configs.md +121 -0
- package/.framework/src/instructions/reference/functions/roles.md +121 -0
- package/.framework/src/instructions/reference/functions/security.md +140 -0
- package/.framework/src/instructions/reference/functions/system-cron.md +136 -0
- package/.framework/src/instructions/reference/functions/system.md +106 -0
- package/.framework/src/instructions/reference/functions/tests.md +97 -0
- package/.framework/src/instructions/reference/functions/timers.md +117 -0
- package/.framework/src/instructions/reference/functions/trigger-scheduler.md +102 -0
- package/.framework/src/instructions/reference/functions/triggers.md +140 -0
- package/.framework/src/instructions/reference/functions/types.md +146 -0
- package/.framework/src/instructions/reference/seeds.md +126 -0
- package/.framework/src/instructions/reference/shared-modules.md +127 -0
- package/.framework/src/instructions/reference/spine-config.md +114 -0
- package/.framework/src/instructions/roles.md +80 -0
- package/.framework/src/instructions/runtime-collaboration.md +106 -0
- package/.framework/src/instructions/runtime-detail.md +70 -0
- package/.framework/src/instructions/runtime-list.md +77 -0
- package/.framework/src/instructions/security.md +67 -0
- package/.framework/src/instructions/tasks/add-a-pipeline.md +110 -0
- package/.framework/src/instructions/tasks/add-a-role.md +96 -0
- package/.framework/src/instructions/tasks/add-a-timer.md +114 -0
- package/.framework/src/instructions/tasks/add-a-trigger.md +106 -0
- package/.framework/src/instructions/tasks/add-a-type.md +151 -0
- package/.framework/src/instructions/tasks/add-a-webhook.md +108 -0
- package/.framework/src/instructions/tasks/add-an-ai-agent.md +108 -0
- package/.framework/src/instructions/tasks/add-an-integration.md +124 -0
- package/.framework/src/instructions/tasks/build-a-custom-page.md +118 -0
- package/.framework/src/instructions/tasks/document-your-app.md +117 -0
- package/.framework/src/instructions/tasks/package-and-install-an-app.md +136 -0
- package/.framework/src/instructions/tasks/register-an-app.md +137 -0
- package/.framework/src/instructions/tasks/test-your-code.md +108 -0
- package/.framework/src/instructions/tasks/write-a-function.md +132 -0
- package/.framework/src/instructions/tasks/write-a-migration.md +110 -0
- package/.framework/src/instructions/testing.md +59 -0
- package/.framework/src/instructions/timers.md +84 -0
- package/.framework/src/instructions/triggers.md +69 -0
- package/.framework/src/instructions/types.md +93 -0
- package/.framework/src/lib/app-mount.ts +88 -0
- package/.framework/src/pages/NotFoundPage.tsx +62 -31
- package/.framework/src/pages/admin/AIAgentDetailPage.tsx +117 -106
- package/.framework/src/pages/admin/AIAgentsPage.tsx +86 -48
- package/.framework/src/pages/admin/APIKeyDetailPage.tsx +153 -152
- package/.framework/src/pages/admin/APIKeysPage.tsx +91 -90
- package/.framework/src/pages/admin/AlertsConfigPage.tsx +29 -18
- package/.framework/src/pages/admin/AppDetailPage.tsx +6 -60
- package/.framework/src/pages/admin/AppsPage.tsx +104 -66
- package/.framework/src/pages/admin/DeliveriesPage.tsx +5 -5
- package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +5 -23
- package/.framework/src/pages/admin/EmbeddingsPage.tsx +5 -26
- package/.framework/src/pages/admin/IntegrationDetailPage.tsx +7 -7
- package/.framework/src/pages/admin/IntegrationsPage.tsx +85 -43
- package/.framework/src/pages/admin/LogsPage.tsx +144 -76
- package/.framework/src/pages/admin/ObservabilityDashboard.tsx +223 -140
- package/.framework/src/pages/admin/PipelineDetailPage.tsx +134 -113
- package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +108 -45
- package/.framework/src/pages/admin/PipelinesPage.tsx +160 -115
- package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +96 -98
- package/.framework/src/pages/admin/PromptConfigsPage.tsx +101 -59
- package/.framework/src/pages/admin/RoleDetailPage.tsx +90 -79
- package/.framework/src/pages/admin/RolesPage.tsx +86 -43
- package/.framework/src/pages/admin/SecurityPage.tsx +1 -1
- package/.framework/src/pages/admin/TestingDashboard.tsx +16 -28
- package/.framework/src/pages/admin/TimerDetailPage.tsx +117 -100
- package/.framework/src/pages/admin/TimersPage.tsx +90 -62
- package/.framework/src/pages/admin/TriggerDetailPage.tsx +90 -83
- package/.framework/src/pages/admin/TriggersPage.tsx +124 -99
- package/.framework/src/pages/admin/TypeDetailPage.tsx +152 -138
- package/.framework/src/pages/admin/TypesPage.tsx +90 -48
- package/.framework/src/pages/admin/cil/CILAgentPage.tsx +17 -25
- package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +17 -27
- package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +72 -40
- package/.framework/src/pages/admin/cil/CILFileListPage.tsx +321 -129
- package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +18 -29
- package/.framework/src/pages/admin/cil/CILGraphPage.tsx +26 -38
- package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +34 -46
- package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +189 -103
- package/.framework/src/pages/admin/cil/CILSearchPage.tsx +246 -136
- package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +11 -22
- package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +9 -20
- package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +26 -38
- package/.framework/src/pages/admin/kb/KBAdminPage.tsx +7 -26
- package/.framework/src/pages/auth/LoginPage.tsx +3 -2
- package/.framework/src/pages/auth/RegisterPage.tsx +16 -8
- package/.framework/templates/agent/AGENTS.md +167 -0
- package/.framework/templates/agent/skills/spine-add-ai-agent/SKILL.md +58 -0
- package/.framework/templates/agent/skills/spine-add-automation/SKILL.md +62 -0
- package/.framework/templates/agent/skills/spine-add-integration/SKILL.md +63 -0
- package/.framework/templates/agent/skills/spine-architect/SKILL.md +86 -0
- package/.framework/templates/agent/skills/spine-install-app/SKILL.md +79 -0
- package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
- package/.framework/templates/agent/skills/spine-new-app/SKILL.md +79 -0
- package/.framework/templates/agent/skills/spine-new-function/SKILL.md +71 -0
- package/.framework/templates/agent/skills/spine-new-type/SKILL.md +72 -0
- package/.framework/templates/agent/skills/spine-schema/SKILL.md +77 -0
- package/.framework/templates/agent/skills/spine-test/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-troubleshoot/SKILL.md +74 -0
- package/.framework/templates/agent/skills/spine-upgrade/SKILL.md +69 -0
- package/.framework/templates/project.json +4 -1
- package/.framework/tests/unit/account-scoping-gates.test.ts +394 -0
- package/.framework/tests/unit/agent-files.test.ts +355 -0
- package/.framework/tests/unit/agent-runner.test.ts +479 -44
- package/.framework/tests/unit/api-keys-create-scope-gate.test.ts +160 -0
- package/.framework/tests/unit/api-keys-omit-secret-fields.test.ts +38 -0
- package/.framework/tests/unit/app-mount.test.ts +80 -0
- package/.framework/tests/unit/bootstrap.test.ts +242 -0
- package/.framework/tests/unit/cli-context-api-key.test.ts +114 -0
- package/.framework/tests/unit/debug-auth.test.ts +38 -0
- package/.framework/tests/unit/deliveries-account-scoping.test.ts +185 -0
- package/.framework/tests/unit/integration-outbound-credentials.test.ts +167 -0
- package/.framework/tests/unit/integrations-audit-redaction.test.ts +130 -0
- package/.framework/tests/unit/logging-app-id.test.ts +75 -0
- package/.framework/tests/unit/security-definer-grants.test.ts +154 -0
- package/.framework/tests/unit/system-context-denial.test.ts +145 -0
- package/.framework/tests/unit/system-cron.test.ts +240 -0
- package/.framework/tests/unit/timers.test.ts +159 -0
- package/README.md +125 -166
- package/config/tsconfig.cli.json +2 -2
- package/package.json +15 -4
- package/templates/project/package-lock.json +2 -2
- package/.framework/docs/admin-routes.md +0 -76
- package/.framework/docs/api-docs-progress.md +0 -38
- package/.framework/docs/api-governance.md +0 -146
- package/.framework/docs/api-testing-results.md +0 -212
- package/.framework/docs/assembly-launch-guide.md +0 -138
- package/.framework/docs/audit-results.md +0 -590
- package/.framework/docs/db-api-inventory.md +0 -95
- package/.framework/docs/gap-remediation-backlog.md +0 -103
- package/.framework/docs/ui-adoption-verification.md +0 -111
- package/.framework/docs/ui-api-coverage.md +0 -84
- package/.framework/docs/v2-compatibility-audit.md +0 -228
- package/.framework/mcp/index.ts +0 -133
- package/.framework/mcp/load-env.ts +0 -60
- package/.framework/mcp/shared/app-manifest.ts +0 -275
- package/.framework/mcp/shared/db.ts +0 -477
- package/.framework/mcp/shared/kb.ts +0 -118
- package/.framework/mcp/tools/architect.ts +0 -431
- package/.framework/mcp/tools/cil/context.ts +0 -345
- package/.framework/mcp/tools/cil/graph.ts +0 -242
- package/.framework/mcp/tools/cil/ingest.ts +0 -1217
- package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +0 -693
- package/.framework/mcp/tools/cil/pipeline.ts +0 -767
- package/.framework/mcp/tools/cil/project.ts +0 -791
- package/.framework/mcp/tools/cil/retrieval.ts +0 -336
- package/.framework/mcp/tools/cil/search-core.ts +0 -454
- package/.framework/mcp/tools/cil/search.ts +0 -97
- package/.framework/mcp/tools/cil/suggest.ts +0 -186
- package/.framework/mcp/tools/cil/validate.ts +0 -1369
- package/.framework/mcp/tools/kb/delete.ts +0 -139
- package/.framework/mcp/tools/kb/index.ts +0 -52
- package/.framework/mcp/tools/kb/ingest.ts +0 -84
- package/.framework/mcp/tools/kb/list-types.ts +0 -102
- package/.framework/mcp/tools/kb/publish.ts +0 -100
- package/.framework/mcp/tools/kb/reindex.ts +0 -86
- package/.framework/mcp/tools/kb/revoke.ts +0 -85
- package/.framework/mcp/tools/kb/search.ts +0 -101
- package/.framework/mcp/tools/kb/upsert.ts +0 -125
- 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
|