create-flowdular 0.2.6 → 0.3.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 (103) hide show
  1. package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
  2. package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
  3. package/agent-template/.agents/skills/cli-extension/SKILL.md +1 -1
  4. package/agent-template/.agents/skills/deploy-operate/SKILL.md +114 -0
  5. package/agent-template/.agents/skills/module-new/SKILL.md +29 -0
  6. package/agent-template/.agents/skills/module-update/SKILL.md +9 -1
  7. package/agent-template/.agents/skills/spec-interview/SKILL.md +114 -0
  8. package/agent-template/.agents/skills/ux-design/SKILL.md +34 -3
  9. package/agent-template/.ai/README.md +2 -1
  10. package/agent-template/.ai/agents/sandbox/business-manager.md +5 -1
  11. package/agent-template/.ai/blueprints/author-spec/README.md +1 -1
  12. package/agent-template/.ai/blueprints/author-spec/spec-requirements.yaml +44 -0
  13. package/agent-template/.ai/blueprints/author-spec/steps.yaml +5 -5
  14. package/agent-template/.ai/blueprints/author-spec/templates/module.yaml +99 -3
  15. package/agent-template/.ai/blueprints/edit-module/gates.yaml +4 -0
  16. package/agent-template/.ai/blueprints/edit-module/required-files.yaml +9 -0
  17. package/agent-template/.ai/blueprints/new-module/gates.yaml +4 -0
  18. package/agent-template/.ai/blueprints/new-module/spec-requirements.yaml +2 -2
  19. package/agent-template/.ai/blueprints/release/gates.yaml +4 -0
  20. package/agent-template/.ai/platform-capabilities.md +128 -0
  21. package/agent-template/.ai/policies/capabilities.yaml +130 -3
  22. package/agent-template/.ai/policies/path-ownership.yaml +5 -2
  23. package/agent-template/.ai/policies/task-budgets.yaml +5 -3
  24. package/agent-template/.ai/references/catalog/module.json +4 -4
  25. package/agent-template/.ai/references/catalog/package.json +2 -2
  26. package/agent-template/.ai/references/catalog/spec/module.yaml +5 -3
  27. package/agent-template/.ai/references/catalog/src/platform.ts +2 -0
  28. package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +89 -1
  29. package/agent-template/.ai/references/catalog/src/services/data-classes.ts +47 -0
  30. package/agent-template/.ai/references/catalog/src/services/database-repository.ts +98 -1
  31. package/agent-template/.ai/references/catalog/src/services/repository.ts +22 -1
  32. package/agent-template/.ai/references/catalog/tests/data-classes.test.ts +157 -0
  33. package/agent-template/.ai/references/catalog.provenance.json +12 -10
  34. package/agent-template/.ai/rules/flowdular.md +4 -0
  35. package/agent-template/.ai/skills/README.md +10 -0
  36. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -2
  37. package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
  38. package/agent-template/.ai/skills/business-agent-design/SKILL.md +0 -1
  39. package/agent-template/.ai/skills/cli-extension/SKILL.md +1 -1
  40. package/agent-template/.ai/skills/deploy-operate/SKILL.md +119 -0
  41. package/agent-template/.ai/skills/module-new/SKILL.md +29 -3
  42. package/agent-template/.ai/skills/module-update/SKILL.md +9 -3
  43. package/agent-template/.ai/skills/perf-audit/SKILL.md +0 -1
  44. package/agent-template/.ai/skills/release-eject-pr/SKILL.md +0 -1
  45. package/agent-template/.ai/skills/spec-interview/SKILL.md +120 -0
  46. package/agent-template/.ai/skills/test-hardening/SKILL.md +1 -0
  47. package/agent-template/.ai/skills/ux-design/SKILL.md +34 -3
  48. package/agent-template/.ai/skills/variables/SKILL.md +0 -2
  49. package/agent-template/.ai/skills/workflow-development/SKILL.md +0 -1
  50. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
  51. package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
  52. package/agent-template/.claude/skills/cli-extension/SKILL.md +1 -1
  53. package/agent-template/.claude/skills/deploy-operate/SKILL.md +114 -0
  54. package/agent-template/.claude/skills/module-new/SKILL.md +29 -0
  55. package/agent-template/.claude/skills/module-update/SKILL.md +9 -1
  56. package/agent-template/.claude/skills/spec-interview/SKILL.md +114 -0
  57. package/agent-template/.claude/skills/ux-design/SKILL.md +34 -3
  58. package/agent-template/AGENTS.md +4 -0
  59. package/agent-template/CLAUDE.md +4 -0
  60. package/agent-template/docs/adr/0003-module-settings.md +1 -1
  61. package/agent-template/docs/adr/0006-agentic-workflows.md +24 -21
  62. package/agent-template/docs/agent-contract.md +2 -2
  63. package/agent-template/docs/cli-extensions.md +82 -0
  64. package/agent-template/docs/cli.md +195 -0
  65. package/agent-template/docs/configuration.md +593 -36
  66. package/agent-template/docs/design-system.md +185 -31
  67. package/agent-template/docs/getting-started.md +118 -0
  68. package/agent-template/docs/module-distribution.md +96 -0
  69. package/agent-template/docs/module-web-surfaces.md +221 -0
  70. package/agent-template/docs/modules.md +216 -0
  71. package/agent-template/docs/operations.md +545 -0
  72. package/agent-template/docs/sandbox.md +212 -0
  73. package/agent-template/platform/scripts/build.mjs +11 -0
  74. package/dist/bin.js +29 -0
  75. package/package.json +1 -1
  76. package/template/default/.dockerignore +14 -0
  77. package/template/default/.env.example +96 -0
  78. package/template/default/README.md +37 -1
  79. package/template/default/flowdular.json +15 -4
  80. package/template/default/infra/README.md +116 -0
  81. package/template/default/infra/docker/Dockerfile +37 -0
  82. package/template/default/infra/docker/compose.yaml +158 -0
  83. package/template/default/infra/docker/postgres/10-roles.sh +31 -0
  84. package/template/default/infra/docker/postgres/tls-init.sh +28 -0
  85. package/template/default/infra/kubernetes/database-secret.example.yaml +15 -0
  86. package/template/default/infra/kubernetes/deployment.yaml +211 -0
  87. package/template/default/infra/kubernetes/kustomization.yaml +9 -0
  88. package/template/default/infra/kubernetes/secrets.example.yaml +52 -0
  89. package/template/default/infra/kubernetes/service.yaml +13 -0
  90. package/template/default/modules/example/module.json +2 -1
  91. package/template/default/modules/example/package.json +1 -1
  92. package/template/default/modules/example/spec/module.yaml +1 -1
  93. package/template/default/modules/example/src/services/database-repository.ts +2 -12
  94. package/template/default/package.json +3 -2
  95. package/template/default/platform/octane.config.ts +99 -9
  96. package/template/default/platform/package.json +1 -1
  97. package/template/default/platform/src/generated/modules.client.ts +26 -2
  98. package/template/default/platform/src/generated/modules.server.ts +241 -10
  99. package/template/default/platform/src/server/health.ts +47 -0
  100. package/template/default/platform/src/server/metrics.ts +100 -0
  101. package/template/default/platform/src/server/storage.ts +172 -0
  102. package/template/default/platform/src/server/tracing.ts +85 -0
  103. package/template/default/specs/application.yaml +15 -0
