create-flowdular 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +8 -5
  2. package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
  3. package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
  4. package/agent-template/.agents/skills/database-adapter/SKILL.md +5 -5
  5. package/agent-template/.agents/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  6. package/agent-template/.agents/skills/deploy-operate/SKILL.md +1 -1
  7. package/agent-template/.agents/skills/migration-authoring/SKILL.md +4 -4
  8. package/agent-template/.agents/skills/module-new/SKILL.md +1 -1
  9. package/agent-template/.agents/skills/spec-approval/SKILL.md +6 -2
  10. package/agent-template/.agents/skills/spec-interview/SKILL.md +2 -2
  11. package/agent-template/.agents/skills/test-hardening/SKILL.md +2 -2
  12. package/agent-template/.agents/skills/workflow-development/SKILL.md +95 -9
  13. package/agent-template/.ai/agents/sandbox/business-manager.md +2 -2
  14. package/agent-template/.ai/blueprints/add-migration/required-files.yaml +1 -1
  15. package/agent-template/.ai/blueprints/new-module/required-files.yaml +1 -1
  16. package/agent-template/.ai/platform-capabilities.md +10 -8
  17. package/agent-template/.ai/policies/capabilities.yaml +28 -12
  18. package/agent-template/.ai/skills/README.md +1 -1
  19. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -1
  20. package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
  21. package/agent-template/.ai/skills/database-adapter/SKILL.md +5 -5
  22. package/agent-template/.ai/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  23. package/agent-template/.ai/skills/deploy-operate/SKILL.md +1 -1
  24. package/agent-template/.ai/skills/migration-authoring/SKILL.md +4 -4
  25. package/agent-template/.ai/skills/module-new/SKILL.md +1 -1
  26. package/agent-template/.ai/skills/spec-approval/SKILL.md +6 -2
  27. package/agent-template/.ai/skills/spec-interview/SKILL.md +2 -2
  28. package/agent-template/.ai/skills/test-hardening/SKILL.md +2 -2
  29. package/agent-template/.ai/skills/workflow-development/SKILL.md +96 -10
  30. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
  31. package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
  32. package/agent-template/.claude/skills/database-adapter/SKILL.md +5 -5
  33. package/agent-template/.claude/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  34. package/agent-template/.claude/skills/deploy-operate/SKILL.md +1 -1
  35. package/agent-template/.claude/skills/migration-authoring/SKILL.md +4 -4
  36. package/agent-template/.claude/skills/module-new/SKILL.md +1 -1
  37. package/agent-template/.claude/skills/spec-approval/SKILL.md +6 -2
  38. package/agent-template/.claude/skills/spec-interview/SKILL.md +2 -2
  39. package/agent-template/.claude/skills/test-hardening/SKILL.md +2 -2
  40. package/agent-template/.claude/skills/workflow-development/SKILL.md +95 -9
  41. package/agent-template/docs/adr/0003-module-settings.md +2 -0
  42. package/agent-template/docs/adr/0007-module-owned-agents.md +35 -1
  43. package/agent-template/docs/agent-contract.md +3 -3
  44. package/agent-template/docs/cli-extensions.md +1 -0
  45. package/agent-template/docs/cli.md +40 -3
  46. package/agent-template/docs/configuration.md +64 -9
  47. package/agent-template/docs/database-adapters.md +30 -22
  48. package/agent-template/docs/design-system.md +7 -3
  49. package/agent-template/docs/getting-started.md +29 -32
  50. package/agent-template/docs/module-distribution.md +79 -86
  51. package/agent-template/docs/module-web-surfaces.md +9 -7
  52. package/agent-template/docs/modules.md +6 -2
  53. package/agent-template/docs/sandbox.md +23 -4
  54. package/agent-template/platform/scripts/build.mjs +7 -0
  55. package/dist/bin.js +3 -6
  56. package/package.json +1 -1
  57. package/template/default/.env.example +8 -3
  58. package/template/default/.vercelignore +8 -0
  59. package/template/default/README.md +20 -11
  60. package/template/default/_gitignore +3 -2
  61. package/template/default/infra/README.md +86 -65
  62. package/template/default/infra/docker/.env.example +71 -0
  63. package/template/default/infra/docker/Dockerfile +29 -11
  64. package/template/default/infra/docker/app-entrypoint.mjs +5 -0
  65. package/template/default/infra/docker/compose.yaml +109 -58
  66. package/template/default/infra/docker/database-urls.mjs +28 -0
  67. package/template/default/infra/docker/pitr.sh +177 -0
  68. package/template/default/infra/docker/postgres/10-roles.sh +16 -12
  69. package/template/default/infra/docker/start.mjs +402 -0
  70. package/template/default/infra/kubernetes/database-secret.example.yaml +3 -3
  71. package/template/default/infra/kubernetes/deployment.yaml +5 -0
  72. package/template/default/infra/sdk-module-manifests.mjs +118 -0
  73. package/template/default/infra/vercel/README.md +262 -0
  74. package/template/default/infra/vercel/build.mjs +223 -0
  75. package/template/default/infra/vercel/handler.mjs +100 -0
  76. package/template/default/modules/example/migrations/0001_example_core.up.sql +2 -2
  77. package/template/default/modules/example/module.json +1 -1
  78. package/template/default/modules/example/package.json +3 -3
  79. package/template/default/modules/example/spec/module.yaml +1 -1
  80. package/template/default/modules/example/src/services/migration.ts +2 -2
  81. package/template/default/modules/example/tests/module.test.ts +1 -1
  82. package/template/default/package.json +2 -3
  83. package/template/default/platform/octane.config.ts +290 -156
  84. package/template/default/platform/package.json +5 -5
  85. package/template/default/platform/scripts/build.mjs +56 -0
  86. package/template/default/platform/scripts/dev.mjs +101 -18
  87. package/template/default/platform/src/generated/modules.server.ts +1 -0
  88. package/template/default/platform/src/server/database.ts +24 -0
  89. package/template/default/platform/src/server/lifecycle.ts +325 -0
  90. package/template/default/platform/src/server/runtime-role.ts +33 -0
  91. package/template/default/platform/src/server/setup/access.ts +160 -0
  92. package/template/default/platform/src/server/setup/adapters.ts +554 -0
  93. package/template/default/platform/src/server/setup/environment.ts +154 -0
  94. package/template/default/platform/src/server/setup/gate.ts +84 -0
  95. package/template/default/platform/src/server/setup/index.ts +181 -0
  96. package/template/default/platform/src/server/setup/modules.ts +119 -0
  97. package/template/default/platform/src/server/setup/page.ts +548 -0
  98. package/template/default/platform/src/server/setup/routes.ts +788 -0
  99. package/template/default/platform/src/server/setup/sanitize.ts +111 -0
  100. package/template/default/platform/src/server/setup/seed.ts +192 -0
  101. package/template/default/platform/src/server/setup/token.ts +79 -0
  102. package/template/default/platform/src/server/worker-tick.ts +193 -0
  103. package/template/default/platform/src/server/workspace-root.ts +16 -0
  104. package/template/default/render.yaml +70 -0
  105. package/template/default/vercel.json +5 -0
