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.
- package/README.md +8 -5
- package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.agents/skills/database-adapter/SKILL.md +5 -5
- package/agent-template/.agents/skills/database-adapter/references/first-run-and-matrix.md +2 -2
- package/agent-template/.agents/skills/deploy-operate/SKILL.md +1 -1
- package/agent-template/.agents/skills/migration-authoring/SKILL.md +4 -4
- package/agent-template/.agents/skills/module-new/SKILL.md +1 -1
- package/agent-template/.agents/skills/spec-approval/SKILL.md +6 -2
- package/agent-template/.agents/skills/spec-interview/SKILL.md +2 -2
- package/agent-template/.agents/skills/test-hardening/SKILL.md +2 -2
- package/agent-template/.agents/skills/workflow-development/SKILL.md +95 -9
- package/agent-template/.ai/agents/sandbox/business-manager.md +2 -2
- package/agent-template/.ai/blueprints/add-migration/required-files.yaml +1 -1
- package/agent-template/.ai/blueprints/new-module/required-files.yaml +1 -1
- package/agent-template/.ai/platform-capabilities.md +10 -8
- package/agent-template/.ai/policies/capabilities.yaml +28 -12
- package/agent-template/.ai/skills/README.md +1 -1
- package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.ai/skills/database-adapter/SKILL.md +5 -5
- package/agent-template/.ai/skills/database-adapter/references/first-run-and-matrix.md +2 -2
- package/agent-template/.ai/skills/deploy-operate/SKILL.md +1 -1
- package/agent-template/.ai/skills/migration-authoring/SKILL.md +4 -4
- package/agent-template/.ai/skills/module-new/SKILL.md +1 -1
- package/agent-template/.ai/skills/spec-approval/SKILL.md +6 -2
- package/agent-template/.ai/skills/spec-interview/SKILL.md +2 -2
- package/agent-template/.ai/skills/test-hardening/SKILL.md +2 -2
- package/agent-template/.ai/skills/workflow-development/SKILL.md +96 -10
- package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
- package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.claude/skills/database-adapter/SKILL.md +5 -5
- package/agent-template/.claude/skills/database-adapter/references/first-run-and-matrix.md +2 -2
- package/agent-template/.claude/skills/deploy-operate/SKILL.md +1 -1
- package/agent-template/.claude/skills/migration-authoring/SKILL.md +4 -4
- package/agent-template/.claude/skills/module-new/SKILL.md +1 -1
- package/agent-template/.claude/skills/spec-approval/SKILL.md +6 -2
- package/agent-template/.claude/skills/spec-interview/SKILL.md +2 -2
- package/agent-template/.claude/skills/test-hardening/SKILL.md +2 -2
- package/agent-template/.claude/skills/workflow-development/SKILL.md +95 -9
- package/agent-template/docs/adr/0003-module-settings.md +2 -0
- package/agent-template/docs/adr/0007-module-owned-agents.md +35 -1
- package/agent-template/docs/agent-contract.md +3 -3
- package/agent-template/docs/cli-extensions.md +1 -0
- package/agent-template/docs/cli.md +40 -3
- package/agent-template/docs/configuration.md +64 -9
- package/agent-template/docs/database-adapters.md +30 -22
- package/agent-template/docs/design-system.md +7 -3
- package/agent-template/docs/getting-started.md +29 -32
- package/agent-template/docs/module-distribution.md +79 -86
- package/agent-template/docs/module-web-surfaces.md +9 -7
- package/agent-template/docs/modules.md +6 -2
- package/agent-template/docs/sandbox.md +23 -4
- package/agent-template/platform/scripts/build.mjs +7 -0
- package/dist/bin.js +3 -6
- package/package.json +1 -1
- package/template/default/.env.example +8 -3
- package/template/default/.vercelignore +8 -0
- package/template/default/README.md +20 -11
- package/template/default/_gitignore +3 -2
- package/template/default/infra/README.md +86 -65
- package/template/default/infra/docker/.env.example +71 -0
- package/template/default/infra/docker/Dockerfile +29 -11
- package/template/default/infra/docker/app-entrypoint.mjs +5 -0
- package/template/default/infra/docker/compose.yaml +109 -58
- package/template/default/infra/docker/database-urls.mjs +28 -0
- package/template/default/infra/docker/pitr.sh +177 -0
- package/template/default/infra/docker/postgres/10-roles.sh +16 -12
- package/template/default/infra/docker/start.mjs +402 -0
- package/template/default/infra/kubernetes/database-secret.example.yaml +3 -3
- package/template/default/infra/kubernetes/deployment.yaml +5 -0
- package/template/default/infra/sdk-module-manifests.mjs +118 -0
- package/template/default/infra/vercel/README.md +262 -0
- package/template/default/infra/vercel/build.mjs +223 -0
- package/template/default/infra/vercel/handler.mjs +100 -0
- package/template/default/modules/example/migrations/0001_example_core.up.sql +2 -2
- package/template/default/modules/example/module.json +1 -1
- package/template/default/modules/example/package.json +3 -3
- package/template/default/modules/example/spec/module.yaml +1 -1
- package/template/default/modules/example/src/services/migration.ts +2 -2
- package/template/default/modules/example/tests/module.test.ts +1 -1
- package/template/default/package.json +2 -3
- package/template/default/platform/octane.config.ts +290 -156
- package/template/default/platform/package.json +5 -5
- package/template/default/platform/scripts/build.mjs +56 -0
- package/template/default/platform/scripts/dev.mjs +101 -18
- package/template/default/platform/src/generated/modules.server.ts +1 -0
- package/template/default/platform/src/server/database.ts +24 -0
- package/template/default/platform/src/server/lifecycle.ts +325 -0
- package/template/default/platform/src/server/runtime-role.ts +33 -0
- package/template/default/platform/src/server/setup/access.ts +160 -0
- package/template/default/platform/src/server/setup/adapters.ts +554 -0
- package/template/default/platform/src/server/setup/environment.ts +154 -0
- package/template/default/platform/src/server/setup/gate.ts +84 -0
- package/template/default/platform/src/server/setup/index.ts +181 -0
- package/template/default/platform/src/server/setup/modules.ts +119 -0
- package/template/default/platform/src/server/setup/page.ts +548 -0
- package/template/default/platform/src/server/setup/routes.ts +788 -0
- package/template/default/platform/src/server/setup/sanitize.ts +111 -0
- package/template/default/platform/src/server/setup/seed.ts +192 -0
- package/template/default/platform/src/server/setup/token.ts +79 -0
- package/template/default/platform/src/server/worker-tick.ts +193 -0
- package/template/default/platform/src/server/workspace-root.ts +16 -0
- package/template/default/render.yaml +70 -0
- package/template/default/vercel.json +5 -0
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: workflow-development
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
16
|
-
`modules/workflows/spec/module.yaml`, and the contracts
|
|
17
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
-
##
|
|
238
|
+
## Module Studio distribution
|
|
202
239
|
|
|
203
|
-
`module search`, `module info`, `module
|
|
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
|
|
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
|
|
614
|
-
Administration, Modules: `mailTransport`
|
|
615
|
-
`mailSmtpUrl` (secret, write only),
|
|
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 `
|
|
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
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
passwords
|
|
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.
|
|
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
|
|
83
|
-
|
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
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
|
|
104
|
-
| ------------ |
|
|
105
|
-
| `migration` | `
|
|
106
|
-
| `runtime` | `
|
|
107
|
-
| `preview` | `
|
|
108
|
-
| `test` | `
|
|
109
|
-
| `background` | `
|
|
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
|
-
`
|
|
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('
|
|
174
|
-
WITH CHECK (tenant_id = current_setting('
|
|
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 `
|
|
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 `
|
|
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
|
|
283
|
+
FOR SELECT TO flowdular_background
|
|
276
284
|
USING (<the narrowest predicate that still finds the work>);
|
|
277
|
-
REVOKE SELECT ON <table> FROM
|
|
278
|
-
GRANT SELECT (<only the columns the poll reads>) ON <table> TO
|
|
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`),
|