spine-framework 0.4.3 → 0.5.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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-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/agent-runner.ts +1000 -81
- package/.framework/functions/_shared/cil.ts +2 -1
- package/.framework/functions/_shared/db.ts +3 -6
- package/.framework/functions/_shared/hash-api-key.ts +183 -79
- package/.framework/functions/_shared/kb.ts +61 -7
- package/.framework/functions/_shared/logging.ts +562 -224
- package/.framework/functions/_shared/message-sequence.ts +53 -0
- package/.framework/functions/_shared/middleware.ts +47 -44
- package/.framework/functions/_shared/pipeline-runner.ts +150 -37
- package/.framework/functions/_shared/principal.ts +9 -6
- package/.framework/functions/_shared/trigger-engine.ts +149 -40
- package/.framework/functions/admin-data.ts +48 -31
- package/.framework/functions/ai-agents.ts +103 -33
- package/.framework/functions/api-keys.ts +301 -122
- package/.framework/functions/apps.ts +43 -26
- 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 +145 -76
- 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 +277 -147
- package/.framework/functions/pipeline-executions.ts +1 -1
- package/.framework/functions/pipeline-runner-background.ts +120 -0
- 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 +250 -128
- package/.framework/functions/tests.ts +7 -6
- package/.framework/functions/timers.ts +310 -107
- package/.framework/functions/trigger-scheduler.ts +65 -5
- package/.framework/functions/triggers.ts +28 -13
- package/.framework/functions/types.ts +13 -10
- package/.framework/migrations/064_episode_leases.sql +196 -0
- package/.framework/migrations/065_execution_tables_updated_at.sql +20 -0
- package/.framework/migrations/066_episode_steal_thread_merge.sql +148 -0
- package/.framework/migrations/067_episode_lease_stale_steal.sql +93 -0
- package/.framework/migrations/068_cron_schedule_timer_rpcs.sql +251 -0
- 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/schemas/spine.config.schema.json +62 -0
- package/.framework/src/App.tsx +3 -1
- 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 +92 -81
- package/.framework/src/components/admin/EntitySelect.tsx +181 -0
- package/.framework/src/components/admin/PageInstructions.tsx +86 -127
- package/.framework/src/components/app-shell/GenericAppShell.tsx +3 -1
- package/.framework/src/components/app-shell/GenericDetailPage.tsx +37 -13
- package/.framework/src/components/app-shell/GenericListPage.tsx +28 -32
- package/.framework/src/components/auth/TurnstileWidget.tsx +9 -6
- package/.framework/src/components/kb/KBArticleEditor.tsx +49 -6
- package/.framework/src/components/layout/Layout.tsx +4 -2
- package/.framework/src/components/layout/Sidebar.tsx +304 -179
- 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/hooks/useBranding.ts +47 -0
- package/.framework/src/hooks/useEntityList.ts +12 -9
- package/.framework/src/hooks/useListSchema.ts +108 -58
- package/.framework/src/instructions/README.md +57 -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 +162 -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/invites.md +60 -0
- package/.framework/src/instructions/kb.md +80 -0
- package/.framework/src/instructions/logs.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 +148 -0
- package/.framework/src/instructions/operations/troubleshooting.md +170 -0
- package/.framework/src/instructions/operations/upgrade.md +116 -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 +150 -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/functions/account-nodes.md +75 -0
- package/.framework/src/instructions/reference/functions/admin-data.md +195 -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-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/package-and-install-an-app.md +127 -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 +82 -0
- package/.framework/src/instructions/triggers.md +69 -0
- package/.framework/src/instructions/types.md +93 -0
- package/.framework/src/lib/branding.ts +128 -0
- package/.framework/src/pages/NoAppPage.tsx +3 -1
- package/.framework/src/pages/NotFoundPage.tsx +62 -31
- package/.framework/src/pages/SetupPage.tsx +5 -2
- 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/AuthConfirmPage.tsx +3 -1
- package/.framework/src/pages/auth/ForgotPasswordPage.tsx +3 -1
- package/.framework/src/pages/auth/LoginPage.tsx +4 -2
- package/.framework/src/pages/auth/RegisterPage.tsx +3 -1
- package/.framework/templates/agent/AGENTS.md +150 -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 +72 -0
- package/.framework/templates/agent/skills/spine-migration/SKILL.md +75 -0
- package/.framework/templates/agent/skills/spine-new-app/SKILL.md +67 -0
- package/.framework/templates/agent/skills/spine-new-function/SKILL.md +65 -0
- package/.framework/templates/agent/skills/spine-new-type/SKILL.md +65 -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 +5 -1
- package/.framework/tests/unit/account-scoping-gates.test.ts +392 -0
- package/.framework/tests/unit/agent-files.test.ts +355 -0
- package/.framework/tests/unit/agent-runner.test.ts +496 -47
- 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/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/episode-lease.test.ts +695 -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/kb-ingest-lease.test.ts +19 -0
- package/.framework/tests/unit/logging-app-id.test.ts +75 -0
- package/.framework/tests/unit/pipeline-runner-background.test.ts +120 -0
- package/.framework/tests/unit/pipeline-runner.test.ts +153 -1
- package/.framework/tests/unit/principal-cache.test.ts +15 -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/.framework/tests/unit/trigger-engine.test.ts +278 -91
- package/.framework/tests/unit/trigger-scheduler.test.ts +78 -0
- package/README.md +125 -166
- package/config/tsconfig.cli.json +2 -2
- package/netlify.toml +5 -0
- 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,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
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Design Schema
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: concept
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-29
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/src/types/types.ts
|
|
9
|
+
- .framework/functions/types.ts
|
|
10
|
+
- .framework/functions/admin-data.ts
|
|
11
|
+
- .framework/functions/_shared/schema-utils.ts
|
|
12
|
+
- .framework/functions/_shared/permissions.ts
|
|
13
|
+
- .framework/functions/_shared/resolve-ids.ts
|
|
14
|
+
- .framework/src/components/app-shell/GenericListPage.tsx
|
|
15
|
+
- .framework/src/components/app-shell/GenericDetailPage.tsx
|
|
16
|
+
- .framework/src/hooks/useListSchema.ts
|
|
17
|
+
- .framework/migrations/058_account_data_field_declarations.sql
|
|
18
|
+
- .framework/migrations/060_account_person_record_permissions.sql
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Design Schema
|
|
22
|
+
|
|
23
|
+
`design_schema` is the JSON contract on a `public.types` row that declares
|
|
24
|
+
everything the platform needs to store, validate, permission, and render one
|
|
25
|
+
kind of runtime record — without code. It is the single most important
|
|
26
|
+
structure in Spine: get it right and permissions, validation, and the generic
|
|
27
|
+
UI all work for free; get it wrong or leave it out and records are created
|
|
28
|
+
unpermissionable and unrenderable.
|
|
29
|
+
|
|
30
|
+
The TypeScript shape is `DesignSchema` in `.framework/src/types/types.ts`:
|
|
31
|
+
`fields`, `views`, `record_permissions`, plus optional `scope` and
|
|
32
|
+
`functionality`.
|
|
33
|
+
|
|
34
|
+
## Where it lives and how it propagates
|
|
35
|
+
|
|
36
|
+
- The **type definition** holds the canonical copy in
|
|
37
|
+
`public.types.design_schema` (created/updated only via
|
|
38
|
+
`.framework/functions/types.ts`, which requires `system_admin`).
|
|
39
|
+
- **Every record created through `admin-data` is stamped** with the type's
|
|
40
|
+
`design_schema` and `validation_schema` at insert time
|
|
41
|
+
(`.framework/functions/admin-data.ts`, `create`). The stamped copy on the
|
|
42
|
+
record — not the type row — is what the permission engine reads at request
|
|
43
|
+
time (`.framework/functions/_shared/permissions.ts`,
|
|
44
|
+
`sanitizeFirstSurfaceRecordData` / `validateFirstSurfaceUpdatePermissions`
|
|
45
|
+
both refuse when the record carries no `record_permissions`).
|
|
46
|
+
- **Editing a type does not restamp existing records.** Migrations that change
|
|
47
|
+
a type schema re-stamp rows explicitly — see
|
|
48
|
+
`.framework/migrations/058_account_data_field_declarations.sql` and
|
|
49
|
+
`.framework/migrations/060_account_person_record_permissions.sql` for the
|
|
50
|
+
pattern.
|
|
51
|
+
- `validation_schema` is a derived artifact: `generateValidationSchema` in
|
|
52
|
+
`.framework/functions/_shared/schema-utils.ts` extracts `data_type`,
|
|
53
|
+
`required`, `validation`, `options`, and `reference_*` from `fields` and runs
|
|
54
|
+
on every type create and on every update that touches `design_schema`
|
|
55
|
+
(`.framework/functions/types.ts`). Never hand-edit it — change
|
|
56
|
+
`design_schema` and let the API regenerate.
|
|
57
|
+
|
|
58
|
+
## The sections
|
|
59
|
+
|
|
60
|
+
### `scope`
|
|
61
|
+
|
|
62
|
+
`"account"` (default when omitted) or `"platform"`. At create time `admin-data`
|
|
63
|
+
reads `designSchema.scope ?? 'account'`; `platform` records are stamped with
|
|
64
|
+
the `spine-system` account's id instead of the caller's account. Child records
|
|
65
|
+
created against a parent (via `thread_id`, `target_id`, `parent_id`) also
|
|
66
|
+
inherit the parent's stamped scope. See
|
|
67
|
+
[Scope and Accounts](scope-and-accounts.md).
|
|
68
|
+
|
|
69
|
+
### `fields`
|
|
70
|
+
|
|
71
|
+
A flat map of `snake_case` field names to `FieldDefinition`
|
|
72
|
+
(`.framework/src/types/types.ts`): `data_type`, `label`, `required`, plus
|
|
73
|
+
optional `system`, `placeholder`, `validation` (pattern, min/max, lengths,
|
|
74
|
+
`reference_kind`/`reference_type`, `currency_code`), `options` (required for
|
|
75
|
+
`select`/`multiselect`/`radio`), and `permissions` (per-role `read`/`write`
|
|
76
|
+
overrides — see [Permissions](permissions.md)).
|
|
77
|
+
|
|
78
|
+
The `system` flag controls storage: `system: true` fields are top-level record
|
|
79
|
+
columns; all others live under `record.data.<field>` — see
|
|
80
|
+
[Runtime Records](runtime-records.md).
|
|
81
|
+
|
|
82
|
+
Known `data_type` values (the authoritative switch is `sanitizeFieldData` in
|
|
83
|
+
`.framework/functions/_shared/schema-utils.ts`): `text`, `textarea`,
|
|
84
|
+
`rich_text`, `email`, `phone`, `url`, `number`, `currency`, `range`, `date`,
|
|
85
|
+
`datetime`, `boolean`, `checkbox`, `select`, `multiselect`, `radio`, `color`,
|
|
86
|
+
`file`, `image`, `json`, `reference`, `address`. Unknown types pass through
|
|
87
|
+
unsanitized — they are not rejected, they are unvalidated.
|
|
88
|
+
|
|
89
|
+
### `views`
|
|
90
|
+
|
|
91
|
+
A map of view names to view configs. `default_list` and `default_detail` are
|
|
92
|
+
what the generic renderer consumes:
|
|
93
|
+
|
|
94
|
+
- `default_list` (`type: "list"`, `display: "table" | "card" | "board"`,
|
|
95
|
+
`fields` map with `sortable`/`display_type`, `default_sort`, optional
|
|
96
|
+
`filters`/`stats`) becomes columns in `GenericListPage`
|
|
97
|
+
(`.framework/src/components/app-shell/GenericListPage.tsx`) — the schema
|
|
98
|
+
itself resolves via `useListSchema`, which prefers a stamped record's
|
|
99
|
+
`design_schema` and falls back to the types API.
|
|
100
|
+
- `default_detail` (`type: "detail"`, `sections[]` of `{title, fields}`) drives
|
|
101
|
+
`GenericDetailPage`'s `SchemaDetailForm`; each section field resolves its
|
|
102
|
+
`FieldDefinition` and reads `record[field]` or `record.data[field]` by
|
|
103
|
+
`system` (`.framework/src/components/app-shell/GenericDetailPage.tsx`).
|
|
104
|
+
|
|
105
|
+
`display_type` values for view fields come from `ViewFieldConfig` in
|
|
106
|
+
`.framework/src/types/types.ts` (`input`, `text`, `badge`, `timestamp`,
|
|
107
|
+
`select`, `date_picker`, `file_upload`, …). An app with `renderer = "generic"`
|
|
108
|
+
is broken without these views; a `custom` app may omit them but loses the
|
|
109
|
+
generic admin/runtime fallback.
|
|
110
|
+
|
|
111
|
+
### `record_permissions`
|
|
112
|
+
|
|
113
|
+
`{ "<role_slug>": ["create" | "read" | "update" | "delete", ...] }` — the base
|
|
114
|
+
grant for the record, evaluated per request against the caller's role plus the
|
|
115
|
+
`'all'` wildcard role. This is the contract evaluated in
|
|
116
|
+
`.framework/functions/_shared/permissions.ts`; the full model is in
|
|
117
|
+
[Permissions](permissions.md).
|
|
118
|
+
|
|
119
|
+
If a type has no `record_permissions`, `admin-data` create merges a permissive
|
|
120
|
+
default (`{ all: ["create","read","update","delete"], fields: {} }`) before
|
|
121
|
+
stamping — an explicit empty object would instead deny everything downstream,
|
|
122
|
+
so always author them deliberately.
|
|
123
|
+
|
|
124
|
+
### `functionality`
|
|
125
|
+
|
|
126
|
+
Optional automation bindings the runtime interprets — e.g.
|
|
127
|
+
`functionality.lineage` (parent/child resolution for items, read in
|
|
128
|
+
`admin-data` list/get) and the integration/constraint bindings declared in
|
|
129
|
+
`FunctionalityBindings` (`field_map` sync, `unique`/`conditional_required`/
|
|
130
|
+
`immutable` constraints) in `.framework/src/types/types.ts`. Set it to `null`
|
|
131
|
+
until the automation binding you need is confirmed live.
|
|
132
|
+
|
|
133
|
+
## What each consumer reads
|
|
134
|
+
|
|
135
|
+
| Consumer | Reads |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `admin-data` create | `fields`/`record_permissions`/`scope` for stamping, gating, and account resolution |
|
|
138
|
+
| `permissions.ts` | `record_permissions`, `fields.*.permissions` — on the **record's** stamped schema |
|
|
139
|
+
| `schema-utils.ts` | generated `validation_schema` for `sanitizeFieldData` / `formatFieldData` |
|
|
140
|
+
| `GenericListPage` | `views.default_list` (columns, sort, filters, stats) |
|
|
141
|
+
| `GenericDetailPage` | `views.default_detail`, `fields`, `display_field` |
|
|
142
|
+
| `useEntityRecord` | `fields.*.system` to split system vs `data` writes |
|
|
143
|
+
|
|
144
|
+
## Authoring minimum
|
|
145
|
+
|
|
146
|
+
Every type needs, at minimum: `fields` (each with `data_type`, `label`,
|
|
147
|
+
`required`), `record_permissions` (at least `system_admin`), and — for generic
|
|
148
|
+
apps — `default_list` + `default_detail`. `slug` must be unique per `kind`
|
|
149
|
+
(enforced by the `types_kind_slug_unique` index in
|
|
150
|
+
`.framework/migrations/015_types_kind_slug_unique.sql`), and `kind` must match
|
|
151
|
+
the runtime table the type governs. The entity page
|
|
152
|
+
[../types.md](../types.md) has minimal and full examples.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Integrations and webhooks"
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: concept
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-29
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/integrations.ts
|
|
9
|
+
- .framework/functions/integration-outbound.ts
|
|
10
|
+
- .framework/functions/integration-routes.ts
|
|
11
|
+
- .framework/functions/deliveries.ts
|
|
12
|
+
- .framework/functions/_shared/webhook-registration.ts
|
|
13
|
+
- .framework/functions/_shared/webhook-registry.ts
|
|
14
|
+
- .framework/functions/_shared/resolve-credentials.ts
|
|
15
|
+
- .framework/functions/_shared/retry-deliveries.ts
|
|
16
|
+
- .framework/functions/_shared/notify.ts
|
|
17
|
+
- .framework/functions/adapters/generic-webhook-outbound.ts
|
|
18
|
+
- .framework/migrations/004_core_rpc_and_broker.sql
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
# Integrations and webhooks
|
|
22
|
+
|
|
23
|
+
An **integration** (`public.integrations`) is a named bundle of `config` +
|
|
24
|
+
`credentials` that outbound calls and inbound webhooks share. Spine routes both
|
|
25
|
+
directions through `public.webhook_deliveries`, so every attempt is a durable
|
|
26
|
+
row you can list, inspect, and retry — there is no fire-and-forget.
|
|
27
|
+
|
|
28
|
+
## The integration row
|
|
29
|
+
|
|
30
|
+
`integrations` (`.framework/functions/integrations.ts`) requires `name`,
|
|
31
|
+
`integration_type`, and `provider` on create; it also carries `app_id`,
|
|
32
|
+
`version`, `config`, `credentials`, `metadata`, and a computed
|
|
33
|
+
`is_configured` flag.
|
|
34
|
+
|
|
35
|
+
Credentials live in a dedicated `credentials` jsonb column (added by migration
|
|
36
|
+
`004`) shaped `{ <field>: { source: "value" | "env_var", value: string } }`.
|
|
37
|
+
`resolve-credentials` resolves them **server-side only**, inside the functions
|
|
38
|
+
runtime: `source: "env_var"` reads `process.env[value]`; `source: "value"`
|
|
39
|
+
returns the literal string. Every resolution (or failure) emits an
|
|
40
|
+
`integration.credential_*` audit event. Non-system-admin, non-machine API reads
|
|
41
|
+
of an integration row are sanitized to minimal fields — credential values are
|
|
42
|
+
never returned to ordinary users, though they are stored in the database (use
|
|
43
|
+
`env_var` sources for the tightest posture).
|
|
44
|
+
|
|
45
|
+
## Outbound calls
|
|
46
|
+
|
|
47
|
+
`integration-outbound` (`.framework/functions/integration-outbound.ts`) is the
|
|
48
|
+
single egress path — `callOpenAI` in `_shared/llm.ts`, the `http_request`,
|
|
49
|
+
`webhook`, `send_email`, and `agent_inference` pipeline stages, and manual
|
|
50
|
+
calls all go through its `execute`:
|
|
51
|
+
|
|
52
|
+
1. **Resolve the integration**: account-scoped row matching
|
|
53
|
+
`integration_slug` first, then a system integration (`account_id IS NULL`).
|
|
54
|
+
Both must be `is_active`.
|
|
55
|
+
2. **Open a delivery row**: inserts `webhook_deliveries` with
|
|
56
|
+
`direction: 'outbound'`, `status: 'pending'`, `attempt_count: 0`, and
|
|
57
|
+
`max_attempts` from `action_config.max_attempts` →
|
|
58
|
+
`integration.config.retry_policy.max_attempts` → default 3.
|
|
59
|
+
3. **Resolve the credential** (`action_config.credential_field`, default
|
|
60
|
+
`api_token`) — skipped when the entry is absent or an empty
|
|
61
|
+
`source: 'value'` literal, which means "call without auth"; the adapter
|
|
62
|
+
then decides whether a key is required — and **pick an adapter**:
|
|
63
|
+
`action_config.adapter || integration.config.outbound_adapter`. Built-ins:
|
|
64
|
+
`adapters/openai-outbound`, `adapters/generic-webhook-outbound`,
|
|
65
|
+
`adapters/resend-outbound`; anything else is dynamically imported as a
|
|
66
|
+
sibling module exposing `buildRequest`.
|
|
67
|
+
4. **Send**: the adapter builds `{ url, method, headers, body }`; the function
|
|
68
|
+
fetches it, then updates the delivery to `delivered` (with
|
|
69
|
+
`response_status`, `response_body`, `duration_ms`) or `pending`/`failed`.
|
|
70
|
+
5. Optionally, `action_config.update_record` stamps
|
|
71
|
+
`last_outbound_status` / `last_outbound_at` back onto the source item.
|
|
72
|
+
|
|
73
|
+
## Delivery and retry semantics
|
|
74
|
+
|
|
75
|
+
Both directions share the same retry math: `computeNextAttempt` is exponential
|
|
76
|
+
backoff — `30s * 2^attempt`, capped at 1 hour, with up to 20% jitter. A failed
|
|
77
|
+
attempt stays `pending` with `next_attempt_at` set until `attempt_count`
|
|
78
|
+
reaches `max_attempts`, when it becomes `failed`.
|
|
79
|
+
|
|
80
|
+
Retries are driven by `retryPendingDeliveries`
|
|
81
|
+
(`.framework/functions/_shared/retry-deliveries.ts`), which `system-cron` runs
|
|
82
|
+
every tick (see [automation](./automation.md)). It sweeps `webhook_deliveries`
|
|
83
|
+
rows that are `pending`, `attempt_count < 10`, and `next_attempt_at` due (or
|
|
84
|
+
null), up to 100 per pass, and re-invokes `integration-outbound` with
|
|
85
|
+
`delivery_id` under the system principal — the `delivery_id` path reuses the
|
|
86
|
+
existing row instead of inserting a duplicate. You can also retry one delivery
|
|
87
|
+
manually via `POST /api/deliveries?action=retry { id }`
|
|
88
|
+
(`.framework/functions/deliveries.ts`); manual retry is outbound-only. Inbound
|
|
89
|
+
retry is not implemented — inbound failures are counted and logged.
|
|
90
|
+
|
|
91
|
+
`deliveries` list/get are account-scoped reads over the same table, filterable
|
|
92
|
+
by `direction`, `status`, `integration_id`, `api_key_id`.
|
|
93
|
+
|
|
94
|
+
## Inbound webhooks
|
|
95
|
+
|
|
96
|
+
`integration-routes` (`.framework/functions/integration-routes.ts`) is a
|
|
97
|
+
`createPublicHandler` endpoint — it resolves the caller as an **API-key
|
|
98
|
+
machine principal** (see [auth](./auth-and-api-keys.md)):
|
|
99
|
+
|
|
100
|
+
1. `?slug=` names the integration; the caller's API key must belong to the
|
|
101
|
+
same account, and keys pinned to `integration_id` or `app_id` can only hit
|
|
102
|
+
those integrations.
|
|
103
|
+
2. The event name comes from the `x-event-type` header or
|
|
104
|
+
`integration.config.event_path` (a dot-path into the payload).
|
|
105
|
+
3. `config.handler_name || provider || 'generic-webhook'` is resolved through
|
|
106
|
+
the **webhook registry** (`_shared/webhook-registry.ts`): the
|
|
107
|
+
`webhook_handlers` table maps a handler name to a `function_name`, scoped
|
|
108
|
+
app → account → global fallback. The named module is dynamically imported
|
|
109
|
+
(`custom/…` paths resolve into `custom/`) and must export `adapt`,
|
|
110
|
+
`handler`, or a default function.
|
|
111
|
+
4. The adapter returns `{ type_slug, data, title?, status? }` — the route then
|
|
112
|
+
creates an `items` record through `admin-data` (full permission/type/audit
|
|
113
|
+
pipeline) and marks the inbound delivery `delivered`. Adapter errors marked
|
|
114
|
+
`retryable` leave it `pending`.
|
|
115
|
+
|
|
116
|
+
Handlers are registered with `registerWebhookHandler`
|
|
117
|
+
(`_shared/webhook-registration.ts`), an upsert on `(app_id, account_id,
|
|
118
|
+
name)`; `deregisterWebhookHandler` soft-deletes (`is_active=false`,
|
|
119
|
+
`is_deleted=true`).
|
|
120
|
+
|
|
121
|
+
## Notifications
|
|
122
|
+
|
|
123
|
+
`_shared/notify.ts` resolves pipeline `send_notification` recipients: each
|
|
124
|
+
entry is either a person UUID or `role:<slug>`, resolved to active `people`
|
|
125
|
+
rows in the caller's account via the `role_id → roles.slug` join. Anything
|
|
126
|
+
else is skipped with a warning. The resulting ids feed the `notifications`
|
|
127
|
+
insert in the pipeline runner.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Observability: logs, deliveries, executions"
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: concept
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-29
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/logs.ts
|
|
9
|
+
- .framework/functions/observability.ts
|
|
10
|
+
- .framework/functions/deliveries.ts
|
|
11
|
+
- .framework/functions/pipeline-executions.ts
|
|
12
|
+
- .framework/functions/item-progress.ts
|
|
13
|
+
- .framework/functions/_shared/logging.ts
|
|
14
|
+
- .framework/functions/_shared/tracing.ts
|
|
15
|
+
- .framework/functions/_shared/audit.ts
|
|
16
|
+
- .framework/migrations/069_log_stats_window.sql
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# Observability: logs, deliveries, executions
|
|
20
|
+
|
|
21
|
+
Everything an operator needs lands in a small set of queryable tables. There is
|
|
22
|
+
no external telemetry service: `public.logs` is the unified sink, and the
|
|
23
|
+
`logs`, `observability`, `deliveries`, and `pipeline-executions` functions are
|
|
24
|
+
the read APIs over it.
|
|
25
|
+
|
|
26
|
+
## What gets written, and where
|
|
27
|
+
|
|
28
|
+
`public.logs` holds three overlapping kinds of rows:
|
|
29
|
+
|
|
30
|
+
- **Audit/log events** — `emitLog`, `emitAudit`, and `emitActivity` in
|
|
31
|
+
`_shared/audit.ts` insert business events (`trigger.fired`,
|
|
32
|
+
`pipeline.completed`, `integration.outbound.failed_http`,
|
|
33
|
+
`api_key.created`, `registration.completed`, …) with actor, target, and
|
|
34
|
+
details.
|
|
35
|
+
- **Spans** — `emitSpan` writes trace spans (`span_name`, `trace_id`,
|
|
36
|
+
`span_id`, `parent_span_id`, `duration_ms`, `outcome`). `_shared/tracing.ts`
|
|
37
|
+
implements W3C `traceparent` parsing/formatting, and `traceDb` wraps the
|
|
38
|
+
request's DB client so every query emits a `db.<operation>` span — a single
|
|
39
|
+
`trace_id` stitches the request, its queries, and the pipeline stages it
|
|
40
|
+
caused.
|
|
41
|
+
- **Operation logs** — `logAdminDataOperation` in `_shared/logging.ts` writes
|
|
42
|
+
admin-data CRUD records, gated by resolved log levels.
|
|
43
|
+
|
|
44
|
+
Log **levels** are `'off' | 'timing' | 'warnings' | 'errors' | 'all'`, resolved
|
|
45
|
+
per request from `system_settings` rows (`key='logging'`, global plus scoped
|
|
46
|
+
overrides); the default is `['timing', 'errors']`. Under the default, errors
|
|
47
|
+
and timing rows are written; `warnings`-level and `all`-level verbosity require
|
|
48
|
+
opt-in settings. A request's `x-log-level` header can raise verbosity for that
|
|
49
|
+
call.
|
|
50
|
+
|
|
51
|
+
## Reading logs
|
|
52
|
+
|
|
53
|
+
`logs` (`.framework/functions/logs.ts`) is system-admin oriented (system
|
|
54
|
+
admins query with the service client across accounts; account members get
|
|
55
|
+
their RLS-scoped view):
|
|
56
|
+
|
|
57
|
+
- `action=account` / `action=target` / `action=person` — filtered lists.
|
|
58
|
+
- `action=search` — `ILIKE` message search.
|
|
59
|
+
- `action=stats` — calls the `get_log_stats` RPC (totals, error count,
|
|
60
|
+
distinct levels/sources). **Note:** since migration `069`, when no
|
|
61
|
+
`p_date_from` is passed the window defaults to the **last 7 days** — a call
|
|
62
|
+
that omits the date range returns 7-day stats, not all-time stats.
|
|
63
|
+
- `action=cleanup` — deletes rows older than `days_to_keep` (default 30).
|
|
64
|
+
`system-cron` also runs `cleanup_old_logs` on the 00:00 UTC tick, so
|
|
65
|
+
retention is enforced continuously.
|
|
66
|
+
|
|
67
|
+
`observability` (`.framework/functions/observability.ts`) aggregates the same
|
|
68
|
+
rows into dashboard metrics — actions `event_volume`, `error_rate`,
|
|
69
|
+
`latency_percentiles`, `pipeline_stats`, `top_actors`, `ai_support_metrics`,
|
|
70
|
+
`cleanup` — scoped to the caller's account unless the principal is
|
|
71
|
+
system-admin.
|
|
72
|
+
|
|
73
|
+
## Executions and deliveries
|
|
74
|
+
|
|
75
|
+
Two sibling tables answer "did it run / did it arrive":
|
|
76
|
+
|
|
77
|
+
- `public.pipeline_executions` — one row per pipeline run with `status`
|
|
78
|
+
(`queued` → `running` → `completed`/`failed`/`cancelled`), per-stage results,
|
|
79
|
+
`error_message`, `duration_ms`, and the `trigger_data` that caused it
|
|
80
|
+
(`.framework/functions/pipeline-executions.ts`; `action=running`,
|
|
81
|
+
`action=stats`, `action=cleanup`). Rows that never reach a terminal state
|
|
82
|
+
are swept to `failed` after 10 minutes.
|
|
83
|
+
- `public.webhook_deliveries` — one row per inbound/outbound webhook attempt
|
|
84
|
+
with `direction`, `status` (`pending`/`processing`/`delivered`/`failed`),
|
|
85
|
+
`attempt_count`, `next_attempt_at`, `last_error`, `response_status`, and
|
|
86
|
+
`response_body` (`.framework/functions/deliveries.ts`; `action=retry`).
|
|
87
|
+
|
|
88
|
+
`item-progress` (`.framework/functions/item-progress.ts`) is a different lens:
|
|
89
|
+
per-person, per-item progress rows (`status`, `score`, `attempts`) that follow
|
|
90
|
+
the type's `design_schema.pipeline` stage list (default
|
|
91
|
+
`not_started → in_progress → completed`). Transitions must move forward
|
|
92
|
+
unless `force: true` is passed; `started_at`/`completed_at` are stamped on
|
|
93
|
+
first entry. Use it when you need per-user completion state rather than
|
|
94
|
+
execution forensics.
|
|
95
|
+
|
|
96
|
+
## Debugging a failed automation
|
|
97
|
+
|
|
98
|
+
Work backwards through the tables:
|
|
99
|
+
|
|
100
|
+
1. **`pipeline_executions`** — find the run. `status: 'failed'` +
|
|
101
|
+
`error_message` and per-stage results name the failing stage.
|
|
102
|
+
`status: 'queued'` past 10 minutes = dispatch never arrived (sweeper will
|
|
103
|
+
mark it failed); `status: 'running'` stuck = the runner died (same
|
|
104
|
+
sweeper).
|
|
105
|
+
2. **`logs`** — search by `trace_id` (from the request) or `span_name`
|
|
106
|
+
(`pipeline.run`, `pipeline.stage.<handler>`, `db.<operation>`); audit rows
|
|
107
|
+
`trigger.fired` / `trigger.failed` / `pipeline.completed` /
|
|
108
|
+
`pipeline.failed` bracket the run. Check log levels if expected `timing`
|
|
109
|
+
rows are absent.
|
|
110
|
+
3. **`webhook_deliveries`** — for outbound stages/integrations, the delivery
|
|
111
|
+
row carries `response_status`, `response_body`, `last_error`, and
|
|
112
|
+
`next_attempt_at`; retry with `action=retry`.
|
|
113
|
+
4. **`api_key_usage_logs`** (`api-keys?action=usage-logs`) — whether the
|
|
114
|
+
machine caller ever arrived, and from where.
|
|
115
|
+
5. **`observability` actions** — confirm the blast radius (`error_rate`,
|
|
116
|
+
`pipeline_stats`, `top_actors`) rather than chasing a single event.
|