create-flowdular 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +8 -5
  2. package/agent-template/.agents/skills/agent-tool-design/SKILL.md +1 -1
  3. package/agent-template/.agents/skills/auth-security-review/SKILL.md +1 -1
  4. package/agent-template/.agents/skills/database-adapter/SKILL.md +5 -5
  5. package/agent-template/.agents/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  6. package/agent-template/.agents/skills/deploy-operate/SKILL.md +1 -1
  7. package/agent-template/.agents/skills/migration-authoring/SKILL.md +4 -4
  8. package/agent-template/.agents/skills/module-new/SKILL.md +1 -1
  9. package/agent-template/.agents/skills/spec-approval/SKILL.md +6 -2
  10. package/agent-template/.agents/skills/spec-interview/SKILL.md +2 -2
  11. package/agent-template/.agents/skills/test-hardening/SKILL.md +2 -2
  12. package/agent-template/.agents/skills/workflow-development/SKILL.md +95 -9
  13. package/agent-template/.ai/agents/sandbox/business-manager.md +2 -2
  14. package/agent-template/.ai/blueprints/add-migration/required-files.yaml +1 -1
  15. package/agent-template/.ai/blueprints/new-module/required-files.yaml +1 -1
  16. package/agent-template/.ai/platform-capabilities.md +10 -8
  17. package/agent-template/.ai/policies/capabilities.yaml +28 -12
  18. package/agent-template/.ai/skills/README.md +1 -1
  19. package/agent-template/.ai/skills/agent-tool-design/SKILL.md +1 -1
  20. package/agent-template/.ai/skills/auth-security-review/SKILL.md +1 -1
  21. package/agent-template/.ai/skills/database-adapter/SKILL.md +5 -5
  22. package/agent-template/.ai/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  23. package/agent-template/.ai/skills/deploy-operate/SKILL.md +1 -1
  24. package/agent-template/.ai/skills/migration-authoring/SKILL.md +4 -4
  25. package/agent-template/.ai/skills/module-new/SKILL.md +1 -1
  26. package/agent-template/.ai/skills/spec-approval/SKILL.md +6 -2
  27. package/agent-template/.ai/skills/spec-interview/SKILL.md +2 -2
  28. package/agent-template/.ai/skills/test-hardening/SKILL.md +2 -2
  29. package/agent-template/.ai/skills/workflow-development/SKILL.md +96 -10
  30. package/agent-template/.claude/skills/agent-tool-design/SKILL.md +1 -1
  31. package/agent-template/.claude/skills/auth-security-review/SKILL.md +1 -1
  32. package/agent-template/.claude/skills/database-adapter/SKILL.md +5 -5
  33. package/agent-template/.claude/skills/database-adapter/references/first-run-and-matrix.md +2 -2
  34. package/agent-template/.claude/skills/deploy-operate/SKILL.md +1 -1
  35. package/agent-template/.claude/skills/migration-authoring/SKILL.md +4 -4
  36. package/agent-template/.claude/skills/module-new/SKILL.md +1 -1
  37. package/agent-template/.claude/skills/spec-approval/SKILL.md +6 -2
  38. package/agent-template/.claude/skills/spec-interview/SKILL.md +2 -2
  39. package/agent-template/.claude/skills/test-hardening/SKILL.md +2 -2
  40. package/agent-template/.claude/skills/workflow-development/SKILL.md +95 -9
  41. package/agent-template/docs/adr/0003-module-settings.md +2 -0
  42. package/agent-template/docs/adr/0007-module-owned-agents.md +35 -1
  43. package/agent-template/docs/agent-contract.md +3 -3
  44. package/agent-template/docs/cli-extensions.md +1 -0
  45. package/agent-template/docs/cli.md +40 -3
  46. package/agent-template/docs/configuration.md +64 -9
  47. package/agent-template/docs/database-adapters.md +30 -22
  48. package/agent-template/docs/design-system.md +7 -3
  49. package/agent-template/docs/getting-started.md +29 -32
  50. package/agent-template/docs/module-distribution.md +79 -86
  51. package/agent-template/docs/module-web-surfaces.md +9 -7
  52. package/agent-template/docs/modules.md +6 -2
  53. package/agent-template/docs/sandbox.md +23 -4
  54. package/agent-template/platform/scripts/build.mjs +7 -0
  55. package/dist/bin.js +3 -6
  56. package/package.json +1 -1
  57. package/template/default/.env.example +8 -3
  58. package/template/default/.vercelignore +8 -0
  59. package/template/default/README.md +20 -11
  60. package/template/default/_gitignore +3 -2
  61. package/template/default/infra/README.md +86 -65
  62. package/template/default/infra/docker/.env.example +71 -0
  63. package/template/default/infra/docker/Dockerfile +29 -11
  64. package/template/default/infra/docker/app-entrypoint.mjs +5 -0
  65. package/template/default/infra/docker/compose.yaml +109 -58
  66. package/template/default/infra/docker/database-urls.mjs +28 -0
  67. package/template/default/infra/docker/pitr.sh +177 -0
  68. package/template/default/infra/docker/postgres/10-roles.sh +16 -12
  69. package/template/default/infra/docker/start.mjs +402 -0
  70. package/template/default/infra/kubernetes/database-secret.example.yaml +3 -3
  71. package/template/default/infra/kubernetes/deployment.yaml +5 -0
  72. package/template/default/infra/sdk-module-manifests.mjs +118 -0
  73. package/template/default/infra/vercel/README.md +262 -0
  74. package/template/default/infra/vercel/build.mjs +223 -0
  75. package/template/default/infra/vercel/handler.mjs +100 -0
  76. package/template/default/modules/example/migrations/0001_example_core.up.sql +2 -2
  77. package/template/default/modules/example/module.json +1 -1
  78. package/template/default/modules/example/package.json +3 -3
  79. package/template/default/modules/example/spec/module.yaml +1 -1
  80. package/template/default/modules/example/src/services/migration.ts +2 -2
  81. package/template/default/modules/example/tests/module.test.ts +1 -1
  82. package/template/default/package.json +2 -3
  83. package/template/default/platform/octane.config.ts +290 -156
  84. package/template/default/platform/package.json +5 -5
  85. package/template/default/platform/scripts/build.mjs +56 -0
  86. package/template/default/platform/scripts/dev.mjs +101 -18
  87. package/template/default/platform/src/generated/modules.server.ts +1 -0
  88. package/template/default/platform/src/server/database.ts +24 -0
  89. package/template/default/platform/src/server/lifecycle.ts +325 -0
  90. package/template/default/platform/src/server/runtime-role.ts +33 -0
  91. package/template/default/platform/src/server/setup/access.ts +160 -0
  92. package/template/default/platform/src/server/setup/adapters.ts +554 -0
  93. package/template/default/platform/src/server/setup/environment.ts +154 -0
  94. package/template/default/platform/src/server/setup/gate.ts +84 -0
  95. package/template/default/platform/src/server/setup/index.ts +181 -0
  96. package/template/default/platform/src/server/setup/modules.ts +119 -0
  97. package/template/default/platform/src/server/setup/page.ts +548 -0
  98. package/template/default/platform/src/server/setup/routes.ts +788 -0
  99. package/template/default/platform/src/server/setup/sanitize.ts +111 -0
  100. package/template/default/platform/src/server/setup/seed.ts +192 -0
  101. package/template/default/platform/src/server/setup/token.ts +79 -0
  102. package/template/default/platform/src/server/worker-tick.ts +193 -0
  103. package/template/default/platform/src/server/workspace-root.ts +16 -0
  104. package/template/default/render.yaml +70 -0
  105. package/template/default/vercel.json +5 -0
