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.
- 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/cli-extension/SKILL.md +1 -1
- package/agent-template/.agents/skills/deploy-operate/SKILL.md +114 -0
- package/agent-template/.agents/skills/module-new/SKILL.md +29 -0
- package/agent-template/.agents/skills/module-update/SKILL.md +9 -1
- package/agent-template/.agents/skills/spec-interview/SKILL.md +114 -0
- package/agent-template/.agents/skills/ux-design/SKILL.md +34 -3
- package/agent-template/.ai/README.md +2 -1
- package/agent-template/.ai/agents/sandbox/business-manager.md +5 -1
- package/agent-template/.ai/blueprints/author-spec/README.md +1 -1
- package/agent-template/.ai/blueprints/author-spec/spec-requirements.yaml +44 -0
- package/agent-template/.ai/blueprints/author-spec/steps.yaml +5 -5
- package/agent-template/.ai/blueprints/author-spec/templates/module.yaml +99 -3
- package/agent-template/.ai/blueprints/edit-module/gates.yaml +4 -0
- package/agent-template/.ai/blueprints/edit-module/required-files.yaml +9 -0
- package/agent-template/.ai/blueprints/new-module/gates.yaml +4 -0
- package/agent-template/.ai/blueprints/new-module/spec-requirements.yaml +2 -2
- package/agent-template/.ai/blueprints/release/gates.yaml +4 -0
- package/agent-template/.ai/platform-capabilities.md +128 -0
- package/agent-template/.ai/policies/capabilities.yaml +130 -3
- package/agent-template/.ai/policies/path-ownership.yaml +5 -2
- package/agent-template/.ai/policies/task-budgets.yaml +5 -3
- package/agent-template/.ai/references/catalog/module.json +4 -4
- package/agent-template/.ai/references/catalog/package.json +2 -2
- package/agent-template/.ai/references/catalog/spec/module.yaml +5 -3
- package/agent-template/.ai/references/catalog/src/platform.ts +2 -0
- package/agent-template/.ai/references/catalog/src/services/catalog-service.ts +89 -1
- package/agent-template/.ai/references/catalog/src/services/data-classes.ts +47 -0
- package/agent-template/.ai/references/catalog/src/services/database-repository.ts +98 -1
- package/agent-template/.ai/references/catalog/src/services/repository.ts +22 -1
- package/agent-template/.ai/references/catalog/tests/data-classes.test.ts +157 -0
- package/agent-template/.ai/references/catalog.provenance.json +12 -10
- package/agent-template/.ai/rules/flowdular.md +4 -0
- package/agent-template/.ai/skills/README.md +10 -0
- package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -2
- package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
- package/agent-template/.ai/skills/business-agent-design/SKILL.md +0 -1
- package/agent-template/.ai/skills/cli-extension/SKILL.md +1 -1
- package/agent-template/.ai/skills/deploy-operate/SKILL.md +119 -0
- package/agent-template/.ai/skills/module-new/SKILL.md +29 -3
- package/agent-template/.ai/skills/module-update/SKILL.md +9 -3
- package/agent-template/.ai/skills/perf-audit/SKILL.md +0 -1
- package/agent-template/.ai/skills/release-eject-pr/SKILL.md +0 -1
- package/agent-template/.ai/skills/spec-interview/SKILL.md +120 -0
- package/agent-template/.ai/skills/test-hardening/SKILL.md +1 -0
- package/agent-template/.ai/skills/ux-design/SKILL.md +34 -3
- package/agent-template/.ai/skills/variables/SKILL.md +0 -2
- package/agent-template/.ai/skills/workflow-development/SKILL.md +0 -1
- 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/cli-extension/SKILL.md +1 -1
- package/agent-template/.claude/skills/deploy-operate/SKILL.md +114 -0
- package/agent-template/.claude/skills/module-new/SKILL.md +29 -0
- package/agent-template/.claude/skills/module-update/SKILL.md +9 -1
- package/agent-template/.claude/skills/spec-interview/SKILL.md +114 -0
- package/agent-template/.claude/skills/ux-design/SKILL.md +34 -3
- package/agent-template/AGENTS.md +4 -0
- package/agent-template/CLAUDE.md +4 -0
- package/agent-template/docs/adr/0003-module-settings.md +1 -1
- package/agent-template/docs/adr/0006-agentic-workflows.md +24 -21
- package/agent-template/docs/agent-contract.md +2 -2
- package/agent-template/docs/cli-extensions.md +82 -0
- package/agent-template/docs/cli.md +195 -0
- package/agent-template/docs/configuration.md +593 -36
- package/agent-template/docs/design-system.md +185 -31
- package/agent-template/docs/getting-started.md +118 -0
- package/agent-template/docs/module-distribution.md +96 -0
- package/agent-template/docs/module-web-surfaces.md +221 -0
- package/agent-template/docs/modules.md +216 -0
- package/agent-template/docs/operations.md +545 -0
- package/agent-template/docs/sandbox.md +212 -0
- package/agent-template/platform/scripts/build.mjs +11 -0
- package/dist/bin.js +29 -0
- package/package.json +1 -1
- package/template/default/.dockerignore +14 -0
- package/template/default/.env.example +96 -0
- package/template/default/README.md +37 -1
- package/template/default/flowdular.json +15 -4
- package/template/default/infra/README.md +116 -0
- package/template/default/infra/docker/Dockerfile +37 -0
- package/template/default/infra/docker/compose.yaml +158 -0
- package/template/default/infra/docker/postgres/10-roles.sh +31 -0
- package/template/default/infra/docker/postgres/tls-init.sh +28 -0
- package/template/default/infra/kubernetes/database-secret.example.yaml +15 -0
- package/template/default/infra/kubernetes/deployment.yaml +211 -0
- package/template/default/infra/kubernetes/kustomization.yaml +9 -0
- package/template/default/infra/kubernetes/secrets.example.yaml +52 -0
- package/template/default/infra/kubernetes/service.yaml +13 -0
- package/template/default/modules/example/module.json +2 -1
- package/template/default/modules/example/package.json +1 -1
- package/template/default/modules/example/spec/module.yaml +1 -1
- package/template/default/modules/example/src/services/database-repository.ts +2 -12
- package/template/default/package.json +3 -2
- package/template/default/platform/octane.config.ts +99 -9
- package/template/default/platform/package.json +1 -1
- package/template/default/platform/src/generated/modules.client.ts +26 -2
- package/template/default/platform/src/generated/modules.server.ts +241 -10
- package/template/default/platform/src/server/health.ts +47 -0
- package/template/default/platform/src/server/metrics.ts +100 -0
- package/template/default/platform/src/server/storage.ts +172 -0
- package/template/default/platform/src/server/tracing.ts +85 -0
- 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
|
@@ -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
|
|
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
|
-
"
|
|
18
|
+
"approvals.core",
|
|
19
|
+
"audit.core",
|
|
16
20
|
"workflows.core",
|
|
17
|
-
"automations
|
|
18
|
-
"
|
|
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
|
-
"
|
|
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"]
|