@@ -1,9 +1,8 @@
1
1
  ---
2
2
  name: workflow-development
3
3
  description: >-
4
- Build, publish, invoke, and test a workflows.core DAG through its typed graph
5
- and public execution capability without bypassing agent, action, tenant, or
6
- audit boundaries.
4
+ Author module-owned action templates, or build, publish, invoke, and test a
5
+ workflows.core DAG through its typed graph and public execution capability.
7
6
  ---
8
7
  # Build and integrate an agentic workflow
9
8
 
@@ -12,18 +11,22 @@ pinned agent revisions, deterministic gates, schema validators, registered
12
11
  module actions, data mappings, and terminal output. It does not own schedules or
13
12
  webhook secrets. Those remain optional concerns of `automations.core`.
14
13
 
15
- Read `docs/adr/0006-agentic-workflows.md`, the approved
16
- `modules/workflows/spec/module.yaml`, and the contracts in
17
- `modules/workflows/src/domain/types.ts` before changing a workflow surface.
14
+ At the repository root, read `docs/adr/0006-agentic-workflows.md`, the approved
15
+ `modules/workflows/spec/module.yaml`, and the owning public contracts before
16
+ changing a workflow surface. In the Sandbox, read the approved active-module
17
+ specification and the relevant public contract under `reference/sdk` when it is
18
+ installed. A missing public contract is a core blocker, not a reason to invent
19
+ one or search beyond the session workspace.
18
20
 
19
21
  ## Pick the correct extension point
20
22
 
21
23
  - A workflow definition belongs in `workflows.core` and is edited through its
22
24
  API or canvas. Do not hardcode a tenant workflow in source.
23
25
  - A business operation that a workflow may call is a versioned agent action.
24
- Register it through the agents action catalog. If missing, implement it in a
25
- separate `agent-tool-design` phase with permission, input, output, timeout,
26
- idempotency and audit tests before returning to workflow integration.
26
+ Register one ordinary `AgentTool` with `context.agentTools`. Optional
27
+ `workflowTemplate` metadata makes that action a named palette choice through
28
+ `agents.actions.v2`; it creates no second handler or node kind. Use the
29
+ authoring recipe below when the action is part of this task.
27
30
  - A business module that starts a workflow resolves
28
31
  `workflows.execution.v1` from `context.capabilities`. It never imports a
29
32
  workflow repository or database.
@@ -33,6 +36,87 @@ Read `docs/adr/0006-agentic-workflows.md`, the approved
33
36
  - If the workflow module is absent, the capability registry returns `null`.
34
37
  Hide an optional feature or return a clear stable refusal.
35
38
 
39
+ ## Author a module-owned workflow node template
40
+
41
+ An action-backed template is module source, not graph source. The business
42
+ manager first records its stable action id, permission, named inputs and
43
+ validation rules, output, effect, replay behavior, and success and refusal
44
+ scenarios in the owning module's specification. In a Sandbox session, the
45
+ operator must approve the hash of that exact spec before implementation. A
46
+ later edit requires renewed approval. If any of these decisions are absent,
47
+ hand the spec delta to `business-manager`; do not infer a field, permission,
48
+ external effect, or idempotency rule from the brief.
49
+
50
+ Work inside the existing role boundaries:
51
+
52
+ 1. `backend-engineer` owns the tenant-bound service, code-level validator,
53
+ endpoint or CLI target, and durable target ledger for a mutation. Validation
54
+ runs before any write and returns a stable bounded refusal. A repeated
55
+ idempotency key returns its first result; the same key with different input
56
+ conflicts. Ask backend to supply a missing service rather than writing in
57
+ `src/services/**` or `src/api/**` from the agentic role.
58
+ 2. `agentic-engineer` defines the tool under `src/agent/**`, registers it once
59
+ from `createServerComposition` in `src/platform.ts`, and adds behavioral
60
+ tests. `defineApiAgentTool` or `defineCliAgentTool` carries the existing
61
+ public target. `agents.core` exposes the registered action through
62
+ `agents.actions.v2`; the business module does not register that capability
63
+ or access the workflow database.
64
+ 3. Give the tool a stable dotted id, positive `contractVersion`, required
65
+ permission, bounded input and output JSON Schemas, `risk: 'read'` or
66
+ `'workspace-write'`, `idempotency: 'required'`, cancellation policy, and
67
+ timeout. A workspace write also needs
68
+ `idempotencyProtection: 'target-ledger'` backed by the service's real ledger.
69
+ The handler receives trusted tenant, actor, permission snapshot,
70
+ idempotency key, and `AbortSignal` from `AgentToolContext`; none comes from
71
+ graph input. Its output must satisfy its declared schema.
72
+ 4. Add static `workflowTemplate: { label, description, effect }` on that same
73
+ tool. The label is at most 80 characters, the description at most 240, and
74
+ effect is `local` or `connector-egress`. Metadata contains no code,
75
+ credential, or tenant value. Invalid or duplicate metadata must fail
76
+ composition with a safe diagnostic. A workflow-eligible tool without the
77
+ metadata remains in the generic action editor.
78
+
79
+ The selected template becomes a normal `action` node with input, success, and
80
+ failure ports. It pins the action id, contract version, schemas, permissions,
81
+ risk, idempotency protection, timeout, cancellation, and effect. Changing any
82
+ of those or the handler's behavior requires a new action identity or contract
83
+ version. The current registry keeps one version per action id, so retain the
84
+ old id and register a distinct id when published graphs must keep running;
85
+ label and description may change without rebinding a published graph.
86
+
87
+ Graph bindings may never supply a raw secret. `writeOnly` and
88
+ `x-flowdular-secret` apply at every schema depth, including array items and
89
+ alternatives. A marked field cannot carry `default`, `const`, `enum`,
90
+ `example`, or `examples` data. A template requiring
91
+ a raw secret input is ineligible. Accept a nonsecret opaque reference and let
92
+ the owning module resolve a credential from its tenant-bound vault or a
93
+ declared capability. Never put secret values in fixtures, graph definitions,
94
+ events, audit, or error text.
95
+
96
+ For `connector-egress`, the consumer module declares `connectors.core` and
97
+ `connectors.calls.v1`, uses `caller: 'workflow'`, checks the instance's
98
+ `allowWorkflows` consent, and passes the stable workflow side-effect key to
99
+ the connector. Credentials remain in `connectors.core`. A replay-stable
100
+ output may contain the recorded call id and outcome, not a response body the
101
+ connector replay does not return. Propagate `CALL_OUTCOME_UNKNOWN` as a
102
+ terminal failure when the remote mutation may have happened but no call was
103
+ recorded; never send a second request just to rebuild output. A separate
104
+ provider contract with remote idempotency or readback is required before
105
+ retrying an uncertain mutation.
106
+
107
+ Prove the module behavior at its public service or action boundary: valid
108
+ input, structural and business validation refusal before mutation, missing
109
+ permission, foreign tenant, same-key replay, different-input conflict,
110
+ cancellation, and recovery around the target commit. Connector actions also
111
+ prove absent consent and `CALL_OUTCOME_UNKNOWN` with a recorded fixture. Run a
112
+ deterministic workflow simulation on invented, reviewed fixtures for success,
113
+ failure, and refusal edges. Assert semantic attempts and edge outcomes, not
114
+ real timestamps; simulation must never invoke the handler, connector, or
115
+ network. Inspect the named template and safe run trail in Sandbox preview,
116
+ then run scoped gates, exact-source `auto-review`, and host eject. The agent
117
+ cannot approve the spec, install code into the running server, or push a
118
+ repository from the Sandbox.
119
+
36
120
  ## Graph contract