@@ -1,5 +1,49 @@
1
1
  # Deployment
2
2
 
3
+ ## Deployment targets
4
+
5
+ Run `pnpm flowdular deploy targets` to inspect the target contracts and
6
+ `pnpm flowdular deploy plan docker` before starting the local stack.
7
+ `pnpm flowdular deploy start docker --apply` starts Docker Compose and opens
8
+ first-run setup. It prints the setup token, so it requires a private
9
+ interactive terminal and refuses `--json` or redirected output.
10
+
11
+ The repository-root `render.yaml` is a Render Blueprint for an always-on
12
+ container. It needs external PostgreSQL with separate runtime, background and
13
+ migrator roles, verified TLS and an existing S3 bucket. The Blueprint prompts
14
+ for the database and storage values, derives its public HTTPS origin from the
15
+ Render service and generates encryption keys. Back up those keys separately.
16
+ If you add a custom domain, set its HTTPS origin in `render.yaml` so the next
17
+ Blueprint sync keeps it. It uses
18
+ `/api/health` during initial setup; verify
19
+ `/api/ready` after setup. See the main Flowdular deployment guide for details.
20
+ Render Blueprints can create a database, but `fromDatabase` gives its internal
21
+ URL, whose self-signed TLS certificate cannot satisfy Flowdular's production
22
+ `verify-full` requirement. The Blueprint also cannot declare the three
23
+ separate roles. Render can host MinIO, but this Blueprint does not initialize
24
+ its bucket or wire its credentials. For a store other than AWS S3, set
25
+ `FD_STORAGE_S3_ENDPOINT` and, if needed, `FD_STORAGE_S3_FORCE_PATH_STYLE` on
26
+ the Render service. See the [Render Postgres connection guide](https://render.com/docs/postgresql-creating-connecting)
27
+ and [MinIO guide](https://render.com/docs/deploy-minio).
28
+ After committing
29
+ `render.yaml` and `infra/docker/Dockerfile` and pushing the branch to a
30
+ credential-free Git origin, `deploy plan render --json` returns a Deploy to
31
+ Render URL when both files pass structural checks and match the pushed branch. The URL
32
+ selects that branch for Render, but the external PostgreSQL, storage and secret
33
+ prerequisites still require operator setup. Local Git tracking refs are not
34
+ live provider validation, so confirm that Render can access the branch.
35
+
36
+ The [Vercel artifact](vercel/README.md) packages the Octane handler and client
37
+ assets as a web Function and a worker Function. [Vercel Functions scale down to zero](https://vercel.com/docs/functions),
38
+ so module workers run only inside ticks from Vercel Cron and from
39
+ state-changing requests. `deploy plan vercel --json` checks its source and
40
+ returns an import URL for a pushed branch. `deploy start vercel --apply`
41
+ provisions Neon PostgreSQL, a private Blob store and the stable keys through the
42
+ Vercel CLI, deploys to Production and prints a one-time token for creating the
43
+ first workspace at `/setup`.
44
+ Cloudflare's Durable Object Container API can keep a process alive, but
45
+ Flowdular has no verified adapter for its restart, secret and rollout lifecycle.
46
+
3
47
  The production artifact is the server built from `platform`. It runs as a
4
48
  non-root user and serves two public probes from
5
49
  `platform/src/server/health.ts`: `GET /api/health` answers as soon as the
@@ -11,73 +55,50 @@ background roles hold neither `SUPERUSER` nor `BYPASSRLS`, and answers 503 with
11
55
  ## Local container
12
56
 
13
57
  ```bash
14
- cp .env.example infra/docker/.env # then fill in every value
15
- docker compose -f infra/docker/compose.yaml up --build
58
+ node infra/docker/start.mjs
16
59
  ```
17
60
 
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.
61
+ The launcher creates `infra/docker/.env` with owner-only permissions, generates
62
+ PostgreSQL passwords and platform keys once, and keeps them on later runs. It
63
+ starts PostgreSQL, a private MinIO bucket and the app. When the database is
64
+ empty, it opens the first-run setup in the workstation browser and prints the
65
+ one-time setup token in the terminal. Use `--no-open` on a headless host. The
66
+ normal address is `http://localhost:3000`; change `FD_PORT` in the Docker env
67
+ file to use another port. The container binds to `127.0.0.1` by default. A
68
+ generated Compose project name keeps this app's volumes separate from other
69
+ Flowdular projects on the same machine.
70
+
71
+ The database and object store are connected before the setup page appears, so
72
+ the wizard starts with workspace and owner details. After confirmation, Compose
73
+ restarts the app into the sign-in screen. AI models are optional and can be
74
+ connected in Providers after signing in. Mail remains off until the operator
75
+ sets `FD_AUTH_MAIL_TRANSPORT=smtp`, `FD_AUTH_SMTP_URL` and `FD_AUTH_MAIL_FROM`.
76
+
77
+ To run Compose directly, copy `infra/docker/.env.example` to
78
+ `infra/docker/.env`, fill every required secret, set
79
+ `FD_AUTH_SECURE_COOKIE=false` for local HTTP, and run
80
+ `docker compose --env-file infra/docker/.env -f infra/docker/compose.yaml up --build`.
81
+ The setup token is in the `app` container logs. Keep that token out of URLs and
82
+ shared logs. For remote access, configure a TLS proxy, `FD_AUTH_PUBLIC_ORIGIN`,
83
+ `FD_BIND_ADDRESS` and secure cookies.
84
+
85
+ Compose gives the PostgreSQL runtime and background roles neither `SUPERUSER`
86
+ nor `BYPASSRLS`, and the migrator owns the schema. Its TLS certificate is
87
+ created once and verified by the app. The image ships the bundled server and a
88
+ small entrypoint that URL-encodes database passwords before starting it.
89
+
90
+ Back up the PostgreSQL data and WAL volumes, the MinIO object volume, and
91
+ `infra/docker/.env` together. The env file holds the keys needed to decrypt
92
+ records and objects after restore. `infra/docker/pitr.sh` can create a base
93
+ backup and replay archived PostgreSQL WAL segments; run its `--help` before a
94
+ restore, which requires explicit confirmation and stops the app when requested.
42
95
 
43
96
  ## Migrations on rollout
44
97
 
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.
98
+ Module migrations run through the migration role on first use. Applied
99
+ migrations are immutable: add a numbered migration rather than editing one
100
+ that is already in the ledger. Check migration status with the workspace CLI
101
+ before a production rollout, and verify it afterwards.
81
102
 
82
103
  ## Published image
83
104
 
@@ -93,9 +114,9 @@ kubectl create secret generic flowdular-secrets \
93
114
  --from-literal=workflowsPayloadKey="$(openssl rand -base64 32)" \
94
115
  --from-literal=workflowsCursorKey="$(openssl rand -base64 32)"
95
116
  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'
117
+ --from-literal=migratorUrl='postgresql://flowdular_migrator:...@postgres:5432/flowdular' \
118
+ --from-literal=runtimeUrl='postgresql://flowdular_runtime:...@postgres:5432/flowdular' \
119
+ --from-literal=backgroundUrl='postgresql://flowdular_background:...@postgres:5432/flowdular'
99
120
  kubectl apply -k infra/kubernetes
100
121
  ```
101
122
 
@@ -0,0 +1,71 @@
1
+ # node infra/docker/start.mjs creates infra/docker/.env automatically.
2
+ # To use docker compose directly, copy this file and fill every required secret.
3
+ # Never commit the copy. Generate encryption keys with: openssl rand -base64 32
4
+ # Set a different project name for each app so their named volumes cannot mix.
5
+ COMPOSE_PROJECT_NAME=
6
+ FD_AGENT_CREDENTIAL_KEY=
7
+ FD_AGENT_CREDENTIAL_KEY_PREVIOUS=
8
+ FD_AGENT_RUN_GRANT_KEY=
9
+ FD_AUTH_MFA_KEY=
10
+ FD_AUTH_MFA_KEY_PREVIOUS=
11
+ FD_APPROVAL_GRANT_KEY=
12
+ FD_APPROVAL_GRANT_KEY_PREVIOUS=
13
+ FD_AUTOMATIONS_CREDENTIAL_KEY=
14
+ FD_AUTOMATIONS_CREDENTIAL_KEY_PREVIOUS=
15
+ FD_NOTIFICATIONS_SECRET_KEY=
16
+ FD_NOTIFICATIONS_SECRET_KEY_PREVIOUS=
17
+ FD_WORKFLOWS_PAYLOAD_KEY=
18
+ FD_WORKFLOWS_PAYLOAD_KEY_PREVIOUS=
19
+ FD_WORKFLOWS_CURSOR_KEY=
20
+ FD_WORKFLOWS_CURSOR_KEY_PREVIOUS=
21
+ FD_STORAGE_ENCRYPTION_KEY=
22
+ FD_STORAGE_ENCRYPTION_KEY_PREVIOUS=
23
+ FD_CONNECTORS_SECRET_KEY=
24
+ FD_CONNECTORS_SECRET_KEY_PREVIOUS=
25
+ FD_AUDIT_ANCHOR_KEY=
26
+ FD_AUDIT_ANCHOR_KEY_PREVIOUS=
27
+
28
+ # The default S3-compatible store is the MinIO service in compose.yaml, with a
29
+ # private flowdular bucket. Override these values for an external S3 service.
30
+ # Back up the MinIO object volume with PostgreSQL and this .env file.
31
+ FD_MINIO_ROOT_PASSWORD=
32
+ FD_STORAGE_S3_BUCKET=
33
+ FD_STORAGE_S3_REGION=
34
+ FD_STORAGE_S3_ENDPOINT=
35
+ FD_STORAGE_S3_ACCESS_KEY_ID=
36
+ FD_STORAGE_S3_SECRET_ACCESS_KEY=
37
+ FD_STORAGE_S3_FORCE_PATH_STYLE=true
38
+
39
+ # PostgreSQL credentials. The superuser only bootstraps the cluster; the app
40
+ # connects as flowdular_runtime and migrates as flowdular_migrator. Newly created
41
+ # passwords are URL-safe hex. Existing passwords are kept as-is.
42
+ FD_POSTGRES_SUPERUSER_PASSWORD=
43
+ FD_DATABASE_MIGRATOR_PASSWORD=
44
+ FD_DATABASE_RUNTIME_PASSWORD=
45
+ FD_DATABASE_BACKGROUND_PASSWORD=
46
+
47
+ # Outgoing mail. With none, workspace invitations are refused and password reset
48
+ # messages are never delivered. Set smtp and both values below to send them; the
49
+ # URL carries the relay password, so treat this file as a secret.
50
+ FD_AUTH_MAIL_TRANSPORT=none
51
+ FD_AUTH_SMTP_URL=
52
+ FD_AUTH_MAIL_FROM=
53
+
54
+ # Host port for the published container port 3000.
55
+ FD_PORT=3000
56
+ FD_BIND_ADDRESS=127.0.0.1
57
+ FD_APPLICATION_PATH=/app
58
+ FD_AUTH_PUBLIC_ORIGIN=
59
+
60
+ # The local launcher writes false for HTTP on a workstation. For a deployment
61
+ # behind TLS, set true and set FD_AUTH_PUBLIC_ORIGIN to its HTTPS origin.
62
+ FD_AUTH_SECURE_COOKIE=
63
+
64
+ # Tenant id that overrides the operator workspace, the one that changes platform
65
+ # settings such as the branding and the mail relay. Empty, the workspace auth.core
66
+ # recorded at first-run setup decides (pnpm flowdular auth operator shows it).
67
+ FD_OPERATOR_TENANT=
68
+
69
+ # Prometheus exposition on GET /api/metrics. Leave FD_METRICS unset to keep it off.
70
+ FD_METRICS=false
71
+ FD_METRICS_TOKEN=
@@ -7,9 +7,18 @@ WORKDIR /workspace
7
7
 
8
8
  RUN corepack enable && corepack prepare pnpm@11.17.0 --activate
9
9
 
10
+ # The image only builds: tests and checks run in CI before a tag is cut. The
11
+ # package store is fetched from the lockfile alone, so this layer is reused
12
+ # until the lockfile changes.
13
+ COPY pnpm-lock.yaml pnpm-workspace.yaml ./
14
+ RUN pnpm fetch
10
15
  COPY . .
11
- RUN pnpm install --frozen-lockfile
12
- RUN pnpm verify && pnpm build
16
+ RUN pnpm install --frozen-lockfile --offline
17
+ RUN pnpm build
18
+ RUN mkdir -p module-plans \
19
+ && if [ ! -f flowdular.modules.lock.json ]; then printf '{"schemaVersion":1,"modules":[]}\n' > flowdular.modules.lock.json; fi \
20
+ && if [ ! -f flowdular.module-sources.json ]; then printf '{"schemaVersion":1,"sources":[]}\n' > flowdular.module-sources.json; fi
21
+ RUN mkdir -p /sdk-manifests && node infra/sdk-module-manifests.mjs . /sdk-manifests
13
22
 
14
23
  FROM node:24-bookworm-slim AS runtime
15
24
 
@@ -17,21 +26,30 @@ ENV NODE_ENV=production
17
26
  ENV PORT=3000
18
27
  WORKDIR /app
19
28
 
20
- RUN groupadd --system --gid 1001 flowdular \
21
- && useradd --system --uid 1001 --gid flowdular flowdular \
29
+ RUN groupadd --system --gid 1001 octane \
30
+ && useradd --system --uid 1001 --gid octane octane \
22
31
  && mkdir -p /data \
23
- && chown flowdular:flowdular /data
32
+ && chown octane:octane /data
24
33
 
25
34
  # 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
35
+ # only node:* built-ins, so no node_modules (and no devDependencies) ship. The
36
+ # platform does read the manifests and specs of the modules @flowdular/sdk
37
+ # ships, so the last copy puts those files alone where platform/ resolves them.
38
+ COPY --from=builder --chown=octane:octane /workspace/platform/dist ./platform/dist
39
+ COPY --from=builder --chown=octane:octane /workspace/platform/package.json ./platform/package.json
40
+ COPY --chown=octane:octane infra/docker/app-entrypoint.mjs infra/docker/database-urls.mjs ./infra/docker/
41
+ COPY --from=builder --chown=octane:octane /workspace/flowdular.json ./flowdular.json
42
+ COPY --from=builder --chown=octane:octane /workspace/flowdular.modules.lock.json ./flowdular.modules.lock.json
43
+ COPY --from=builder --chown=octane:octane /workspace/flowdular.module-sources.json ./flowdular.module-sources.json
44
+ COPY --from=builder --chown=octane:octane /workspace/module-plans ./module-plans
45
+ COPY --from=builder --chown=octane:octane /workspace/modules ./modules
46
+ COPY --from=builder --chown=octane:octane /sdk-manifests/ ./
47
+
48
+ USER octane
31
49
  EXPOSE 3000
32
50
  VOLUME ["/data"]
33
51
 
34
52
  HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
35
53
  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
54
 
37
- CMD ["node", "platform/dist/server/entry.js"]
55
+ CMD ["node", "--import", "./infra/docker/app-entrypoint.mjs", "platform/dist/server/entry.js"]
@@ -0,0 +1,5 @@
1
+ import { installDatabaseUrls } from './database-urls.mjs';
2
+
3
+ if (process.env.FD_DATABASE_ADAPTER === 'postgresql') {
4
+ installDatabaseUrls(process.env);
5
+ }
@@ -5,6 +5,7 @@ services:
5
5
  volumes:
6
6
  - ./postgres/tls-init.sh:/tls-init.sh:ro
7
7
  - flowdular-postgres-tls:/tls
8
+ - flowdular-postgres-wal:/var/lib/postgresql/wal-archive
8
9
  restart: 'no'
9
10
 
10
11
  postgres:
@@ -14,10 +15,10 @@ services:
14
15
  condition: service_completed_successfully
15
16
  environment:
16
17
  POSTGRES_DB: flowdular
17
- POSTGRES_PASSWORD: ${FD_POSTGRES_SUPERUSER_PASSWORD:?Set FD_POSTGRES_SUPERUSER_PASSWORD in infra/docker/.env (openssl rand -base64 32)}
18
- FD_DATABASE_MIGRATOR_PASSWORD: ${FD_DATABASE_MIGRATOR_PASSWORD:?Set FD_DATABASE_MIGRATOR_PASSWORD in infra/docker/.env (openssl rand -base64 32)}
19
- FD_DATABASE_RUNTIME_PASSWORD: ${FD_DATABASE_RUNTIME_PASSWORD:?Set FD_DATABASE_RUNTIME_PASSWORD in infra/docker/.env (openssl rand -base64 32)}
20
- FD_DATABASE_BACKGROUND_PASSWORD: ${FD_DATABASE_BACKGROUND_PASSWORD:?Set FD_DATABASE_BACKGROUND_PASSWORD in infra/docker/.env (openssl rand -base64 32)}
18
+ POSTGRES_PASSWORD: ${FD_POSTGRES_SUPERUSER_PASSWORD:?Run node infra/docker/start.mjs or set FD_POSTGRES_SUPERUSER_PASSWORD}
19
+ FD_DATABASE_MIGRATOR_PASSWORD: ${FD_DATABASE_MIGRATOR_PASSWORD:?Run node infra/docker/start.mjs or set FD_DATABASE_MIGRATOR_PASSWORD}
20
+ FD_DATABASE_RUNTIME_PASSWORD: ${FD_DATABASE_RUNTIME_PASSWORD:?Run node infra/docker/start.mjs or set FD_DATABASE_RUNTIME_PASSWORD}
21
+ FD_DATABASE_BACKGROUND_PASSWORD: ${FD_DATABASE_BACKGROUND_PASSWORD:?Run node infra/docker/start.mjs or set FD_DATABASE_BACKGROUND_PASSWORD}
21
22
  command:
22
23
  - postgres
23
24
  - -c
@@ -26,8 +27,21 @@ services:
26
27
  - ssl_cert_file=/tls/server.crt
27
28
  - -c
28
29
  - ssl_key_file=/tls/server.key
30
+ # Point-in-time recovery: every completed WAL segment is copied into the
31
+ # archive volume, and a quiet database still ships one every five
32
+ # minutes. See infra/README.md, "Backups and PITR", for the limits of a
33
+ # plain cp archive and for the base backup this needs.
34
+ - -c
35
+ - wal_level=replica
36
+ - -c
37
+ - archive_mode=on
38
+ - -c
39
+ - archive_timeout=300
40
+ - -c
41
+ - 'archive_command=test ! -f /var/lib/postgresql/wal-archive/%f && cp %p /var/lib/postgresql/wal-archive/%f'
29
42
  volumes:
30
43
  - flowdular-postgres:/var/lib/postgresql/data
44
+ - flowdular-postgres-wal:/var/lib/postgresql/wal-archive
31
45
  - flowdular-postgres-tls:/tls:ro
32
46
  - ./postgres/10-roles.sh:/docker-entrypoint-initdb.d/10-roles.sh:ro
33
47
  healthcheck:
@@ -40,29 +54,40 @@ services:
40
54
  - no-new-privileges:true
41
55
  restart: unless-stopped
42
56
 
43
- # Rollout gate. Each module applies its own migrations on first use through a
44
- # migration lease, so this one-shot run does not apply anything: it reports
45
- # what is pending and fails the rollout when an applied migration no longer
46
- # matches the checksum of the code being deployed. It needs the workspace and
47
- # the CLI, so it runs the builder stage rather than the runtime image.
48
- migration-check:
49
- build:
50
- context: ../..
51
- dockerfile: infra/docker/Dockerfile
52
- target: builder
53
- image: flowdular-app-builder:local
54
- depends_on:
55
- postgres:
56
- condition: service_healthy
57
- command: ['pnpm', 'flowdular', 'migration', 'status', '--json']
57
+ minio:
58
+ image: minio/minio:RELEASE.2025-09-07T16-13-09Z
59
+ command: server /data --console-address :9001
58
60
  environment:
59
- FD_DATABASE_ADAPTER: postgresql
60
- FD_DATABASE_URL: postgresql://coreloom_runtime:${FD_DATABASE_RUNTIME_PASSWORD}@postgres:5432/flowdular
61
- FD_DATABASE_MIGRATOR_URL: postgresql://coreloom_migrator:${FD_DATABASE_MIGRATOR_PASSWORD}@postgres:5432/flowdular
62
- FD_DATABASE_TLS: verify-full
63
- FD_DATABASE_TLS_CA_FILE: /tls/server.crt
61
+ MINIO_ROOT_USER: ${FD_MINIO_ROOT_USER:-flowdularlocal}
62
+ MINIO_ROOT_PASSWORD: ${FD_MINIO_ROOT_PASSWORD:?Run node infra/docker/start.mjs or set FD_MINIO_ROOT_PASSWORD}
64
63
  volumes:
65
- - flowdular-postgres-tls:/tls:ro
64
+ - flowdular-objects:/data
65
+ security_opt:
66
+ - no-new-privileges:true
67
+ restart: unless-stopped
68
+
69
+ minio-init:
70
+ image: minio/mc:RELEASE.2025-08-13T08-35-41Z
71
+ entrypoint: /bin/sh
72
+ command:
73
+ - -ec
74
+ - |
75
+ for attempt in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30; do
76
+ if mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" >/dev/null 2>&1; then
77
+ mc mb --ignore-existing "local/$$FD_STORAGE_S3_BUCKET"
78
+ exit 0
79
+ fi
80
+ sleep 2
81
+ done
82
+ echo 'MinIO did not become ready within 60 seconds.' >&2
83
+ exit 1
84
+ depends_on:
85
+ minio:
86
+ condition: service_started
87
+ environment:
88
+ MINIO_ROOT_USER: ${FD_MINIO_ROOT_USER:-flowdularlocal}
89
+ MINIO_ROOT_PASSWORD: ${FD_MINIO_ROOT_PASSWORD:?Run node infra/docker/start.mjs or set FD_MINIO_ROOT_PASSWORD}
90
+ FD_STORAGE_S3_BUCKET: ${FD_STORAGE_S3_BUCKET:-flowdular}
66
91
  security_opt:
67
92
  - no-new-privileges:true
68
93
  restart: 'no'
@@ -71,11 +96,11 @@ services:
71
96
  build:
72
97
  context: ../..
73
98
  dockerfile: infra/docker/Dockerfile
74
- image: flowdular-app:local
99
+ image: flowdular:local
75
100
  depends_on:
76
101
  postgres:
77
102
  condition: service_healthy
78
- migration-check:
103
+ minio-init:
79
104
  condition: service_completed_successfully
80
105
  environment:
81
106
  NODE_ENV: production
@@ -83,29 +108,60 @@ services:
83
108
  # Every module reads and writes through the platform provider; the
84
109
  # deployment has one database and no per-module files.
85
110
  FD_DATABASE_ADAPTER: postgresql
86
- FD_DATABASE_URL: postgresql://coreloom_runtime:${FD_DATABASE_RUNTIME_PASSWORD}@postgres:5432/flowdular
87
- FD_DATABASE_MIGRATOR_URL: postgresql://coreloom_migrator:${FD_DATABASE_MIGRATOR_PASSWORD}@postgres:5432/flowdular
111
+ # The image entrypoint encodes these passwords before making PostgreSQL
112
+ # URLs. Older deployments may have generated base64 values containing /.
113
+ FD_DATABASE_RUNTIME_PASSWORD: ${FD_DATABASE_RUNTIME_PASSWORD:?Run node infra/docker/start.mjs or set FD_DATABASE_RUNTIME_PASSWORD}
114
+ FD_DATABASE_MIGRATOR_PASSWORD: ${FD_DATABASE_MIGRATOR_PASSWORD:?Run node infra/docker/start.mjs or set FD_DATABASE_MIGRATOR_PASSWORD}
88
115
  # Cross-tenant reads (the scheduler poll) run as this role. It carries no
89
116
  # blanket grant: a migration hands it named columns under its own policy.
90
- FD_DATABASE_BACKGROUND_URL: postgresql://coreloom_background:${FD_DATABASE_BACKGROUND_PASSWORD}@postgres:5432/flowdular
117
+ FD_DATABASE_BACKGROUND_PASSWORD: ${FD_DATABASE_BACKGROUND_PASSWORD:?Run node infra/docker/start.mjs or set FD_DATABASE_BACKGROUND_PASSWORD}
118
+ FD_DATABASE_URL: ${FD_DATABASE_URL:-}
119
+ FD_DATABASE_MIGRATOR_URL: ${FD_DATABASE_MIGRATOR_URL:-}
120
+ FD_DATABASE_BACKGROUND_URL: ${FD_DATABASE_BACKGROUND_URL:-}
91
121
  FD_DATABASE_TLS: verify-full
92
122
  FD_DATABASE_TLS_CA_FILE: /tls/server.crt
123
+ FD_AUTH_PUBLIC_ORIGIN: ${FD_AUTH_PUBLIC_ORIGIN:-http://localhost:${FD_PORT:-3000}}
124
+ FD_APPLICATION_PATH: ${FD_APPLICATION_PATH:-/app}
125
+ FD_SETUP_AUTO_RESTART: 'true'
93
126
  FD_AUTH_ALLOW_SIGN_UP: 'false'
127
+ # Tenant id that overrides the operator workspace, the one that changes
128
+ # platform settings such as the branding and the mail relay. Empty, the
129
+ # workspace auth.core recorded at first-run setup decides.
130
+ FD_OPERATOR_TENANT: ${FD_OPERATOR_TENANT:-}
94
131
  # TLS terminates in front of this container. Only a plain-HTTP run on a
95
132
  # workstation may set FD_AUTH_SECURE_COOKIE=false in .env.
96
133
  FD_AUTH_SECURE_COOKIE: ${FD_AUTH_SECURE_COOKIE:-true}
97
- # Every key comes from infra/docker/.env (copy the root .env.example).
98
- # Compose refuses to start without them; the owning module would refuse at
99
- # boot anyway.
100
- FD_AGENT_CREDENTIAL_KEY: ${FD_AGENT_CREDENTIAL_KEY:?Set FD_AGENT_CREDENTIAL_KEY in infra/docker/.env (openssl rand -base64 32)}
101
- FD_AGENT_RUN_GRANT_KEY: ${FD_AGENT_RUN_GRANT_KEY:?Set FD_AGENT_RUN_GRANT_KEY in infra/docker/.env (openssl rand -base64 32)}
102
- FD_AUTOMATIONS_CREDENTIAL_KEY: ${FD_AUTOMATIONS_CREDENTIAL_KEY:?Set FD_AUTOMATIONS_CREDENTIAL_KEY in infra/docker/.env (openssl rand -base64 32)}
103
- FD_NOTIFICATIONS_SECRET_KEY: ${FD_NOTIFICATIONS_SECRET_KEY:?Set FD_NOTIFICATIONS_SECRET_KEY in infra/docker/.env (openssl rand -base64 32)}
104
- FD_WORKFLOWS_PAYLOAD_KEY: ${FD_WORKFLOWS_PAYLOAD_KEY:?Set FD_WORKFLOWS_PAYLOAD_KEY in infra/docker/.env (openssl rand -base64 32)}
105
- FD_WORKFLOWS_CURSOR_KEY: ${FD_WORKFLOWS_CURSOR_KEY:?Set FD_WORKFLOWS_CURSOR_KEY in infra/docker/.env (openssl rand -base64 32)}
106
- FD_STORAGE_ENCRYPTION_KEY: ${FD_STORAGE_ENCRYPTION_KEY:?Set FD_STORAGE_ENCRYPTION_KEY in infra/docker/.env (openssl rand -base64 32)}
107
- FD_CONNECTORS_SECRET_KEY: ${FD_CONNECTORS_SECRET_KEY:?Set FD_CONNECTORS_SECRET_KEY in infra/docker/.env (openssl rand -base64 32)}
108
- FD_AUDIT_ANCHOR_KEY: ${FD_AUDIT_ANCHOR_KEY:?Set FD_AUDIT_ANCHOR_KEY in infra/docker/.env (openssl rand -base64 32)}
134
+ FD_AUTH_MFA_KEY: ${FD_AUTH_MFA_KEY:?Run node infra/docker/start.mjs or set FD_AUTH_MFA_KEY}
135
+ FD_AUTH_MFA_KEY_PREVIOUS: ${FD_AUTH_MFA_KEY_PREVIOUS:-}
136
+ # Outgoing mail. none refuses invitations and delivers no password reset
137
+ # message; smtp requires the URL and the sender below.
138
+ FD_AUTH_MAIL_TRANSPORT: ${FD_AUTH_MAIL_TRANSPORT:-none}
139
+ FD_TRUST_PROXY: ${FD_TRUST_PROXY:-false}
140
+ FD_METRICS: ${FD_METRICS:-false}
141
+ FD_METRICS_TOKEN: ${FD_METRICS_TOKEN:-}
142
+ FD_AUTH_SMTP_URL: ${FD_AUTH_SMTP_URL:-}
143
+ FD_AUTH_MAIL_FROM: ${FD_AUTH_MAIL_FROM:-}
144
+ # start.mjs creates these once in infra/docker/.env. Existing values win.
145
+ FD_AGENT_CREDENTIAL_KEY: ${FD_AGENT_CREDENTIAL_KEY:?Run node infra/docker/start.mjs or set FD_AGENT_CREDENTIAL_KEY}
146
+ FD_AGENT_RUN_GRANT_KEY: ${FD_AGENT_RUN_GRANT_KEY:?Run node infra/docker/start.mjs or set FD_AGENT_RUN_GRANT_KEY}
147
+ FD_APPROVAL_GRANT_KEY: ${FD_APPROVAL_GRANT_KEY:?Run node infra/docker/start.mjs or set FD_APPROVAL_GRANT_KEY}
148
+ FD_AUTOMATIONS_CREDENTIAL_KEY: ${FD_AUTOMATIONS_CREDENTIAL_KEY:?Run node infra/docker/start.mjs or set FD_AUTOMATIONS_CREDENTIAL_KEY}
149
+ FD_NOTIFICATIONS_SECRET_KEY: ${FD_NOTIFICATIONS_SECRET_KEY:?Run node infra/docker/start.mjs or set FD_NOTIFICATIONS_SECRET_KEY}
150
+ FD_WORKFLOWS_PAYLOAD_KEY: ${FD_WORKFLOWS_PAYLOAD_KEY:?Run node infra/docker/start.mjs or set FD_WORKFLOWS_PAYLOAD_KEY}
151
+ FD_WORKFLOWS_CURSOR_KEY: ${FD_WORKFLOWS_CURSOR_KEY:?Run node infra/docker/start.mjs or set FD_WORKFLOWS_CURSOR_KEY}
152
+ FD_STORAGE_ENCRYPTION_KEY: ${FD_STORAGE_ENCRYPTION_KEY:?Run node infra/docker/start.mjs or set FD_STORAGE_ENCRYPTION_KEY}
153
+ FD_CONNECTORS_SECRET_KEY: ${FD_CONNECTORS_SECRET_KEY:?Run node infra/docker/start.mjs or set FD_CONNECTORS_SECRET_KEY}
154
+ FD_AUDIT_ANCHOR_KEY: ${FD_AUDIT_ANCHOR_KEY:?Run node infra/docker/start.mjs or set FD_AUDIT_ANCHOR_KEY}
155
+ # Previous keys are operator-managed during rotation and remain empty on a new install.
156
+ FD_AGENT_CREDENTIAL_KEY_PREVIOUS: ${FD_AGENT_CREDENTIAL_KEY_PREVIOUS:-}
157
+ FD_APPROVAL_GRANT_KEY_PREVIOUS: ${FD_APPROVAL_GRANT_KEY_PREVIOUS:-}
158
+ FD_AUTOMATIONS_CREDENTIAL_KEY_PREVIOUS: ${FD_AUTOMATIONS_CREDENTIAL_KEY_PREVIOUS:-}
159
+ FD_NOTIFICATIONS_SECRET_KEY_PREVIOUS: ${FD_NOTIFICATIONS_SECRET_KEY_PREVIOUS:-}
160
+ FD_WORKFLOWS_PAYLOAD_KEY_PREVIOUS: ${FD_WORKFLOWS_PAYLOAD_KEY_PREVIOUS:-}
161
+ FD_WORKFLOWS_CURSOR_KEY_PREVIOUS: ${FD_WORKFLOWS_CURSOR_KEY_PREVIOUS:-}
162
+ FD_STORAGE_ENCRYPTION_KEY_PREVIOUS: ${FD_STORAGE_ENCRYPTION_KEY_PREVIOUS:-}
163
+ FD_CONNECTORS_SECRET_KEY_PREVIOUS: ${FD_CONNECTORS_SECRET_KEY_PREVIOUS:-}
164
+ FD_AUDIT_ANCHOR_KEY_PREVIOUS: ${FD_AUDIT_ANCHOR_KEY_PREVIOUS:-}
109
165
  # Object storage. Production refuses the local adapter, so the app writes
110
166
  # to an S3-compatible bucket and encrypts every object under
111
167
  # FD_STORAGE_ENCRYPTION_KEY before it leaves the process. The named volume
@@ -113,23 +169,14 @@ services:
113
169
  # run outside production for a demonstration.
114
170
  FD_STORAGE_ADAPTER: ${FD_STORAGE_ADAPTER:-s3}
115
171
  FD_STORAGE_LOCAL_DIRECTORY: /var/lib/flowdular/storage
116
- FD_STORAGE_S3_BUCKET: ${FD_STORAGE_S3_BUCKET:-}
117
- FD_STORAGE_S3_REGION: ${FD_STORAGE_S3_REGION:-}
118
- FD_STORAGE_S3_ENDPOINT: ${FD_STORAGE_S3_ENDPOINT:-}
119
- FD_STORAGE_S3_ACCESS_KEY_ID: ${FD_STORAGE_S3_ACCESS_KEY_ID:-}
120
- FD_STORAGE_S3_SECRET_ACCESS_KEY: ${FD_STORAGE_S3_SECRET_ACCESS_KEY:-}
121
- FD_STORAGE_S3_FORCE_PATH_STYLE: ${FD_STORAGE_S3_FORCE_PATH_STYLE:-false}
122
- # Optional: without it the app runs and only MFA enrollment is unavailable.
123
- FD_AUTH_MFA_KEY: ${FD_AUTH_MFA_KEY:-}
124
- # Outgoing mail. With none, invitations and password resets are refused.
125
- FD_AUTH_MAIL_TRANSPORT: ${FD_AUTH_MAIL_TRANSPORT:-none}
126
- FD_TRUST_PROXY: ${FD_TRUST_PROXY:-false}
127
- FD_METRICS: ${FD_METRICS:-false}
128
- FD_METRICS_TOKEN: ${FD_METRICS_TOKEN:-}
129
- FD_AUTH_SMTP_URL: ${FD_AUTH_SMTP_URL:-}
130
- FD_AUTH_MAIL_FROM: ${FD_AUTH_MAIL_FROM:-}
172
+ FD_STORAGE_S3_BUCKET: ${FD_STORAGE_S3_BUCKET:-flowdular}
173
+ FD_STORAGE_S3_REGION: ${FD_STORAGE_S3_REGION:-us-east-1}
174
+ FD_STORAGE_S3_ENDPOINT: ${FD_STORAGE_S3_ENDPOINT-}
175
+ FD_STORAGE_S3_ACCESS_KEY_ID: ${FD_STORAGE_S3_ACCESS_KEY_ID:-${FD_MINIO_ROOT_USER:-flowdularlocal}}
176
+ FD_STORAGE_S3_SECRET_ACCESS_KEY: ${FD_STORAGE_S3_SECRET_ACCESS_KEY:-${FD_MINIO_ROOT_PASSWORD:?Run node infra/docker/start.mjs or set FD_MINIO_ROOT_PASSWORD}}
177
+ FD_STORAGE_S3_FORCE_PATH_STYLE: ${FD_STORAGE_S3_FORCE_PATH_STYLE:-true}
131
178
  ports:
132
- - '${FD_PORT:-3000}:3000'
179
+ - '${FD_BIND_ADDRESS:-127.0.0.1}:${FD_PORT:-3000}:3000'
133
180
  healthcheck:
134
181
  test:
135
182
  - CMD
@@ -153,6 +200,10 @@ services:
153
200
 
154
201
  volumes:
155
202
  flowdular-postgres:
203
+ # WAL segments and base backups for point-in-time recovery (infra/docker/pitr.sh).
204
+ flowdular-postgres-wal:
156
205
  flowdular-postgres-tls:
157
206
  # Only the local adapter writes here; a production run uses the S3 bucket.
158
207
  flowdular-storage:
208
+ # Local S3-compatible storage; back up this volume alongside PostgreSQL.
209
+ flowdular-objects:
@@ -0,0 +1,28 @@
1
+ /** Build local PostgreSQL URLs after Compose has injected raw passwords. */
2
+ export function installDatabaseUrls(environment) {
3
+ const connections = [
4
+ ['FD_DATABASE_URL', 'FD_DATABASE_RUNTIME_PASSWORD', 'flowdular_runtime'],
5
+ [
6
+ 'FD_DATABASE_MIGRATOR_URL',
7
+ 'FD_DATABASE_MIGRATOR_PASSWORD',
8
+ 'flowdular_migrator',
9
+ ],
10
+ [
11
+ 'FD_DATABASE_BACKGROUND_URL',
12
+ 'FD_DATABASE_BACKGROUND_PASSWORD',
13
+ 'flowdular_background',
14
+ ],
15
+ ];
16
+ for (const [urlKey, passwordKey, role] of connections) {
17
+ if (!environment[urlKey]?.trim()) {
18
+ const password = environment[passwordKey];
19
+ if (!password)
20
+ throw new Error(
21
+ `${passwordKey} is required for the local PostgreSQL service.`,
22
+ );
23
+ environment[urlKey] =
24
+ `postgresql://${role}:${encodeURIComponent(password)}@postgres:5432/flowdular`;
25
+ }
26
+ delete environment[passwordKey];
27
+ }
28
+ }