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