37
121
 
38
122
  Version one is a bounded DAG. The graph contains:
@@ -41,9 +125,11 @@ Version one is a bounded DAG. The graph contains:
41
125
  - `agent`: calls one exact immutable agent revision and validates structured
42
126
  output.
43
127
  - `agent-decision`: produces one schema-valid `pass` or `fail` outcome.
128
+ - `typed-decision`: routes one bounded typed answer through `pass` or `fail`.
44
129
  - `gate`: evaluates the versioned allowlisted logic language.
45
130
  - `validator`: validates an envelope against a pinned JSON schema.
46
131
  - `action`: calls one exact registered action contract version.
132
+ - `human-approval`: waits for an `approvals.core` request to resolve.
47
133
  - `merge`: waits for all declared incoming paths.
48
134
  - `output`: settles the workflow with a typed result.
49
135
 
@@ -20,3 +20,5 @@ The first setting is `auth.core.allowSignUp`. The server is authoritative and re
20
20
  - Administration: `GET /api/settings` (`system.settings.read`) lists every declaration with its current tenant value and metadata; `POST /api/settings/update` (`system.settings.manage`, session only) validates against the declaration, stores or clears the value, and appends `settings.updated` to the auth audit trail. Secrets are write-only: the API returns whether a value is set, never the value. Administration > Modules renders the selected module's declarations in its Settings drawer section. Administration > Settings contains only workspace and organization settings.
21
21
  - `emailConfirmation` cannot be enabled while no mail transport is composed; the API refuses with `MAIL_TRANSPORT_REQUIRED` and the screen shows the setting as locked.
22
22
  - The cross-module read rule (declared dependency, shared, non-secret) is not enforced by `get`; it is a review rule until a requester-aware read exists.
23
+ - Platform-scoped writes (amendment, 2026-10-06): every workspace owner holds `system.settings.manage`, so `POST /api/settings/update` changes a platform-scoped setting only for a principal of the operator workspace, the tenant id the deployment sets in `FD_OPERATOR_TENANT`, and refuses anyone else with 403 `PLATFORM_SETTING_OPERATOR_ONLY`; `GET /api/settings` locks those rows outside it. Unset, no workspace changes one.
24
+ - Recorded operator workspace (amendment, 2026-10-06, 0.6.1): auth.core records the operator workspace in `auth_operator_workspace`, at most one row under forced row-level security bound to the workspace it names. Any path that creates the first workspace of an empty database records it in the same transaction, a deployment with exactly one workspace and no record records it when auth.core opens the database, and `pnpm flowdular auth operator-set` is the only way to change it. `FD_OPERATOR_TENANT` becomes an override: set, it alone decides, and a value that is not a workspace id leaves no operator. system.core resolves the operator on every settings request; while none is known every platform row is locked with `system.settings.platformOperatorUnset` and the write refusal keeps `PLATFORM_SETTING_OPERATOR_ONLY`. This supersedes "unset, no workspace changes one" above.
@@ -266,6 +266,39 @@ existing bindings in one transaction:
266
266
  - a definition absent from the sealed registry is unavailable for new work;
267
267
  retained revisions and historical runs remain untouched.
268
268
 
269
+ Amendment (2026-10-05, `agents.core` 0.15.2): reconciliation no longer runs at
270
+ runtime start, and a lower revision no longer fails boot.
271
+
272
+ - `agents.core` opens once in every role, before its first request, capability
273
+ call or `startWorker()`. Opening runs its migrations, then reconciles the
274
+ registered definitions into the durable catalogue. When every definition
275
+ matches its stored revision and content hash, opening costs one catalogue read
276
+ and writes nothing. Otherwise it takes the advisory lock
277
+ `agents.core.module-catalog`, reads the catalogue again under it and writes
278
+ only what is still missing or behind, so roles that open together never fail
279
+ with a duplicate key. A failed opening fails only the request or worker start
280
+ that triggered it, and the next one opens again.
281
+ - Opening never reads or writes a tenant binding. A request that lists, reads or
282
+ runs a module agent advances that tenant's stale binding first, inside its own
283
+ tenant transaction, under the binding row lock and a share lock on the
284
+ catalogue row, so concurrent requests advance it once. Configuring a binding
285
+ writes it at the served revision under the same locks. After its execution
286
+ workers start, the worker advances the bindings no request has touched, in
287
+ pages of at most 100 tenants, and logs a failing tenant without tenant data.
288
+ That pass never gates worker readiness.
289
+ - Tenant-created definitions saved before the retained revision ledger are
290
+ adopted once. A request adopts its own tenant's owed definitions in its own
291
+ transaction, and the worker adopts the rest in pages after it starts.
292
+ - An instance that registers a lower revision than the catalogue holds, such as
293
+ an older deployment during a rolling deploy, is superseded for that agent and
294
+ keeps running. It writes nothing for the agent, lists it unavailable with
295
+ `MODULE_AGENT_REVISION_SUPERSEDED`, refuses a binding change or a run of it
296
+ with status 409 and logs a warning once per process and agent. Queued runs and
297
+ workflow references pinned to a retained executable revision keep the
298
+ exact-revision contract.
299
+ - The same revision with different content is still refused, by preparation and
300
+ by the opening, as `MODULE_AGENT_REVISION_DRIFT`.
301
+
269
302
  An old retained revision remains executable after a newer module definition is
270
303
  registered only while the owning module is still present, the selected provider
271
304
  is usable, and every required registered tool still exists. Module removal is
@@ -327,7 +360,8 @@ not change shape.
327
360
  - Business-agent definition content is immutable within one definition
328
361
  revision.
329
362
  - Duplicate ids, late registration, revision downgrade and same-revision content
330
- drift fail platform boot before workers start.
363
+ drift fail platform boot before workers start. Since the 2026-10-05
364
+ amendment, a revision downgrade supersedes the agent instead.
331
365
  - Module behavior cannot be edited or deleted through tenant APIs.
332
366
  - Tenant bindings cannot enable a tool outside the code allowlist.
333
367
  - Every execution is tenant-scoped and uses the initiating Actor and trusted
