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,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.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Permission Model
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: concept
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-29
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/_shared/permissions.ts
|
|
9
|
+
- .framework/functions/_shared/principal.ts
|
|
10
|
+
- .framework/functions/_shared/db.ts
|
|
11
|
+
- .framework/functions/_shared/middleware.ts
|
|
12
|
+
- .framework/functions/admin-data.ts
|
|
13
|
+
- .framework/migrations/001_core_schema.sql
|
|
14
|
+
- .framework/migrations/002_core_rls_and_functions.sql
|
|
15
|
+
- .framework/migrations/003_core_system_data.sql
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Permission Model
|
|
19
|
+
|
|
20
|
+
Spine authorization is two independent layers, and the code says so verbatim:
|
|
21
|
+
**"RLS controls row access; design_schema controls what the principal can do.
|
|
22
|
+
No permissions granted = none given."** (`.framework/functions/_shared/permissions.ts`)
|
|
23
|
+
|
|
24
|
+
- **Row visibility (RLS):** Postgres policies scope every tenant table to the
|
|
25
|
+
accounts the caller can reach — see [Scope and Accounts](scope-and-accounts.md).
|
|
26
|
+
- **Action authorization (`record_permissions`):** per request, the permission
|
|
27
|
+
engine evaluates the caller's role against the schema **stamped on the
|
|
28
|
+
record** to decide create/read/update/delete and per-field read/write.
|
|
29
|
+
|
|
30
|
+
## Principals
|
|
31
|
+
|
|
32
|
+
`.framework/functions/_shared/principal.ts` resolves two principal types:
|
|
33
|
+
|
|
34
|
+
- **Human** — Supabase JWT → `people` row → `role_id` join to `roles`; the
|
|
35
|
+
principal carries `roles: [slug]` and gets a JWT-scoped DB client (`ctx.db`),
|
|
36
|
+
so RLS applies.
|
|
37
|
+
- **Machine** — `x-api-key` header → `api_keys` row → `scopes` array and the
|
|
38
|
+
key's `account_id`. Machine principals get `adminDb` (service role), which
|
|
39
|
+
**bypasses RLS entirely**; their only authorization is the scope check.
|
|
40
|
+
|
|
41
|
+
## The three surfaces
|
|
42
|
+
|
|
43
|
+
`detectSurface` in `.framework/functions/_shared/permissions.ts` buckets the
|
|
44
|
+
target by table name:
|
|
45
|
+
|
|
46
|
+
| Surface | Covers | Rule |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| First (default) | runtime records — items, threads, messages, people, accounts, … | `record_permissions` on the record's stamped `design_schema` |
|
|
49
|
+
| Second | config objects — `apps`, `pipelines`, `triggers`, `ai_agents`, `embeddings`, `timers`, `integrations`, `roles`, `types`, `prompt_configs` | `system_admin` full access; machine principals read-only; everyone else denied |
|
|
50
|
+
| Third | system metadata — `logs`, `pipeline_executions`, `trigger_executions`, `link_types`, `links` | `system_admin`/machine full; humans read only their own/account rows |
|
|
51
|
+
|
|
52
|
+
## First-surface evaluation
|
|
53
|
+
|
|
54
|
+
`resolveFirstSurfacePermissions` runs on every runtime-record access:
|
|
55
|
+
|
|
56
|
+
1. **Schema source is the record.** The stamped `design_schema` on the record
|
|
57
|
+
is authoritative; there is deliberately no fallback to the type row — a
|
|
58
|
+
record with no schema or no `record_permissions` is denied.
|
|
59
|
+
2. **Role resolution.** The caller's role slug comes from
|
|
60
|
+
`people.role_id → roles.slug` (one role per person, cached ~60s). No role,
|
|
61
|
+
no permissions.
|
|
62
|
+
3. **Record grant.** `record_permissions[roleSlug]` is an array of actions
|
|
63
|
+
(`create`, `read`, `update`, `delete`). The special key `'all'` is a
|
|
64
|
+
wildcard evaluated for every authenticated principal — grants to `all`
|
|
65
|
+
apply to anyone who passed RLS.
|
|
66
|
+
4. **Field merge.** For each declared field, `fields.<name>.permissions[role]`
|
|
67
|
+
— an array drawn from `read`/`write` only — replaces the default. Fields
|
|
68
|
+
**without** an entry default to `{ read: canRead, write: canUpdate }` from
|
|
69
|
+
the record grant. That is why `record_permissions` is the base, most
|
|
70
|
+
restrictive grant and field entries are *overrides*: an explicit `["read"]`
|
|
71
|
+
on a field removes write access that `update` would otherwise give.
|
|
72
|
+
|
|
73
|
+
> **Role keys are matched verbatim.** `record_permissions` keys must be real
|
|
74
|
+
> `roles.slug` values — nothing normalizes them. The seeded admin slug is
|
|
75
|
+
> `system_admin` (underscore, `.framework/migrations/003_core_system_data.sql`);
|
|
76
|
+
> a `system-admin` key silently matches nobody and denies the admin.
|
|
77
|
+
|
|
78
|
+
## Enforcement points
|
|
79
|
+
|
|
80
|
+
- **Read** (`sanitizeFirstSurfaceRecordData`): `canRead` gates the record;
|
|
81
|
+
`data` is filtered per field by `fieldPermissions[name].read`; values are
|
|
82
|
+
formatted through `validation_schema` (`formatFieldData`); legacy `metadata`
|
|
83
|
+
is stripped from responses. Records with an empty `fields` map pass through
|
|
84
|
+
whole at the record grant.
|
|
85
|
+
- **Update** (`validateFirstSurfaceUpdatePermissions`): `canUpdate` gates the
|
|
86
|
+
record, then **every key inside `data` must have `write`** — one denied field
|
|
87
|
+
fails the whole update with a per-field error. Values are sanitized through
|
|
88
|
+
`sanitizeFieldData` (type coercion + constraint checks; violations are
|
|
89
|
+
errors, not silent drops). Top-level non-`data` keys pass through the gate.
|
|
90
|
+
Writes therefore require both the record action *and* field-level write —
|
|
91
|
+
undeclared fields have no `fieldPermissions` entry and 403.
|
|
92
|
+
- **Create** (`admin-data` `create`): permission is checked against the type's
|
|
93
|
+
`design_schema` (which becomes the record's stamp) before insert;
|
|
94
|
+
`type_id`/`type_slug` is required. A machine principal with `*:*` scope
|
|
95
|
+
bypasses the create check.
|
|
96
|
+
|
|
97
|
+
## `system_admin` bypass
|
|
98
|
+
|
|
99
|
+
`isSystemAdmin` (`roles` contains `system_admin`) short-circuits record
|
|
100
|
+
access, sanitization, and update validation in `permissions.ts`, and gates the
|
|
101
|
+
`types.ts`/`roles.ts`/`apps.ts` write APIs entirely. The same slug drives the
|
|
102
|
+
RLS bypass inside `get_accessible_accounts` — system admins see all accounts.
|
|
103
|
+
|
|
104
|
+
## Machine (API key) scopes
|
|
105
|
+
|
|
106
|
+
`checkMachineScope` grants an action when the key's `scopes` contain
|
|
107
|
+
`resource:action` exactly, `resource:*`, or `*:*` (e.g. `items:read`,
|
|
108
|
+
`items:*`, `*:*`). Scopes are the *only* thing standing between a leaked API
|
|
109
|
+
key and `adminDb`, so issue the narrowest scope that works — `*:*` also
|
|
110
|
+
bypasses the create permission check in `admin-data`.
|
|
111
|
+
|
|
112
|
+
## Canonical example
|
|
113
|
+
|
|
114
|
+
The seeded pattern from the framework migrations — base grant first, field
|
|
115
|
+
override only where needed:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"record_permissions": {
|
|
120
|
+
"system_admin": ["create", "read", "update", "delete"],
|
|
121
|
+
"member_admin": ["create", "read", "update"],
|
|
122
|
+
"member": ["create", "read", "update"]
|
|
123
|
+
},
|
|
124
|
+
"fields": {
|
|
125
|
+
"title": { "data_type": "text", "label": "Title", "required": true, "system": true },
|
|
126
|
+
"status": { "data_type": "select", "label": "Status", "required": true,
|
|
127
|
+
"options": ["open", "closed"],
|
|
128
|
+
"permissions": { "member": ["read"] } },
|
|
129
|
+
"internal_notes": { "data_type": "textarea", "label": "Internal Notes",
|
|
130
|
+
"required": false,
|
|
131
|
+
"permissions": { "member": [] } }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Reading it: `member` can create and update records, so every declared field
|
|
137
|
+
defaults to read+write — except `status`, whose `["read"]` override strips
|
|
138
|
+
`member`'s write, and `internal_notes`, whose empty override hides it from
|
|
139
|
+
`member` entirely. `member_admin` touches neither override, so it inherits
|
|
140
|
+
record-level read+write on both. That is the whole discipline: **add a
|
|
141
|
+
`fields.*.permissions` entry only where the base grant is wrong for that
|
|
142
|
+
field** — usually to restrict a sensitive or `system: true` field, occasionally
|
|
143
|
+
to loosen one the base grant closes.
|
|
144
|
+
|
|
145
|
+
## See also
|
|
146
|
+
|
|
147
|
+
- [Design Schema](design-schema.md) — where `record_permissions` lives
|
|
148
|
+
- [Scope and Accounts](scope-and-accounts.md) — the RLS layer beneath this one
|
|
149
|
+
- [../roles.md](../roles.md) — the role entity (`permissions` on roles bound
|
|
150
|
+
the *subject* side at the app/UI level)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Runtime Records
|
|
3
|
+
audience: developers, agents
|
|
4
|
+
kind: concept
|
|
5
|
+
verified_against: f69c37d
|
|
6
|
+
verified_on: 2026-10-29
|
|
7
|
+
source_files:
|
|
8
|
+
- .framework/functions/admin-data.ts
|
|
9
|
+
- .framework/functions/_shared/resolve-ids.ts
|
|
10
|
+
- .framework/functions/_shared/permissions.ts
|
|
11
|
+
- .framework/migrations/001_core_schema.sql
|
|
12
|
+
- .framework/src/hooks/useEntityRecord.ts
|
|
13
|
+
- .framework/src/components/app-shell/GenericDetailPage.tsx
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Runtime Records
|
|
17
|
+
|
|
18
|
+
Runtime records are the rows your users and apps actually create — as opposed
|
|
19
|
+
to config entities (`types`, `apps`, `roles`, `pipelines`, …) that describe
|
|
20
|
+
the system itself. They share one storage contract: a type, an account, a
|
|
21
|
+
stamped schema, and a split between framework-owned columns and app-owned
|
|
22
|
+
`data`.
|
|
23
|
+
|
|
24
|
+
## The record kinds
|
|
25
|
+
|
|
26
|
+
`admin-data` maps entity names to type `kind`s via `ENTITY_TO_KIND`
|
|
27
|
+
(`.framework/functions/admin-data.ts`):
|
|
28
|
+
|
|
29
|
+
| Entity (table) | `kind` | What it is |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `items` | `item` | The workhorse — tickets, tasks, articles, any typed business object |
|
|
32
|
+
| `threads` | `thread` | A conversation bound to a target (`target_type` + `target_id`); unique per `(target_type, target_id, visibility)` |
|
|
33
|
+
| `messages` | `message` | An entry in a thread (`thread_id` + `sequence`, unique together) |
|
|
34
|
+
| `links` | `link` | A polymorphic edge between two records (`source_type`/`source_id` → `target_type`/`target_id`, optional `link_type`) |
|
|
35
|
+
| `attachments` | `attachment` | A stored file reference (filename, size, mime, `storage_path`) |
|
|
36
|
+
| `watchers` | `watcher` | A person subscribed to a target (unique per target+person) |
|
|
37
|
+
| `people` | `person` | Users/contacts — also carry `auth_uid`, `role_id`, `email` |
|
|
38
|
+
| `accounts` | `account` | Tenants — the only table without `account_id` (it *is* the account) |
|
|
39
|
+
| `item_progress` | `item_progress` | Progress/status snapshots for items |
|
|
40
|
+
|
|
41
|
+
`VALID_ENTITIES` also accepts `logs`, `test_runs`, `observability`, and
|
|
42
|
+
`test_cases` — the `RAW_ENTITIES` path: whitelisted raw-column inserts with no
|
|
43
|
+
type requirement and no `design_schema`. `types` and `link_types` are accepted
|
|
44
|
+
too but are config, not runtime records.
|
|
45
|
+
|
|
46
|
+
## The shared row shape
|
|
47
|
+
|
|
48
|
+
Every runtime table in `.framework/migrations/001_core_schema.sql` carries the
|
|
49
|
+
same spine:
|
|
50
|
+
|
|
51
|
+
- `id`, `created_at`, `updated_at`, `created_by` (+ `updated_by` on most)
|
|
52
|
+
- `type_id uuid NOT NULL` — every record must reference a type
|
|
53
|
+
- `account_id` — the owning tenant (all except `accounts`)
|
|
54
|
+
- `app_id` — optional attribution to the app that created it
|
|
55
|
+
- `data jsonb` — app-defined field values
|
|
56
|
+
- `design_schema jsonb`, `validation_schema jsonb` — **stamped at creation**
|
|
57
|
+
from the type; the runtime source of truth for permissions and validation
|
|
58
|
+
(see [Design Schema](design-schema.md))
|
|
59
|
+
- `is_active` — soft-delete flag; `remove` flips it rather than deleting
|
|
60
|
+
|
|
61
|
+
`admin-data` create requires `type_id` *or* `type_slug` — `resolveTypeRecord`
|
|
62
|
+
in `.framework/functions/_shared/resolve-ids.ts` resolves `(kind, slug)` to
|
|
63
|
+
the type row, and the record is stamped before permission evaluation and
|
|
64
|
+
insert.
|
|
65
|
+
|
|
66
|
+
## `system: true` columns vs `record.data`
|
|
67
|
+
|
|
68
|
+
The field declaration's `system` flag decides where a value lives:
|
|
69
|
+
|
|
70
|
+
- **`system: true`** → a top-level record column (`record.title`,
|
|
71
|
+
`record.status`, `record.email`). These are framework-owned or
|
|
72
|
+
schema-critical fields.
|
|
73
|
+
- **`system: false` / omitted** → inside `record.data.<field>`.
|
|
74
|
+
|
|
75
|
+
Consumers split on the flag — this is the authoritative convention:
|
|
76
|
+
|
|
77
|
+
- `useEntityRecord.save` partitions form data into `systemData` (top-level
|
|
78
|
+
keys) and `customData` (packed into `data`) by `fieldDef.system`
|
|
79
|
+
(`.framework/src/hooks/useEntityRecord.ts`).
|
|
80
|
+
- `GenericDetailPage` initializes each field from `record[field]` when
|
|
81
|
+
`fieldDef.system` and `record.data[field]` otherwise
|
|
82
|
+
(`.framework/src/components/app-shell/GenericDetailPage.tsx`).
|
|
83
|
+
- The permission engine's write gate evaluates every key inside `data`
|
|
84
|
+
against `fieldPermissions[name].write` and sanitizes through
|
|
85
|
+
`validation_schema` (`.framework/functions/_shared/permissions.ts`).
|
|
86
|
+
|
|
87
|
+
**Fact:** send system fields as top-level keys and non-system fields inside a
|
|
88
|
+
`data` object. A non-system field sent top-level is stored but invisible to
|
|
89
|
+
schema-driven readers; a system field sent inside `data` never reaches its
|
|
90
|
+
column.
|
|
91
|
+
|
|
92
|
+
## Reading records back
|
|
93
|
+
|
|
94
|
+
Reads pass through `sanitizeRecordData`: fields the caller can't read are
|
|
95
|
+
dropped from `data`, the legacy `metadata` key is stripped from responses, and
|
|
96
|
+
remaining values are formatted through `validation_schema`
|
|
97
|
+
(`formatFieldData` — e.g. currency, multiselect, dates). A record the caller
|
|
98
|
+
can't read at all collapses to `{id, created_at, updated_at}` — so "record
|
|
99
|
+
exists but looks empty" is a permission answer, not missing data.
|
|
100
|
+
|
|
101
|
+
## Writing rules of thumb
|
|
102
|
+
|
|
103
|
+
- `type_slug` + `entity` on create; `id` on update/delete.
|
|
104
|
+
- Never write `design_schema`, `validation_schema`, `type_id`, `account_id`,
|
|
105
|
+
or `id` in the body — create strips them before insert (`account_id` in the
|
|
106
|
+
body is honored as a *targeting* hint, see
|
|
107
|
+
[Scope and Accounts](scope-and-accounts.md)).
|
|
108
|
+
- Deletes are soft (`is_active = false`) for the standard entities; only a few
|
|
109
|
+
entities hard-delete.
|
|
110
|
+
- Creates fire triggers (`fireCreateTriggers`) and per-type rate limits after
|
|
111
|
+
permission checks — a custom insert path loses both.
|