@@ -0,0 +1,212 @@
1
+ # Sandbox
2
+
3
+ The sandbox is a separate, chat-first application that builds a change in an
4
+ isolated workspace, drives the coding agent your team already uses, runs the
5
+ same gates the platform runs, and previews the result inside the real
6
+ application shell. A session carries as many modules as the work touches.
7
+
8
+ Full documentation lives with the package:
9
+ [packages/sandbox/README.md](https://github.com/flowdular/flowdular/blob/main/packages/sandbox/README.md).
10
+
11
+ ## Start it
12
+
13
+ ```bash
14
+ pnpm sandbox # from this repository
15
+ npx @flowdular/sandbox # from any Flowdular workspace
16
+ ```
17
+
18
+ The launcher walks up to `flowdular.json` to find the workspace and opens
19
+ `http://127.0.0.1:4320`. `--port`, `--workspace`, `--host` and `--mode` override
20
+ the defaults.
21
+
22
+ ## Connect it to a running application
23
+
24
+ The sandbox is a client of a running Flowdular application and never opens the
25
+ platform database. The preview gets its own embedded PostgreSQL under the
26
+ session data directory, with the same roles and the same forced row-level
27
+ security a deployment has, and it is thrown away with the session.
28
+
29
+ 1. In the application, open Administration, API tokens, and issue a token with
30
+ `sandbox.access.use` plus the read scopes the preview should see. Add
31
+ `sandbox.preview.data` for live data and `sandbox.modules.eject` for eject.
32
+ 2. Grant sandbox access to the account, in the app under Development, Sandbox,
33
+ or from the CLI:
34
+
35
+ ```bash
36
+ pnpm flowdular sandbox grant --email admin@example.com --tenant operations-demo --apply
37
+ pnpm flowdular sandbox access --tenant operations-demo
38
+ ```
39
+
40
+ 3. Paste the token and the application address into the sandbox connect screen.
41
+
42
+ The token is encrypted at rest under `.flowdular/sandbox/secret.key` and is never
43
+ returned to the browser. The application may run anywhere: locally on
44
+ `http://127.0.0.1:4310` or a deployment.
45
+
46
+ ## Modes
47
+
48
+ | Mode | Binding | Coding agents |
49
+ | ------------- | -------------------- | ----------------------------------- |
50
+ | `loopback` | loopback interface | local `claude` and `codex`, or BYOK |
51
+ | `self-hosted` | configured interface | bring your own key only |
52
+
53
+ A non-loopback `--host` forces `self-hosted`. A sandbox that cannot prove it is
54
+ loopback never offers a local binary, because a local binary carries the
55
+ operator's own login.
56
+
57
+ ## How a session works
58
+
59
+ A planner names the modules the brief touches and the first specialist role.
60
+ Business, UX, backend, frontend and agentic roles hand off inside one session;
61
+ each turn writes only in its own allowed paths and is followed by the gates that
62
+ role declares. The roles live in [`.ai/agents/sandbox`](../.ai/agents/sandbox),
63
+ so a workspace can change them.
64
+
65
+ When the work is done, eject it into `modules/` and enable it, or open a pull
66
+ request with the gate evidence attached.
67
+
68
+ ## Deliver as a pull request
69
+
70
+ The eject route (`POST /sandbox/api/sessions/:id/eject`) takes `target:
71
+ 'workspace' | 'git-pr'`. `workspace` copies the session modules into `modules/`
72
+ of this checkout. `git-pr` (`packages/sandbox/src/server/delivery/git-pr.ts`)
73
+ commits the same change on a branch and opens a pull request; the operator's
74
+ working tree and index stay untouched because the work happens in a detached
75
+ worktree under `.flowdular/sandbox/worktrees/<session id>`, removed afterwards.
76
+
77
+ ### Configuration
78
+
79
+ Project settings live in `flowdular.json` under `sandbox.delivery`, read at
80
+ request time (`delivery/configuration.ts`): `targets`, `default`,
81
+ `maxChangedFiles` and `git` with `remote`
82
+ (`origin`), `repository` (`owner/name`, derived from the remote when null),
83
+ `baseBranch` (`main`), `branchPrefix` (`sandbox`), `provider` (`github` or
84
+ `none`), `mode` (`auto`, `direct` or `fork`), `forkOwner` and `reviewers`.
85
+
86
+ Operator settings live in the sandbox configuration
87
+ (`.flowdular/sandbox/config.json`, `GitHubDeliveryConfiguration` in
88
+ `server/config.ts`) and are set from the sandbox settings screen, not from
89
+ environment variables: `enabled`, `overridesProject`, `remote`, `repository`,
90
+ `baseBranch`, `branchPrefix`, `mode`, `forkOwner`, `reviewers` (settings
91
+ request fields `githubEnabled`, `githubOverridesProject`, `githubRemote` and so
92
+ on). With `overridesProject` the operator values replace the project `git`
93
+ block except `provider`. `enabled: false` disables the target with
94
+ `EJECT_TARGET_DISABLED`. A provider token (`githubToken` in the settings
95
+ request, stored sealed as `gitProviderToken`) is handed to `gh` as `GH_TOKEN`
96
+ and to `git` as a redacted authorization header; it never appears in command
97
+ arguments, remote URLs or step output.
98
+
99
+ ### Branch and pull request
100
+
101
+ The branch is `<branchPrefix>/<module directory>-<first 8 characters of the
102
+ session id>`, created from `<remote>/<baseBranch>`. In the worktree the sandbox
103
+ stages the modules, runs `pnpm install`, `module enable` for each new module and
104
+ the platform typecheck, then checks the guardrails: changed paths limited to the
105
+ session modules, the lockfile and the CLI-owned composition files, the file
106
+ count within `maxChangedFiles` (else `.ai/policies/task-budgets.yaml`), new
107
+ packages within `maxNewDependencies` from `.ai/policies/task-budgets.yaml`
108
+ (default 0, per-kind overrides). Owners and the cross-owner reviewer
109
+ requirement (`crossOwnerChanges.requireReviewer`) come from
110
+ `.ai/policies/path-ownership.yaml`. The commit reads
111
+ `sandbox: add|update <module id>` with the session id and the gate summary; the
112
+ push uses `--force-with-lease`. A branch that exists but was not created for
113
+ this session is refused (`GIT_BRANCH_CONFLICT`).
114
+
115
+ With `provider: github` and a working `gh auth status`, `gh pr create` opens the
116
+ pull request against `baseBranch` with `--reviewer` from `reviewers`; when a
117
+ pull request for the branch is already open, the push updates it. `mode: auto`
118
+ uses a direct push only after GitHub confirms push access and otherwise asks the
119
+ operator to choose `direct` or `fork`; only an explicit `fork` creates or reuses
120
+ `<forkOwner>/<name>`. Without `gh`, without a login, or with `provider: none`,
121
+ the branch is still pushed; the plan shows a GitHub compare link when
122
+ `repository` is configured or derived from a GitHub remote URL, otherwise the
123
+ remote and branch name.
124
+ `.flowdular/sandbox/sessions/<id>/delivery.json` keeps the branch and the URL.
125
+
126
+ ### Gates as evidence
127
+
128
+ Delivery reruns every gate from `delivery/plan.ts` (`spec-schema`,
129
+ `module-schema`, `dependencies`, `typecheck`, `tests`, `format`, `auto-review`)
130
+ before anything is committed; a missing, failed or skipped result stops it
131
+ (`EJECT_GATES_MISSING`, `EJECT_GATES_FAILED`), and each module needs a current
132
+ review record (`EJECT_REVIEW_REQUIRED`) and an approved spec hash. The pull
133
+ request body carries the spec version and status per module, a table of gate,
134
+ module and result, the files added, modified and removed, the risks the
135
+ guardrails noticed, and the post-merge `auth sync-scopes` command. The table is
136
+ what the sandbox measured on the delivered bytes; `pnpm verify` on the branch
137
+ and a human review remain the repository's own gate.
138
+
139
+ ## Decisions the specialist needs
140
+
141
+ A specialist that cannot continue without a business decision ends its reply
142
+ with one fenced block tagged `questions` holding a single JSON object:
143
+
144
+ ````
145
+ ```questions
146
+ {
147
+ "questions": [
148
+ {
149
+ "id": "Q-1",
150
+ "question": "Who may cancel a booking?",
151
+ "options": ["Only the owner", "Any team member"],
152
+ "recommended": "Only the owner",
153
+ "allowFreeText": true
154
+ }
155
+ ]
156
+ }
157
+ ```
158
+ ````
159
+
160
+ The sandbox parses it server side and bounds it: at most 12 questions, unique
161
+ ids shaped `Q-1`, a question of 1 to 400 characters, at most 8 distinct options
162
+ of 1 to 120 characters, and a recommendation that must be one of those options.
163
+ `allowFreeText` defaults to false, and a question with neither an option nor
164
+ free text cannot be answered, so it is refused. The block has to be the last
165
+ thing in the reply apart from the mandatory handoff line, and a reply carries at
166
+ most one. A block the sandbox cannot read is a warning on that turn, never a
167
+ failed turn: the words of the reply still stand and the transcript says why the
168
+ block was ignored.
169
+
170
+ A readable block is stored on the session as `pendingQuestions`, with the
171
+ transcript sequence of the message that asked, the role that asked and the
172
+ module it asked about. Records written before the field existed read as `null`.
173
+ Every turn rewrites it, so the form only ever shows what the newest specialist
174
+ is waiting on.
175
+
176
+ The session view shows the questions as a list rather than as JSON, and offers a
177
+ form below the open handoff: one radio group per question with the
178
+ recommendation preselected, a free-text field where the specialist allowed one,
179
+ and an optional note. Submitting posts
180
+
181
+ ```
182
+ POST /sandbox/api/sessions/:id/answers
183
+ { "answers": [{ "id": "Q-1", "answer": "Only the owner" }], "message": "optional" }
184
+ ```
185
+
186
+ behind the same origin, header and ownership checks as every other mutation. It
187
+ clears `pendingQuestions` and starts the next turn in the role that asked, in
188
+ the module it asked about, with the decisions leading the request text:
189
+
190
+ ```
191
+ Decisions:
192
+ - Q-1: Who may cancel a booking? -> Only the owner
193
+
194
+ <the operator's optional message>
195
+ ```
196
+
197
+ The response is the turn stream, exactly as `POST /sandbox/api/sessions/:id/turn`
198
+ answers. Refusals, each a stable error code: `409 NO_PENDING_QUESTIONS` when
199
+ nothing is waiting, `409 SESSION_ARCHIVED`, `409 SESSION_DELIVERED`, and `400
200
+ INVALID_INPUT` for a body that leaves a question unanswered, names a question
201
+ the session did not ask, exceeds 400 characters, or gives an answer that is not
202
+ one of the offered options when the specialist allowed no free text.
203
+
204
+ ## Operator commands
205
+
206
+ ```bash
207
+ pnpm flowdular sandbox sessions --tenant <tenant>
208
+ pnpm flowdular sandbox session-archive --tenant <tenant> --id <session-id> --apply
209
+ pnpm flowdular sandbox session-delete --tenant <tenant> --id <session-id> --apply
210
+ pnpm flowdular sandbox revoke --email <email> --tenant <tenant> --apply
211
+ pnpm flowdular sandbox audit-verify --tenant <tenant>
212
+ ```
@@ -17,13 +17,24 @@ const environment = {
17
17
  throwaway state directory below; runtime still reads its real adapter. */
18
18
  FD_ENV: 'development',
19
19
  FD_INTERNAL_BUILD: 'true',
20
+ /* The bundler evaluates the server composition. It must not open a trace or
21
+ error egress to the deployment's collector while building. */
22
+ FD_TRACE_EXPORTER: 'none',
23
+ FD_ERROR_SINK: 'none',
20
24
  FD_DATABASE_ADAPTER: 'pglite',
21
25
  FD_DATABASE_PGLITE_DIRECTORY: join(stateDirectory, 'pglite'),
22
26
  FD_AGENT_CREDENTIAL_KEY: buildSecret(),
23
27
  FD_AGENT_RUN_GRANT_KEY: buildSecret(),
28
+ FD_APPROVAL_GRANT_KEY: buildSecret(),
24
29
  FD_AUTOMATIONS_CREDENTIAL_KEY: buildSecret(),
30
+ FD_NOTIFICATIONS_SECRET_KEY: buildSecret(),
25
31
  FD_WORKFLOWS_PAYLOAD_KEY: buildSecret(),
26
32
  FD_WORKFLOWS_CURSOR_KEY: buildSecret(),
33
+ FD_STORAGE_ADAPTER: 'local',
34
+ FD_STORAGE_LOCAL_DIRECTORY: join(stateDirectory, 'storage'),
35
+ FD_STORAGE_ENCRYPTION_KEY: buildSecret(),
36
+ FD_CONNECTORS_SECRET_KEY: buildSecret(),
37
+ FD_AUDIT_ANCHOR_KEY: buildSecret(),
27
38
  };
28
39
 
29
40
  try {
package/dist/bin.js CHANGED
@@ -36,6 +36,11 @@ function checkProjectName(name) {
36
36
  }
37
37
  return { valid: true };
38
38
  }
39
+ function applicationSlug(name) {
40
+ const slug = name.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
41
+ if (slug.length === 0) return "app";
42
+ return /^[a-z]/.test(slug) ? slug : `app-${slug}`;
43
+ }
39
44
  function checkTargetPath(target) {
40
45
  if (target.length === 0) return { valid: false, reason: "path is empty" };
41
46
  if (target.trim() !== target) {
@@ -314,6 +319,9 @@ function renderNextSteps(input, color = false) {
314
319
  ` ${paint(step, "bold")}`,
315
320
  ""
316
321
  ]),
322
+ ` ${paint("Build a module by chat", "dim")} ${paint(runScript(input.packageManager, "sandbox"), "bold")}`,
323
+ ` ${paint("Agent guidance", "dim")} AGENTS.md, CLAUDE.md, .ai/skills`,
324
+ "",
317
325
  ` ${paint("Local URL", "dim")} ${paint(DEV_URL, "cyan")}`,
318
326
  ` ${paint("Demo login", "dim")} admin@example.com`,
319
327
  ` ${paint("Demo account is created only with local demo setup.", "dim")}`,
@@ -347,8 +355,14 @@ import { randomBytes } from "node:crypto";
347
355
  var SECRET_KEYS = [
348
356
  "FD_AGENT_CREDENTIAL_KEY",
349
357
  "FD_AGENT_RUN_GRANT_KEY",
358
+ "FD_APPROVAL_GRANT_KEY",
359
+ "FD_AUTOMATIONS_CREDENTIAL_KEY",
360
+ "FD_NOTIFICATIONS_SECRET_KEY",
350
361
  "FD_WORKFLOWS_PAYLOAD_KEY",
351
362
  "FD_WORKFLOWS_CURSOR_KEY",
363
+ "FD_STORAGE_ENCRYPTION_KEY",
364
+ "FD_CONNECTORS_SECRET_KEY",
365
+ "FD_AUDIT_ANCHOR_KEY",
352
366
  "FD_AUTH_MFA_KEY"
353
367
  ];
354
368
  function generateSecrets() {
@@ -457,6 +471,20 @@ async function rewritePackageName(directory, name) {
457
471
  manifest.name = name;
458
472
  await writeFile(path, JSON.stringify(manifest, void 0, " ") + "\n");
459
473
  }
474
+ var APPLICATION_SPEC_ID = "application.app-name";
475
+ async function rewriteApplicationSpecId(directory, name) {
476
+ const path = join2(directory, "specs", "application.yaml");
477
+ const spec = await readFile(path, "utf8");
478
+ if (!spec.includes(APPLICATION_SPEC_ID)) {
479
+ throw new ScaffoldError(
480
+ `The template spec ${path} no longer carries the "${APPLICATION_SPEC_ID}" placeholder.`
481
+ );
482
+ }
483
+ await writeFile(
484
+ path,
485
+ spec.replace(APPLICATION_SPEC_ID, `application.${applicationSlug(name)}`)
486
+ );
487
+ }
460
488
  async function scaffold(request) {
461
489
  const directory = resolve2(request.cwd, request.target);
462
490
  const name = basename(directory);
@@ -478,6 +506,7 @@ async function scaffold(request) {
478
506
  await assertTemplateExists(agentTemplate, "agent guidance");
479
507
  const files = await copyTemplate(template, directory) + await copyTemplate(agentTemplate, directory);
480
508
  await rewritePackageName(directory, name);
509
+ await rewriteApplicationSpecId(directory, name);
481
510
  await writeFile(
482
511
  join2(directory, ".env"),
483
512
  renderEnvironmentFile(request.secrets ?? generateSecrets())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-flowdular",
3
- "version": "0.2.6",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Scaffold a Flowdular application: the platform, one example module and the secrets a fresh install needs.",
6
6
  "license": "MIT",
@@ -0,0 +1,14 @@
1
+ .git
2
+ .github
3
+ .DS_Store
4
+ # Matched against the whole context-relative path, so a root-anchored .env*
5
+ # would still copy infra/docker/.env into the builder image.
6
+ **/.env
7
+ **/.env.*
8
+ !**/.env.example
9
+ **/node_modules
10
+ **/dist
11
+ **/coverage
12
+ **/.flowdular
13
+ .flowdular/
14
+ *.log
@@ -0,0 +1,96 @@
1
+ # Production configuration for this Flowdular app. Copy it, fill in every value
2
+ # and keep the copy out of git. The .env this scaffold generated is the local
3
+ # development file and already carries working keys for embedded PostgreSQL.
4
+ #
5
+ # Generate each 32 byte key with: openssl rand -base64 32
6
+ # For FD_AUTH_MFA_KEY use the URL-safe alphabet: openssl rand -base64 32 | tr '+/' '-_'
7
+
8
+ NODE_ENV=production
9
+ FD_PORT=3000
10
+
11
+ # One PostgreSQL database, three roles. The migrator owns the schema, the
12
+ # runtime role holds neither SUPERUSER nor BYPASSRLS so the row-level security
13
+ # every tenant table forces actually binds the app, and the background role
14
+ # serves the cross-tenant scheduler poll. pglite is refused in production.
15
+ FD_DATABASE_ADAPTER=postgresql
16
+ FD_DATABASE_URL=postgresql://coreloom_runtime:REPLACE_ME@postgres:5432/flowdular
17
+ FD_DATABASE_MIGRATOR_URL=postgresql://coreloom_migrator:REPLACE_ME@postgres:5432/flowdular
18
+ FD_DATABASE_BACKGROUND_URL=postgresql://coreloom_background:REPLACE_ME@postgres:5432/flowdular
19
+ FD_DATABASE_TLS=verify-full
20
+ FD_DATABASE_TLS_CA_FILE=/tls/server.crt
21
+
22
+ # TLS terminates in front of the app. Set this to false only for a plain-HTTP
23
+ # run on a workstation: the session cookie then loses the Secure flag and the
24
+ # __Host- prefix.
25
+ FD_AUTH_SECURE_COOKIE=true
26
+ FD_AUTH_ALLOW_SIGN_UP=false
27
+
28
+ # Trust x-forwarded-for and x-forwarded-proto. Keep false unless a proxy you
29
+ # control terminates TLS in front of the app and overwrites those headers; on a
30
+ # directly reachable port a client can forge the address the sign-in limiter
31
+ # and the audit trail record. The compose sample publishes the port directly.
32
+ FD_TRUST_PROXY=false
33
+
34
+ # agents.core, automations.core and workflows.core each refuse to boot in
35
+ # production without their key.
36
+ FD_AGENT_CREDENTIAL_KEY=
37
+ FD_AGENT_RUN_GRANT_KEY=
38
+ FD_APPROVAL_GRANT_KEY=
39
+ FD_APPROVAL_GRANT_KEY_PREVIOUS=
40
+ FD_AUTOMATIONS_CREDENTIAL_KEY=
41
+ FD_NOTIFICATIONS_SECRET_KEY=
42
+ FD_WORKFLOWS_PAYLOAD_KEY=
43
+ FD_WORKFLOWS_CURSOR_KEY=
44
+
45
+ # Object storage. The local adapter is refused in production, so a deployment
46
+ # points at an S3-compatible bucket (AWS, MinIO, R2). Objects are encrypted with
47
+ # AES-256-GCM under this key before they are written, and the key is required in
48
+ # production: without it stored objects cannot be read back.
49
+ FD_STORAGE_ADAPTER=s3
50
+ FD_STORAGE_S3_BUCKET=
51
+ FD_STORAGE_S3_REGION=
52
+ FD_STORAGE_S3_ENDPOINT=
53
+ FD_STORAGE_S3_ACCESS_KEY_ID=
54
+ FD_STORAGE_S3_SECRET_ACCESS_KEY=
55
+ FD_STORAGE_S3_FORCE_PATH_STYLE=false
56
+ FD_STORAGE_MAX_OBJECT_BYTES=26214400
57
+ FD_STORAGE_ENCRYPTION_KEY=
58
+ FD_CONNECTORS_SECRET_KEY=
59
+ FD_AUDIT_ANCHOR_KEY=
60
+
61
+ # Retired keys, comma separated, at most eight each. Rotation replaces a key
62
+ # without losing what it sealed: move the old key here, put the new one above,
63
+ # deploy, re-seal the stored rows with the matching "<module> secrets-rotate
64
+ # --apply", then empty the entry. Full procedure in docs/operations.md.
65
+ FD_AGENT_CREDENTIAL_KEY_PREVIOUS=
66
+ FD_AUTOMATIONS_CREDENTIAL_KEY_PREVIOUS=
67
+ FD_NOTIFICATIONS_SECRET_KEY_PREVIOUS=
68
+ FD_WORKFLOWS_PAYLOAD_KEY_PREVIOUS=
69
+ FD_WORKFLOWS_CURSOR_KEY_PREVIOUS=
70
+ FD_STORAGE_ENCRYPTION_KEY_PREVIOUS=
71
+ FD_CONNECTORS_SECRET_KEY_PREVIOUS=
72
+ FD_AUDIT_ANCHOR_KEY_PREVIOUS=
73
+
74
+ # Base64url 32 byte key encrypting MFA secrets. Without it the app runs and only
75
+ # MFA enrollment is unavailable. It rotates the same way, through
76
+ # "flowdular auth secrets-rotate --apply".
77
+ FD_AUTH_MFA_KEY=
78
+ FD_AUTH_MFA_KEY_PREVIOUS=
79
+
80
+ # Outgoing mail. With none, workspace invitations are refused and password reset
81
+ # messages are never delivered. Set smtp and both values below to send them; the
82
+ # URL carries the relay password, so treat the filled copy as a secret.
83
+ FD_AUTH_MAIL_TRANSPORT=none
84
+ FD_AUTH_SMTP_URL=
85
+ FD_AUTH_MAIL_FROM=
86
+
87
+ # Read by infra/docker/compose.yaml only. It builds the three URLs above from
88
+ # these passwords and publishes the container port on FD_PORT.
89
+ FD_POSTGRES_SUPERUSER_PASSWORD=
90
+ FD_DATABASE_MIGRATOR_PASSWORD=
91
+ FD_DATABASE_RUNTIME_PASSWORD=
92
+ FD_DATABASE_BACKGROUND_PASSWORD=
93
+
94
+ # Prometheus exposition on GET /api/metrics. Leave FD_METRICS unset to keep it off.
95
+ FD_METRICS=false
96
+ FD_METRICS_TOKEN=
@@ -24,8 +24,11 @@ install. Data lives under `.flowdular/data`.
24
24
  | ----------------- | ------------------------------------------------------------ |
25
25
  | `platform` | The deployable composition root and the application shell |
26
26
  | `modules/example` | A tenant-scoped record module: API, migration, screen, tests |
27
+ | `specs` | The platform spec of this application |
28
+ | `infra` | Dockerfile, compose stack and Kubernetes base |
27
29
  | `flowdular.json` | Enabled modules and locales, owned by the CLI |
28
30
  | `.env` | The keys generated for this app. Never commit it |
31
+ | `.env.example` | Every key a production deployment reads |
29
32
 
30
33
  ## Work with coding agents
31
34
 
@@ -56,11 +59,44 @@ tests.
56
59
  `modules.enabled` in `flowdular.json` are written by the CLI. Never edit them by
57
60
  hand.
58
61
 
62
+ ## Build a module by chat
63
+
64
+ ```bash
65
+ pnpm sandbox
66
+ ```
67
+
68
+ The sandbox is a chat-first builder on `http://127.0.0.1:4320`. You describe the
69
+ change, it writes the module in an isolated workspace, runs the same gates this
70
+ repository runs, and previews the result inside the real application shell.
71
+ `--port`, `--workspace`, `--host` and `--mode` override the defaults.
72
+
73
+ It is a client of a running application and never opens its database. Connect it
74
+ once: in the app open Administration, API tokens, issue a token with
75
+ `sandbox.access.use` plus the read scopes the preview should see, grant the
76
+ account access, then paste the token and the application address into the
77
+ connect screen.
78
+
79
+ ```bash
80
+ pnpm flowdular sandbox grant --email admin@example.com --tenant operations-demo --apply
81
+ ```
82
+
83
+ ## Deploy
84
+
85
+ ```bash
86
+ cp .env.example infra/docker/.env # then fill in every value
87
+ docker compose -f infra/docker/compose.yaml up --build
88
+ ```
89
+
90
+ `.env.example` lists every key the server reads in production. `infra/README.md`
91
+ covers the container image, the PostgreSQL roles and TLS, migrations on rollout,
92
+ and the Kubernetes base.
93
+
59
94
  ## Commands
60
95
 
61
96
  ```bash
62
97
  pnpm dev # platform with HMR on http://localhost:4310
63
- pnpm verify # typecheck, tests, module validation, format
98
+ pnpm sandbox # chat-first module builder on http://127.0.0.1:4320
99
+ pnpm verify # typecheck, tests, spec and module validation, format
64
100
  pnpm flowdular doctor # workspace health
65
101
  ```
66
102
 
@@ -11,14 +11,25 @@
11
11
  "enabled": [
12
12
  "system.core",
13
13
  "auth.core",
14
+ "access.core",
15
+ "reports.core",
16
+ "metering.core",
14
17
  "agents.core",
15
- "automations.core",
18
+ "approvals.core",
19
+ "audit.core",
16
20
  "workflows.core",
17
- "automations-workflows.integration",
18
- "example.core",
21
+ "automations.core",
22
+ "connectors.core",
23
+ "directory.core",
24
+ "documents.core",
25
+ "exports.core",
26
+ "import.core",
27
+ "notifications.core",
19
28
  "profile.core",
20
29
  "sandbox.core",
21
- "users.core"
30
+ "search.core",
31
+ "users.core",
32
+ "example.core"
22
33
  ]
23
34
  },
24
35
  "locales": ["en", "pl"],
@@ -0,0 +1,116 @@
1
+ # Deployment
2
+
3
+ The production artifact is the server built from `platform`. It runs as a
4
+ non-root user and serves two public probes from
5
+ `platform/src/server/health.ts`: `GET /api/health` answers as soon as the
6
+ process is up and touches nothing else, so it is the liveness probe, and
7
+ `GET /api/ready` calls the database provider, checks that the runtime and
8
+ background roles hold neither `SUPERUSER` nor `BYPASSRLS`, and answers 503 with
9
+ `retry-after: 1` while the database is unavailable.
10
+
11
+ ## Local container
12
+
13
+ ```bash
14
+ cp .env.example infra/docker/.env # then fill in every value
15
+ docker compose -f infra/docker/compose.yaml up --build
16
+ ```
17
+
18
+ Compose refuses to start while a key or a database password is empty, and the
19
+ owning module would refuse at boot anyway. The app is published on
20
+ `http://localhost:3000`; set `FD_PORT` to change the host port.
21
+
22
+ The build stage runs `pnpm install --frozen-lockfile`, so commit
23
+ `pnpm-lock.yaml` before building.
24
+
25
+ Compose also starts PostgreSQL. A one-shot `postgres-tls` service generates a
26
+ self-signed server certificate for `CN=postgres` on first run, Postgres serves
27
+ TLS with it, and the app verifies it through `FD_DATABASE_TLS_CA_FILE` under
28
+ `verify-full`. First cluster initialization creates three roles:
29
+ `coreloom_migrator` owns the schema, `coreloom_runtime` holds neither
30
+ `SUPERUSER` nor `BYPASSRLS`, so the row-level security tenant tables force
31
+ actually binds the application, and `coreloom_background` serves the
32
+ cross-tenant scheduler poll with no default table grant at all.
33
+
34
+ Public sign-up is disabled. The session cookie is Secure (`__Host-` prefix)
35
+ because the container expects TLS in front of it. For a plain-HTTP run on a
36
+ workstation set `FD_AUTH_SECURE_COOKIE=false` in `infra/docker/.env`; do not do
37
+ this for anything reachable from a network.
38
+
39
+ The runtime image contains only `platform/dist` and `platform/package.json`. The
40
+ server bundle imports node built-ins exclusively, so no `node_modules` directory
41
+ ships with it.
42
+
43
+ ## Migrations on rollout
44
+
45
+ Every module owns its migrations and applies them itself, inside a migration
46
+ lease that connects as `FD_DATABASE_MIGRATOR_URL`, the first time its runtime is
47
+ used. A rollout therefore needs no apply step: start the new image and the
48
+ schema catches up under the schema-owning role, while requests keep running as
49
+ the runtime role.
50
+
51
+ What a rollout should do is fail before the new image serves traffic when an
52
+ already applied migration no longer matches the code being deployed. That is the
53
+ one-shot `migration-check` service in `infra/docker/compose.yaml`: it runs
54
+ `flowdular migration status --json`, reports what is pending, and exits non-zero
55
+ on a checksum mismatch. `app` starts only after it completes successfully.
56
+
57
+ ```bash
58
+ docker compose -f infra/docker/compose.yaml run --rm migration-check
59
+ ```
60
+
61
+ Outside compose, run the same check from a checkout with the deployment's
62
+ `FD_DATABASE_*` values in the environment. On Kubernetes it belongs in a
63
+ pre-upgrade Job or in the pipeline step ahead of `kubectl apply`. After the
64
+ rollout, `flowdular migration verify` should come back clean.
65
+
66
+ `flowdular migration apply` exists, but it is not part of a rollout. It takes one
67
+ module at a time (`--module <id> --apply`; there is no `--all` flag) and the
68
+ capability is local-only: the runner refuses it unless `FD_ENV` or `NODE_ENV` is
69
+ `development` or `test`. Use it on a workstation against a local database:
70
+
71
+ ```bash
72
+ pnpm flowdular migration status # pending work, per module
73
+ pnpm flowdular migration apply --module example.core # dry run
74
+ pnpm flowdular migration apply --module example.core --apply
75
+ ```
76
+
77
+ Repeat the last two lines for each module `migration status` reports.
78
+
79
+ Applied migrations are immutable. To change schema, add a numbered migration;
80
+ never edit one the ledger already recorded.
81
+
82
+ ## Published image
83
+
84
+ Build and publish the image from `infra/docker/Dockerfile`, then set the name in
85
+ `infra/kubernetes/kustomization.yaml` and `deployment.yaml`, which ship with an
86
+ `ghcr.io/OWNER/REPOSITORY` placeholder.
87
+
88
+ ```bash
89
+ kubectl create secret generic flowdular-secrets \
90
+ --from-literal=agentCredentialKey="$(openssl rand -base64 32)" \
91
+ --from-literal=agentRunGrantKey="$(openssl rand -base64 32)" \
92
+ --from-literal=automationsCredentialKey="$(openssl rand -base64 32)" \
93
+ --from-literal=workflowsPayloadKey="$(openssl rand -base64 32)" \
94
+ --from-literal=workflowsCursorKey="$(openssl rand -base64 32)"
95
+ kubectl create secret generic flowdular-database \
96
+ --from-literal=migratorUrl='postgresql://coreloom_migrator:...@postgres:5432/flowdular' \
97
+ --from-literal=runtimeUrl='postgresql://coreloom_runtime:...@postgres:5432/flowdular' \
98
+ --from-literal=backgroundUrl='postgresql://coreloom_background:...@postgres:5432/flowdular'
99
+ kubectl apply -k infra/kubernetes
100
+ ```
101
+
102
+ `secrets.example.yaml` and `database-secret.example.yaml` show both Secret shapes
103
+ with placeholders and are deliberately not part of the kustomization. Rotate
104
+ `agentCredentialKey` and `workflowsPayloadKey` through the `*_KEY_PREVIOUS`
105
+ variables rather than by replacing the value: the retired key keeps opening the
106
+ stored rows until `flowdular agents secrets-rotate --apply` and
107
+ `flowdular workflows secrets-rotate --apply` have re-sealed them. Replacing a key
108
+ outright does lose what it sealed, so follow the key rotation section of
109
+ `docs/operations.md`.
110
+
111
+ The Kubernetes base carries no volume for application data: the deployment is
112
+ stateless and every module writes to PostgreSQL. Scaling writers across nodes
113
+ needs nothing beyond the database they already share.
114
+
115
+ `.env.example` at the repository root lists every key the server reads,
116
+ including the optional mail transport settings.
@@ -0,0 +1,37 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ FROM node:24-bookworm-slim AS builder
3
+
4
+ ENV PNPM_HOME=/pnpm
5
+ ENV PATH=$PNPM_HOME:$PATH
6
+ WORKDIR /workspace
7
+
8
+ RUN corepack enable && corepack prepare pnpm@11.17.0 --activate
9
+
10
+ COPY . .
11
+ RUN pnpm install --frozen-lockfile
12
+ RUN pnpm verify && pnpm build
13
+
14
+ FROM node:24-bookworm-slim AS runtime
15
+
16
+ ENV NODE_ENV=production
17
+ ENV PORT=3000
18
+ WORKDIR /app
19
+
20
+ RUN groupadd --system --gid 1001 flowdular \
21
+ && useradd --system --uid 1001 --gid flowdular flowdular \
22
+ && mkdir -p /data \
23
+ && chown flowdular:flowdular /data
24
+
25
+ # The server bundle is self-contained: platform/dist/server/entry.js imports
26
+ # only node:* built-ins, so no node_modules (and no devDependencies) ship.
27
+ COPY --from=builder --chown=flowdular:flowdular /workspace/platform/dist ./platform/dist
28
+ COPY --from=builder --chown=flowdular:flowdular /workspace/platform/package.json ./platform/package.json
29
+
30
+ USER flowdular
31
+ EXPOSE 3000
32
+ VOLUME ["/data"]
33
+
34
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
35
+ CMD node -e "fetch('http://127.0.0.1:3000/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
36
+
37
+ CMD ["node", "platform/dist/server/entry.js"]