@@ -14,11 +14,11 @@ Use the already selected task skill. Consult `.ai/references/catalog` for implem
14
14
  3. Identifiers: module, permission and capability ids match `^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$`. `inventory.core` lives in `modules/inventory` as `@flowdular/module-inventory`; permissions are `<module>.<entity>.read` and `<module>.<entity>.manage`.
15
15
  4. A module is created only from a spec with `status: approved`. An agent never initiates, infers, or grants approval from its own judgment. After a current, explicit user instruction naming the spec, a host coding agent may record that decision mechanically by following `spec-approval`; a sandbox specialist can only request approval, and the operator route records the exact approved hash. Spec `permissions[].id` equal the constants in `src/acl/permissions.ts`: the spec is what `auth sync-scopes` grants.
16
16
  5. Every endpoint is `defineEndpoint` from `@flowdular/sdk/server` with `access: { kind: 'permission', permission }` and `resolveIdentity: endpointIdentityFromContext`. Deny by default; a public endpoint needs a written reason. No raw `new ServerRoute` outside `modules/auth`.
17
- 6. The tenant id comes only from `principalFromContext(octane)!.tenantId`, never from the body, query or headers. Every query on a tenant-owned table filters by `tenant_id`; unique constraints and indexes start with `tenant_id`; SQL uses bound parameters. PostgreSQL tenant tables also enable and force row-level security with `USING` and `WITH CHECK` policies bound to transaction-local `coreloom.tenant_id`. Every repository operation runs through `database.transaction(..., { tenantId, access })`; the runtime role is never a superuser and never has `BYPASSRLS`, while a separate migration lease may own DDL. `WHERE tenant_id = ...` remains defense in depth.
17
+ 6. The tenant id comes only from `principalFromContext(octane)!.tenantId`, never from the body, query or headers. Every query on a tenant-owned table filters by `tenant_id`; unique constraints and indexes start with `tenant_id`; SQL uses bound parameters. PostgreSQL tenant tables also enable and force row-level security with `USING` and `WITH CHECK` policies bound to transaction-local `flowdular.tenant_id`. Every repository operation runs through `database.transaction(..., { tenantId, access })`; the runtime role is never a superuser and never has `BYPASSRLS`, while a separate migration lease may own DDL. `WHERE tenant_id = ...` remains defense in depth.
18
18
  7. Every mutation calls `sessionMutationDenial(octane, auth)` first and reads its body with `readJsonObject` plus `requiredString`, `optionalString`, `requiredInteger`. Clients send `content-type: application/json`, `x-csrf-token`, and `credentials: 'same-origin'`.
19
19
  8. Routes mount only through `src/platform.ts` exporting `createServerComposition(context)` with `platform.server: true` in `module.json` and a `./platform` export in `package.json`; the context carries `auth`, `settings`, `agentTools`, `agentDefinitions`, `capabilities` and `databases`. A database module passes `context.databases` into one runtime, which acquires and releases one provider lease lazily; `prepare` stays read-only and never opens a database. A composition may return `settings`, read-only `prepare`, `start`, background-work `stop`, and final `dispose`. Client contributions mount only through `createClientContribution(context)` in `src/client/index.ts` with `platform.client: true`. `pnpm flowdular module validate` fails on a missing entry (`PLATFORM_*`).
20
20
  9. Never edit the composition by hand: `platform/octane.config.ts`, `platform/src/App.tsrx`, `platform/src/generated/**`, `platform/package.json` dependencies and `modules.enabled` in `flowdular.json` are written by `pnpm flowdular module enable <id> --apply` and `pnpm flowdular module sync --apply`.
21
- 10. `pnpm flowdular module enable <id> --apply` grants the spec permissions to every tenant owner as its last step; `pnpm flowdular auth sync-scopes --module <id> --apply` re-grants later (new permission, another database). Members receive scopes only through `MEMBER_SCOPES` in `modules/auth/src/acl/scopes.ts`, a core change.
21
+ 10. `pnpm flowdular module enable <id> --apply` grants the spec permissions to every tenant owner as its last step; first-run setup grants every enabled module's permissions to the owner it creates, so a module enabled before the first workspace needs no extra step; `pnpm flowdular auth sync-scopes --module <id> --apply` re-grants later (new permission, another database). Members receive scopes only through `MEMBER_SCOPES` in `modules/auth/src/acl/scopes.ts`, a core change.
22
22
  11. Declare every imported package in the module `package.json`; the sandbox `dependencies` gate and the eject fail otherwise. Every relative import carries its `.ts` or `.tsrx` extension.
23
23
  12. Use the CLI for discovery, validation and scaffolding: `doctor`, `spec validate`, `module validate`, `module new`, `module enable`, `auth sync-scopes`. Run destructive, external or production capabilities only with what the runner demands (`--apply`, `--confirm`, `--spec`) and never work around a refusal.
24
24
  13. Gates are `spec-schema`, `module-schema`, `dependencies`, `typecheck`, `tests`, `format`. The sandbox runs `dependencies` plus the ones your role lists after every turn, per draft module, and feeds a failure back to you; with a shell you may run the module's own gate commands yourself, never installs, network or git. From a checkout run them yourself and `pnpm verify` before any pull request.
@@ -26,7 +26,7 @@ Use the already selected task skill. Consult `.ai/references/catalog` for implem
26
26
  15. One screen, form, table or stateful region per named component. Records own the page; create and edit happen in a `Drawer`. Every screen shows loading, empty, error, populated and denied.
27
27
  16. Tests live in `tests/*.test.ts`: identity, tenant isolation, uniqueness, one 401 and one 403 per endpoint, each validation bound. Repository behavior uses `createTestDatabaseProvider()` from `@flowdular/sdk/database-testing`: PGlite locally and isolated server PostgreSQL in CI. Open one provider per test file, migrate once, and truncate module tables between cases (`modules/profile/tests/support/database.ts`). Tenancy tests use two tenants, prove `TENANT_CONTEXT_REQUIRED` without a transaction tenant id, prove RLS prevents cross-tenant reads and writes under the non-bypass runtime role, and cover `WITH CHECK`. Tenant fixture work also supplies tenant context on a migration connection. The sandbox `tests` gate passes with zero tests, so an empty suite is a defect.
28
28
  17. Translations are live. Every module contribution registers all declared `translations/*.json` bundles, user-facing copy uses fully qualified `t('<module>.<key>')` keys, navigation labels are lazy getters, locale-aware formatting uses `activeLocale()`, and every locale has the same key set, where a plural family (`<key>.one`/`.other`, plus `.few`/`.many` in `pl`, read as `t(key, { count })`) counts as one key. `module validate` rejects missing files, key drift, and missing static translation keys.
29
- 18. Numbered migration SQL is immutable source. `migrations/000N_<module>_<name>.up.sql` and `.down.sql` are PostgreSQL and the only schema source; there is no dialect subdirectory. `src/services/migration.ts` mirrors every `.up.sql` byte for byte as `databaseMigrations: readonly DatabaseMigration[]` with `sql: { postgresql: ... }`, and the runtime calls `runDatabaseMigrations` through its provider lease. The namespaced `_coreloom_migrations_v2` ledger records the checksum, adopts only an explicit complete `inspectExisting` result (`postgresTenantTableState` is the standard check), and refuses drift, duplicates, or partial schema. Add a new numbered, additive migration instead of changing existing bytes. A tenant table's migration includes enabled and forced RLS plus a tenant policy, with the policy behavior covered by tests.
29
+ 18. Numbered migration SQL is immutable source. `migrations/000N_<module>_<name>.up.sql` and `.down.sql` are PostgreSQL and the only schema source; there is no dialect subdirectory. `src/services/migration.ts` mirrors every `.up.sql` byte for byte as `databaseMigrations: readonly DatabaseMigration[]` with `sql: { postgresql: ... }`, and the runtime calls `runDatabaseMigrations` through its provider lease. The namespaced `_flowdular_migrations_v2` ledger records the checksum, adopts only an explicit complete `inspectExisting` result (`postgresTenantTableState` is the standard check), and refuses drift, duplicates, or partial schema. Add a new numbered, additive migration instead of changing existing bytes. A tenant table's migration includes enabled and forced RLS plus a tenant policy, with the policy behavior covered by tests.
30
30
  19. Module CLI commands live in `src/cli/commands.json` and `src/cli/index.ts`, metadata-identical, inside the module namespace.
31
31
  20. Passwords, session tokens and provider credentials never leave `auth.core` (or the `agents.core` vault) and never appear in logs, audit metadata or responses.
32
32
  21. `setup quick` and `auth greenfield` are destructive local resets. Preview first, stop the app, never point them at a custom or deployed database.
@@ -77,6 +77,7 @@ The CLI imports this code only after the exact command or capability is invoked.
77
77
  - External and non-local destructive module capabilities remain disabled until a signed approval verifier is configured.
78
78
  - A workspace-local destructive capability must declare `localOnly` and a typed confirmation token. It remains dry-run unless both `--apply` and the exact `--confirm` value are present, and it is blocked outside development and test.
79
79
  - `capability list`, `capability describe`, and `capability run` use the same descriptors and handlers as direct commands.
80
+ - A command refuses by throwing. An error with a stable `code` (`^[A-Z][A-Z0-9_]{0,63}$`) and a numeric HTTP `status`, the shape of every module service error, keeps its code in the envelope and the human output. So does a coded error from a platform service the runner hands the command, such as a `DatabaseError` from `context.databases` or a refused local database. Any other error is reported as `COMMAND_FAILED`. Only the code and the message are printed, never the stack, the cause or other fields.
80
81
  - Core commands may reuse an extension: `module enable <id> --apply` runs the `auth.scopes.sync` capability of `auth.core` after regenerating the composition, so a freshly enabled module is visible to workspace owners without a second command.
81
82
 
82
83
  The complete customer example is in `.ai/examples/customer-cli-extension`. New module scaffolds include the catalog and implementation files when the approved spec declares the `cli` capability.
@@ -47,8 +47,29 @@ flowdular database restore --input <dir> --apply --confirm restore-database
47
47
  flowdular setup check # alias of doctor
48
48
  flowdular setup quick [--apply --confirm reset-local-auth]
49
49
  flowdular setup migrate-state [--apply --confirm migrate-legacy-state]
50
+ flowdular deploy targets # runtime support and launch modes
51
+ flowdular deploy plan <target> [--json] # read-only provider preflight
52
+ flowdular deploy start docker [--apply] # local Compose launch; without --apply returns plan
53
+ flowdular deploy start vercel [--apply] # provision and deploy to Vercel Production; without --apply returns plan
50
54
  ```
51
55
 
56
+ `deploy start docker --apply` prints a one-time setup token, so it refuses
57
+ `--json` and redirected output and must run in a private interactive terminal. The deployment targets are
58
+ documented in [infra/README.md](../infra/README.md). `deploy plan vercel`
59
+ checks the build source and links to Vercel import when the branch is pushed.
60
+
61
+ `deploy start vercel --apply` drives the signed-in Vercel CLI: it provisions
62
+ Neon PostgreSQL and its roles, writes the stable keys to
63
+ `.flowdular/deploy/vercel-<project id>.env` (mode 0600) before uploading them
64
+ through stdin, connects a private Blob store and deploys to Production. While
65
+ the database has no workspace it prints a one-time token for `/setup`, where the
66
+ first workspace and owner are created in the browser; Vercel holds only the
67
+ token's SHA-256. That gives it the same terminal rules as the Docker launch.
68
+ `--database-url-env NAME` takes the owner URL from a variable instead of Neon;
69
+ `--plan hobby|pro` overrides the plan read from `vercel whoami --json`;
70
+ `--project`, `--scope`, `--origin` and `--cron` are optional. A rerun resumes
71
+ without regenerating keys. See [infra/vercel/README.md](../infra/vercel/README.md).
72
+
52
73
  ### Authoring a migration
53
74
 
54
75
  `migration new` writes exactly two files,
@@ -59,7 +80,7 @@ already filled in. Replace the placeholder columns with the real schema.
59
80
  `migration verify` then checks that every applied ledger checksum still matches,
60
81
  that every tenant table a migration leaves behind has `ENABLE ROW LEVEL
61
82
  SECURITY`, `FORCE ROW LEVEL SECURITY` and a tenant policy declared after the
62
- last statement that puts the table in place, that a `coreloom_background` policy
83
+ last statement that puts the table in place, that a `flowdular_background` policy
63
84
  grants no more than `FOR SELECT`, and that every `migrations/*.up.sql` file has
64
85
  a matching id in `databaseMigrations` and the other way round.
65
86
 
@@ -113,6 +134,8 @@ flowdular auth sync-scopes --module <id> [--apply] # re-grant a module's scopes
113
134
  flowdular auth workspaces [--limit <n>] # workspaces of this deployment and their owners
114
135
  flowdular auth workspace-create --name <name> --owner-email <email> --owner-name <name> [--slug <id>] [--password-env <VAR>] [--actor <label>] [--apply]
115
136
  flowdular auth member-add --workspace <slug|id> --email <email> [--role <key>] [--actor <label>] [--apply]
137
+ flowdular auth operator # the recorded operator workspace and whether FD_OPERATOR_TENANT overrides it
138
+ flowdular auth operator-set <id|slug> [--actor <label>] [--apply]
116
139
  flowdular auth secrets-rotate [--apply] # re-seal enrolled TOTP secrets with the current MFA key
117
140
  flowdular auth greenfield # destructive local auth reset (setup quick)
118
141
 
@@ -176,6 +199,20 @@ Both commands append an audit row to the workspace trail whose actor is
176
199
  what exists, with each workspace's owners, so the slug or id for the other
177
200
  commands is at hand.
178
201
 
202
+ The first workspace of an empty database is recorded as the operator
203
+ workspace, the one whose owners change the branding and the other platform
204
+ settings. `auth operator` shows the record and says whether
205
+ `FD_OPERATOR_TENANT` is set in that shell, which overrides the record wherever
206
+ the deployment sets it. `auth operator-set` names another workspace, previews
207
+ without `--apply`, and with it releases the current operator and records the
208
+ new one, each with an audit row in that workspace's trail. If it fails between
209
+ the two, no workspace is the operator until the same command runs again.
210
+
211
+ ```bash
212
+ flowdular auth operator
213
+ flowdular auth operator-set northwind --apply
214
+ ```
215
+
179
216
  ## Workspace scripts
180
217
 
181
218
  ```bash
@@ -198,6 +235,6 @@ per-package seconds in `scripts/test-weights.json`. A new package without a
198
235
  weight counts as 30 seconds; refresh the file from a CI run when the shards
199
236
  drift apart.
200
237
 
201
- ## Official module distribution
238
+ ## Module Studio distribution
202
239
 
203
- `module search`, `module info`, `module install`, `module update`, `module recover`, and `module validate --locked` manage reviewed external source. See [the distribution contract](module-distribution.md) for flags, trust, activation and recovery.
240
+ `module source`, `module search`, `module info`, `module plan`, `module apply`, `module recover`, and `module validate --locked` manage reviewed external source. See [Module Studio](module-distribution.md) for flags, trust, activation and recovery.
@@ -11,18 +11,56 @@ deployments must set the secret keys.
11
11
  | `FD_ENV` | `NODE_ENV`, else `development` | Environment the CLI and destructive guards check |
12
12
  | `FD_PORT` | `3000` | Host port published by the container |
13
13
  | `FD_TRUST_PROXY` | `false` | Trust `X-Forwarded-*` behind a reverse proxy |
14
+ | `FD_RUNTIME_ROLE` | `combined` | `combined` serves HTTP and runs module workers; `web` or `tick`, see below |
14
15
  | `FD_CSP` | built-in policy | Override the Content Security Policy |
15
16
  | `FD_CSP_REPORT_ONLY` | `true` outside production | Report CSP violations instead of enforcing them |
16
17
  | `FD_LOG_FORMAT` | `json` in production, else `text` | `json` (one object per line) or `text`; see [operations.md](operations.md) |
17
18
  | `FD_LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error` |
18
19
  | `FD_METRICS` | `false` | Expose `GET /api/metrics`; see [operations.md](operations.md) |
19
20
  | `FD_METRICS_TOKEN` | none | Bearer token a metrics scrape must present |
21
+ | `FD_OPERATOR_TENANT` | none | Tenant id that overrides the recorded operator workspace |
22
+
23
+ A platform-scoped module setting has one value for every workspace: the
24
+ branding, sign-up and session policy, the mail relay and the platform settings
25
+ of other modules. Only the operator workspace changes them: a principal there
26
+ holding `system.settings.manage` edits them in Administration. Every other
27
+ workspace sees them read-only, and a write from it is refused with 403
28
+ `PLATFORM_SETTING_OPERATOR_ONLY`.
29
+
30
+ auth.core records the operator workspace. First-run setup records the workspace
31
+ it creates, in the transaction that creates it, and so does any other path that
32
+ creates the first workspace of an empty database (`auth workspace-create`,
33
+ `sandbox provision`, `setup quick`). `pnpm flowdular auth operator` shows the
34
+ record and `pnpm flowdular auth operator-set <id|slug> --apply` moves it to
35
+ another workspace; the next request sees the change, with no restart.
36
+
37
+ `FD_OPERATOR_TENANT` is an override. Set, the workspace whose tenant id it names
38
+ is the operator and the record is not consulted; a value that is not the id of
39
+ an existing workspace leaves no operator rather than falling back to the record.
40
+ Leave it empty to use the record.
41
+
42
+ A deployment upgraded from 0.6.0 with exactly one workspace records that
43
+ workspace the first time it starts. With two or more nothing is recorded, every
44
+ platform row is locked with a reason that names `auth operator-set`, and the
45
+ stored values keep applying until the operator runs that command or sets the
46
+ variable.
47
+
48
+ A `web` process serves HTTP only: it never starts a module worker and never
49
+ claims queued work from a request, so a deployment of `web` processes also needs
50
+ a `combined` or a `tick` process on the same database, object storage and keys.
51
+ A `tick` process runs the module workers only inside a tick request that
52
+ presents `FD_WORKER_TICK_SECRET` (at least 32 characters) as a bearer token, for
53
+ at most `FD_WORKER_TICK_WINDOW_MS` (default 50000), and drains them before it
54
+ answers.
55
+ A Vercel deployment works this way; see
56
+ [infra/vercel/README.md](../infra/vercel/README.md).
20
57
 
21
58
  ## Branding
22
59
 
23
60
  The name, the document title, the description, the link preview image, the
24
61
  browser icon, the theme colour and the logo are not environment variables: they
25
- are `system.core` settings an owner with `system.settings.manage` changes under
62
+ are platform-scoped `system.core` settings a principal with
63
+ `system.settings.manage` in the operator workspace changes under
26
64
  Administration, Branding, and every change is audited. One value serves the
27
65
  whole deployment, so the sign-in screen and a shared link carry it too, and a
28
66
  setting nobody changed renders the product's own.
@@ -289,6 +327,7 @@ message was not delivered.
289
327
  | `FD_AGENT_RUN_GRANT_KEY` | generated dev key | Base64 32-byte key signing run grants |
290
328
  | `FD_AGENT_WORKER_CONCURRENCY` | `2` (1 to 16) | Parallel run workers |
291
329
  | `FD_AGENT_WORKER_LEASE_MS` | `30000` | Run lease before recovery reclaims it |
330
+ | `FD_AGENT_WORKER_DRAIN_MS` | `0` (0 to 720000) | Time a stopping worker lets claimed runs finish |
292
331
  | `FD_AGENT_PROVIDER_HOST_ALLOWLIST` | empty | Hostnames an external provider may be called on |
293
332
 
294
333
  Outside production the keys are generated once under `.flowdular/data`. The
@@ -610,9 +649,10 @@ deployment on the old names keeps working and the server logs one
610
649
  replacement. The platform name wins when both are set, and every refusal names
611
650
  the variable the deployment actually set.
612
651
 
613
- The relay is also five platform-scoped `auth.core` settings, edited under
614
- Administration, Modules: `mailTransport` (`environment`, `none` or `smtp`),
615
- `mailSmtpUrl` (secret, write only), `mailFrom`, `mailRequireTls` and
652
+ The relay is also five platform-scoped `auth.core` settings, edited from the
653
+ operator workspace under Administration, Modules: `mailTransport`
654
+ (`environment`, `none` or `smtp`), `mailSmtpUrl` (secret, write only),
655
+ `mailFrom`, `mailRequireTls` and
616
656
  `mailRejectUnauthorized`. `mailTransport` decides which source wins. It is
617
657
  `environment` by default, and while it stays there the `FD_MAIL_*`
618
658
  configuration above is in effect exactly as described, deprecation warnings
@@ -658,7 +698,7 @@ an object moved into another tenant's prefix does not open.
658
698
 
659
699
  | Variable | Default | Purpose |
660
700
  | ------------------------------------ | ----------------------------------- | ---------------------------------------------------------------------------- |
661
- | `FD_STORAGE_ADAPTER` | `s3` in production, else `local` | `local` or `s3`; `local` is refused in production |
701
+ | `FD_STORAGE_ADAPTER` | `s3` in production, else `local` | `local`, `s3` or `vercel-blob`; `local` is refused in production |
662
702
  | `FD_STORAGE_LOCAL_DIRECTORY` | `.flowdular/data/storage` | Object directory of the local adapter |
663
703
  | `FD_STORAGE_S3_BUCKET` | none | Bucket name; required by the S3 adapter |
664
704
  | `FD_STORAGE_S3_REGION` | none | Signing region; required by the S3 adapter |
@@ -666,10 +706,23 @@ an object moved into another tenant's prefix does not open.
666
706
  | `FD_STORAGE_S3_ACCESS_KEY_ID` | none | Access key id; required by the S3 adapter |
667
707
  | `FD_STORAGE_S3_SECRET_ACCESS_KEY` | none | Secret access key; required by the S3 adapter |
668
708
  | `FD_STORAGE_S3_FORCE_PATH_STYLE` | `false` | `<endpoint>/<bucket>/<key>` instead of a bucket subdomain |
709
+ | `BLOB_STORE_ID` | set by Vercel | Blob store of the `vercel-blob` adapter, authenticated with Vercel OIDC |
710
+ | `BLOB_READ_WRITE_TOKEN` | none | Blob read-write token for the `vercel-blob` adapter outside Vercel |
669
711
  | `FD_STORAGE_MAX_OBJECT_BYTES` | `26214400` (25 MiB) | Per-object limit, 1024 to 268435456; a stream is cut off at it |
670
712
  | `FD_STORAGE_ENCRYPTION_KEY` | derived dev key | Base64 32-byte key sealing every object and read URL; required in production |
671
713
  | `FD_STORAGE_ENCRYPTION_KEY_PREVIOUS` | empty | Retired object keys, comma separated, read only |
672
714
 
715
+ `vercel-blob` keeps the encrypted objects in a private Vercel Blob store, so a
716
+ Vercel deployment needs no separate bucket. Connecting a Blob store to the
717
+ Vercel project sets `BLOB_STORE_ID`, and the SDK authenticates with the
718
+ deployment's OIDC token, so nothing else is configured there. Outside Vercel,
719
+ set `BLOB_READ_WRITE_TOKEN`. The platform refuses to start when neither is
720
+ present. Reads bypass the Blob cache, so a re-sealed or deleted object is never
721
+ served from an older copy. A Vercel Function accepts at most 4.5 MB of request
722
+ or response body, so set `FD_STORAGE_MAX_OBJECT_BYTES` to at most `4194304`
723
+ there. Lowering the limit makes an existing larger object unreadable through
724
+ this adapter, and a key rotation leaves it sealed under its old key.
725
+
673
726
  A module writes through `context.storage` and never sees an adapter, a bucket or
674
727
  a path. Only these content types are stored, and the bytes are verified against
675
728
  the declared type before the write: PDF, PNG, JPEG, GIF, WebP, plain text, CSV,
@@ -746,7 +799,9 @@ acquires a lease from the platform provider configured above, so the
746
799
  openssl rand -base64 32
747
800
  ```
748
801
 
749
- For containers, copy `infra/docker/.env.example` to `infra/docker/.env` and fill
750
- in every empty value: every encryption key above, including the connectors
751
- and audit anchor keys, the object store settings and the four PostgreSQL role
752
- passwords. See [../infra/README.md](../infra/README.md).
802
+ For a local Docker installation, run `node infra/docker/start.mjs`. It creates
803
+ `infra/docker/.env` with missing secrets, starts PostgreSQL and the bundled
804
+ object store, then opens the first-run web setup. Keep that file with database
805
+ and object-store backups; changing its keys or passwords later does not rotate
806
+ existing data or PostgreSQL roles. Other deployments supply these values through
807
+ their own secret manager. See [../infra/README.md](../infra/README.md).
@@ -32,7 +32,12 @@ const databases = createDatabaseProvider(config, {
32
32
  });
33
33
  ```
34
34
 
35
- `@flowdular/sdk/database` owns no driver, so the caller supplies both. Composition
35
+ `@flowdular/sdk/database` owns no driver, so the caller supplies both. The provider
36
+ listens for the `error` events node-postgres emits on a pool and on a leased
37
+ client, so a connection the server closes (a suspended compute, a failover, a
38
+ restart) costs at most the query or transaction it interrupts, never the
39
+ process; a factory
40
+ needs no listener of its own. Composition
36
41
  injects the result as `PlatformServerContext.databases`, and that is the only
37
42
  way a module reaches storage. `GET /api/ready` reports the live adapter and
38
43
  answers 503 while the database is unreachable.
@@ -72,18 +77,21 @@ maximum.
72
77
 
73
78
  With the `pglite` adapter the data directory is the only setting that applies.
74
79
  Under `NODE_ENV=test` the directory is ignored and the database is held in
75
- memory.
80
+ memory. A directory is open once per process: every provider on it in that
81
+ process shares the one embedded database (a development server composes more
82
+ than one), and another process is refused with `LOCAL_DATABASE_LOCKED` until
83
+ the last of them closes it.
76
84
 
77
85
  ## Three roles
78
86
 
79
87
  The embedded adapter creates the same roles a deployment configures, so a local
80
88
  run enforces the isolation a deployment enforces instead of approximating it.
81
89
 
82
- | Role | Owns | Constraints |
83
- | --------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------- |
84
- | `coreloom_migrator` | The schema. Serves the `migration` purpose. | Owns every table the migrations create. |
85
- | `coreloom_runtime` | Request-time reads and writes. | No `SUPERUSER`, no `BYPASSRLS`, and every handle it lends requires a transaction tenant id. |
86
- | `coreloom_background` | Cross-tenant polls. | Read-only, and no blanket table grant. |
90
+ | Role | Owns | Constraints |
91
+ | ---------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------- |
92
+ | `flowdular_migrator` | The schema. Serves the `migration` purpose. | Owns every table the migrations create. |
93
+ | `flowdular_runtime` | Request-time reads and writes. | No `SUPERUSER`, no `BYPASSRLS`, and every handle it lends requires a transaction tenant id. |
94
+ | `flowdular_background` | Cross-tenant polls. | Read-only, and no blanket table grant. |
87
95
 
88
96
  ## Leases
89
97
 
@@ -100,13 +108,13 @@ const lease = await context.databases.acquire({
100
108
  });
101
109
  ```
102
110
 
103
- | Purpose | Role | Used for |
104
- | ------------ | --------------------- | --------------------------------------------------- |
105
- | `migration` | `coreloom_migrator` | Applying migrations and resetting a database |
106
- | `runtime` | `coreloom_runtime` | The deployed application |
107
- | `preview` | `coreloom_runtime` | A local run and the sandbox preview |
108
- | `test` | `coreloom_runtime` | A test suite |
109
- | `background` | `coreloom_background` | A scheduler poll or recovery that precedes a tenant |
111
+ | Purpose | Role | Used for |
112
+ | ------------ | ---------------------- | --------------------------------------------------- |
113
+ | `migration` | `flowdular_migrator` | Applying migrations and resetting a database |
114
+ | `runtime` | `flowdular_runtime` | The deployed application |
115
+ | `preview` | `flowdular_runtime` | A local run and the sandbox preview |
116
+ | `test` | `flowdular_runtime` | A test suite |
117
+ | `background` | `flowdular_background` | A scheduler poll or recovery that precedes a tenant |
110
118
 
111
119
  A runtime acquires its leases lazily, one per runtime, and releases them from
112
120
  composition `dispose()`. `modules/profile/src/server/runtime.ts` is the shape:
@@ -164,14 +172,14 @@ codes.
164
172
 
165
173
  Every tenant table enables and forces row-level security and carries a policy
166
174
  whose `USING` and `WITH CHECK` compare `tenant_id` with the transaction-local
167
- `coreloom.tenant_id` setting:
175
+ `flowdular.tenant_id` setting:
168
176
 
169
177
  ```sql
170
178
  ALTER TABLE profile_records ENABLE ROW LEVEL SECURITY;
171
179
  ALTER TABLE profile_records FORCE ROW LEVEL SECURITY;
172
180
  CREATE POLICY profile_records_tenant_policy ON profile_records
173
- USING (tenant_id = current_setting('coreloom.tenant_id', true))
174
- WITH CHECK (tenant_id = current_setting('coreloom.tenant_id', true));
181
+ USING (tenant_id = current_setting('flowdular.tenant_id', true))
182
+ WITH CHECK (tenant_id = current_setting('flowdular.tenant_id', true));
175
183
  ```
176
184
 
177
185
  `database.transaction(body, { tenantId, access })` sets that value for the
@@ -201,7 +209,7 @@ repository.
201
209
 
202
210
  ## Migrations
203
211
 
204
- The ledger is `_coreloom_migrations_v2`. It carries the module namespace because
212
+ The ledger is `_flowdular_migrations_v2`. It carries the module namespace because
205
213
  every module shares one database. Checksums cover the exact SQL, and a mismatch
206
214
  is checked before any outstanding migration runs.
207
215
 
@@ -257,7 +265,7 @@ schema.
257
265
  `migration verify` checks that every applied ledger checksum still matches, that
258
266
  every tenant table a migration leaves behind has `ENABLE ROW LEVEL SECURITY`,
259
267
  `FORCE ROW LEVEL SECURITY` and a tenant policy declared after the last statement
260
- that puts the table in place, that a `coreloom_background` policy grants no more
268
+ that puts the table in place, that a `flowdular_background` policy grants no more
261
269
  than `FOR SELECT`, and that every `migrations/*.up.sql` file has a matching id in
262
270
  `databaseMigrations` and the other way round.
263
271
 
@@ -272,10 +280,10 @@ A table it may poll says so itself, in its own migration:
272
280
 
273
281
  ```sql
274
282
  CREATE POLICY <table>_background_policy ON <table>
275
- FOR SELECT TO coreloom_background
283
+ FOR SELECT TO flowdular_background
276
284
  USING (<the narrowest predicate that still finds the work>);
277
- REVOKE SELECT ON <table> FROM coreloom_background;
278
- GRANT SELECT (<only the columns the poll reads>) ON <table> TO coreloom_background;
285
+ REVOKE SELECT ON <table> FROM flowdular_background;
286
+ GRANT SELECT (<only the columns the poll reads>) ON <table> TO flowdular_background;
279
287
  ```
280
288
 
281
289
  A table that forgets to is invisible to that role, and every column outside the
@@ -233,7 +233,10 @@ or than the narrowest table beside it; more than two actions always sit in the
233
233
  More menu, and the column then keeps the width of that one button. The menu
234
234
  opens over the page, arrows, Home and End move between items, Enter or Space
235
235
  runs one, and Escape or Tab closes it and returns focus to the button. A refused
236
- action stays in the menu, announced as unavailable, with its `reason`.
236
+ action stays in the menu, announced as unavailable, with its `reason`. The
237
+ action column stays pinned to the right edge while the table scrolls, except a
238
+ single action, which never folds: in a card narrower than three times its
239
+ column it scrolls with the row instead of covering the cells beside it.
237
240
 
238
241
  **Clickable rows.** A table with `onSelect` makes its first cell a button, so
239
242
  Tab reaches the row and Enter or Space opens it. That column holds text, never
@@ -270,7 +273,7 @@ them; outside the shell the English defaults and the host locale apply.
270
273
  | `Tabs` | Accessible tablist: required `id`, `items` (`id`, `label`, `disabled`), `active`, `onChange`, `label`; arrows, Home and End move roving focus, an `active` that names no enabled tab selects the first enabled one, and the caller renders the panel |
271
274
  | `SortableList` | Reorderable list: required `id`, `label`, `items` (`id`, `label`), `onReorder(ids)`, `handleLabel(item)`, `instructions`, `announce(event)`, `renderItem(item, index)`, `disabled`; a grip per item drags with a pointer or picks up, moves and drops from the keyboard, with a polite live region |
272
275
  | `ToastHost` | Renders the toast queue: `label`, `closeLabel`, optional `store`. Raise toasts with `toasts.success/error/info(message)`; `createToastStore` makes a scoped queue |
273
- | `PageHeader` | Every view starts with it: `eyebrow`, `title`, `description`; children render as right-side actions |
276
+ | `PageHeader` | Every view starts with it: `eyebrow`, `title`, `description`; children render as right-side actions, which move under the title and wrap onto more rows when the header is too narrow |
274
277
  | `EmptyState` | `icon`, `title`, children, optional `code` |
275
278
  | `Alert` | Inline message: `tone` danger (default), warning, info |
276
279
  | `Drawer` | Editor panel over the records: `open`, `title`, `subtitle`, `width` md/lg, `onClose`; traps Tab and restores focus to the opener |
@@ -442,7 +445,8 @@ Rendered by components, not written by hand: `ui-page-head*`, `ui-search`,
442
445
  `ui-table__lead` with `ui-table__toggle` and `ui-table__open` (the first cell's
443
446
  expand and open buttons), `ui-table__hide-*` (a column hidden below that width),
444
447
  `ui-table__reveal-*` (the expand button, details list and detail shown below
445
- that width), `ui-table--fold-*` (the action fold step),
448
+ that width), `ui-table--fold-*` (the action fold step), `ui-table--unpin-*`
449
+ (the step below which a single action scrolls with its row),
446
450
  `ui-table__placeholder-body` (the loading and empty row content),
447
451
  `ui-table__details` (+`-list`), `ui-table__detail`,
448
452
  `ui-table-more` (+`--always`), `ui-table-menu` (+`__scrim`, `__label`),