Cloud|

Monorepo development

Develop a built-in application inside the Cloud monorepo.

15 min read Updated 2026-10-17 #development#monorepo#docker

Use the monorepo when you change the platform or a built-in application.

Docker Compose runs infrastructure and application services. Source folders are mounted into the containers. Restart the affected services after edits, as described below.

Start the core stack

bash
bun install --frozen-lockfile
bun run dev

Open http://localhost:3000.

The local administrator login is /auth/login?method=admin with token dev-admin.

bun run dev starts Postgres, Valkey, a second Valkey for integration tests, a persistent single-node NATS JetStream server, Geo, Filegate, Gotenberg, and ClickHouse in the background. It then stays in the foreground and runs the gateway, Gateway Ops, Core, Dashboard, Accounts, Assistant, and the mobile app shell (app-pwa).

Use bun run dev:full only when you need every optional application.

bun run dev:down removes the application stack but keeps the infrastructure available for quick restarts. Stop the infrastructure explicitly with docker compose -f compose.yml down when it is no longer needed.

Root command What it does
bun run dev Start the infrastructure and the core services
bun run dev:full Start the infrastructure, core, and every optional application
bun run dev:down Remove the application stack, keep the infrastructure
bun run dev:start, dev:stop, dev:restart, dev:rebuild Manage individual applications
bun run dev:logs, dev:status, dev:help Inspect the stack and list commands
bun run dev:cld -- <args> Run the checkout's CLI against the local gateway
bun run check Repository rules, formatting, and every package typecheck
bun run format Format the workspace
bun run test [--integration] [--shard n/m] Run tests; see Testing
bun run release:preflight Check a running fleet against a release; see Build and deploy

Change host ports

The development stack publishes every port on 127.0.0.1 only. Other machines cannot reach it; for remote access, use an SSH tunnel or a reverse proxy.

If another Postgres, Valkey, NATS, or ClickHouse already uses its default port on the same host, move the host port before bun run dev. Set the variables in your shell or in the checkout's .env, which Compose reads:

bash
CLOUD_DEV_POSTGRES_PORT=55432
CLOUD_DEV_VALKEY_PORT=56379
CLOUD_DEV_TEST_VALKEY_PORT=56380
CLOUD_DEV_NATS_PORT=54222
CLOUD_DEV_CLICKHOUSE_PORT=58123

The defaults are 5432, 6379, 6380, 4222, and 8123. Containers still talk to each other on the default ports, so only host-side clients change: use the new ports in DATABASE_URL, REDIS_URL, NATS_SERVERS, and the CLOUD_TEST_* URLs.

Serve the stack under another address

When the stack is reached through a reverse proxy or tunnel, for example https://cloud.dev.example, set that address as APP_URL in the checkout's .env before bun run dev. Cloud uses it for links, OAuth redirects, and the same-origin checks on sign-in; with the default localhost:3000, signing in through the other address fails when the terms are accepted. Development Filegate also accepts browser requests from this origin. bun run test keeps .env away from tests; see Testing.

bash
APP_URL=https://cloud.dev.example

Test the mobile app on a phone

The mobile app is unreleased. Its shell (app-pwa) runs in the core development stack, while compose.prod.yml starts it only with the unreleased profile.

A phone cannot reach localhost, and browsers install apps and register service workers only over HTTPS. To try the app on a phone:

  1. Serve the stack through an HTTPS reverse proxy or tunnel, as described in Serve the stack under another address.
  2. Make sure the app.url setting is that HTTPS address. APP_URL sets it only while no value is stored; otherwise, change the URL in Administration → Settings. Pairing and renewal accept only requests whose Origin is this address.
  3. Use the same address on the computer and on the phone. Pair the phone from /me/app on the computer, or open /pwa/ on the phone to install the app first.

Connect Pulse to development ClickHouse

compose.yml runs ClickHouse 26.9.19.9 with its own pulse_clickhouse_data volume. ensureInfra() prepares .local/clickhouse/ with mode 0700 and creates credentials.env (admin credentials) and pulse_service.password once, both mode 0600. Existing values are never overwritten or printed. To prepare just these files, use bun scripts/dev-clickhouse.ts (--help explains it).

Once the stack and Pulse are running and the workspace CLI is signed in as a Cloud admin, run the setup once. The client inherits CLICKHOUSE_USER and CLICKHOUSE_PASSWORD inside the container, so the admin password is absent from shell arguments and host output:

bash
set -o pipefail
bun run dev:cld -- pulse admin store setup-sql --password-file .local/clickhouse/pulse_service.password |
  docker compose -f compose.yml exec -T clickhouse clickhouse-client --multiquery
bun run dev:cld -- pulse admin store set --url http://clickhouse:8123 --password-file .local/clickhouse/pulse_service.password
bun run dev:cld -- pulse admin store check

The address saved by Pulse uses the container network. Host-side integration tests use loopback instead. Load the local admin credentials into an admin URL without printing them (leave shell tracing off):

bash
set +x
set -a
. ./.local/clickhouse/credentials.env
set +a
export CLOUD_TEST_CLICKHOUSE_URL="http://${CLICKHOUSE_USER}:${CLICKHOUSE_PASSWORD}@127.0.0.1:${CLOUD_DEV_CLICKHOUSE_PORT:-8123}"
unset CLICKHOUSE_USER CLICKHOUSE_PASSWORD CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT
bun run test --integration --filter packages/pulse
unset CLOUD_TEST_CLICKHOUSE_URL

The fixture uses pulsetest_<8 hex>_ per process, not the saved pulse_ connection. It removes only that namespace's databases and access objects. Connecting is optional in this release; existing Pulse features stay on Postgres.

Test Filegate locally

Development infrastructure includes Filegate 7.0.0 for Files (filesv2) development. To prepare its backend token and configuration and start only Filegate:

bash
bun scripts/dev-filegate.ts
docker compose -f compose.yml up -d --no-deps --wait filegate
docker compose -f compose.yml exec filegate /app/filegate status

Use http://filegate:4000 from Cloud containers and http://127.0.0.1:4000 from the host. Direct transfer leases use the latter address. Open Cloud at http://localhost:3000 so its cookies do not share Filegate's host; ports alone do not isolate cookies. Browser CORS permits http://localhost:3000, http://127.0.0.1:3000, and the origin of APP_URL. Only the loopback interface publishes Filegate's port, so previews and transfers work only in a browser on the development machine.

The configuration is generated in the Git-ignored .local/filegate/conf.yaml, with its digest in .local/filegate/conf.env. Filegate reads the configuration only on start. When it changes, for example after a new APP_URL, the next docker compose up recreates the container, whether you run it yourself or bun run dev runs it.

The backend token is stored in the Git-ignored .local/filegate/token file with mode 0600. Repeated setup retains it. Supply it only to the application backend; never include it in browser code or public links.

Root Container path Index Version history Managed Unix execution
cloud /data/cloud On On On Off
freeipa /data/freeipa Off Off Off On

Each root and Filegate's state have separate persistent Docker volumes. The freeipa root is local test storage, not an NFS mount or a FreeIPA server. Both capability combinations are deliberate test fixtures, not provider rules. Because cloud is indexed and managed, Filegate reports stable file IDs (stableIds) for it; freeipa has none. The volumes need no migration when the image changes; the next docker compose up recreates the container. The daemon limits individual test uploads to 1 GiB.

This development container runs as root with only CHOWN, DAC_OVERRIDE, FOWNER, FSETID, SETUID, and SETGID capabilities to test Unix execution, numeric ownership, setgid, and POSIX ACLs. It is not a production privilege recommendation or proof of NFS behavior. The published image uses linux/amd64; Docker Desktop uses emulation on ARM.

Test Collabora locally

Development infrastructure also includes Collabora Online (CODE) for Files document editing. It listens on http://localhost:9980 for browsers, while Collabora itself reaches Cloud through the gateway service name. Point Files at it with the administrator CLI:

bash
bun run dev:cld -- filesv2 admin configuration get --json > filesv2.json
# add to filesv2.json:
#   "collabora": { "url": "http://localhost:9980", "internalUrl": "http://collabora:9980",
#                  "wopiOrigin": "http://gateway:3000", "documentFormat": "odf" }
bun run dev:cld -- filesv2 admin configuration set --input-file filesv2.json --json

The container allows gateway:3000 as WOPI host and localhost:3000 as frame ancestor, runs without TLS and without the jail mount helper, and has the welcome dialog, admin console and AI features disabled. These are development settings, not production ones.

Work on one application

bash
bun run dev:start grids
bun run dev:logs grids
bun run dev:status grids
Command Result
dev:start <app...> Starts existing images and waits until the applications are ready
dev:stop <app...> Stops containers without removing them
dev:restart <app...> Reloads mounted source with existing images and waits until ready
dev:restart --running Reloads running Cloud services one at a time with existing images
dev:rebuild <app...> Rebuilds applications and waits until they are ready
dev:logs <app> Follows one application log
dev:status [app] Shows stack or application status
dev:help Lists commands and application names
dev:down Removes the development stack

Development containers do not watch the bind-mounted source tree. Refresh only the boundary changed by the task:

Changed source Refresh
packages/<app>/src or packages/core/src Restart the owning application
packages/gateway/src Restart gateway and gateway-ops
packages/cloud/src, packages/cloud/scripts, or root styles.css Restart all running Cloud services
packages/ui/src or packages/ssr/src Rebuild only the consumers needed for the task
Dependencies, package manifests, or Dockerfiles Rebuild affected applications

dev:restart recreates containers with their existing image so current mounts and Compose commands apply, then waits for direct readiness. It never builds an image. dev:restart --running does not include Postgres, Valkey, or the other infrastructure services because those use the separate infrastructure Compose file. It restarts services one at a time to bound startup CPU and memory.

A command that exits successfully has observed each requested application's direct /_cloud/ready endpoint. dev:status reports ready, starting, or unhealthy; a merely running container is not considered ready.

Use the current CLI

Run the CLI from this checkout when testing the development server:

bash
bun run dev:cld -- apps list --json
bun run dev:cld -- notebooks ls

The alias executes packages/cloud-cli/src/index.ts and targets http://localhost:3000 by default. Pass another --server when the development gateway uses a different origin.

The development CLI keeps its own configuration under .local/cld/ in the checkout: profiles, sign-ins, plugins, and locks. It never reads or changes the installed cld configuration, and it writes no agent skill until you add a target with bun run dev:cld -- skills add <directory>. Sign in once with bun run dev:cld -- login. In the browser that opens, use the local administrator login with token dev-admin described above. Delete .local/cld/ to start over.

Do not use an installed cld for development verification because its release may lag behind the checkout. Use the installed CLI when operating a deployed Cloud installation.

Test app sign-in locally

Use the development Cloud at http://localhost:3000 and the authenticator at http://localhost:4178. Configure only your development instance:

bash
bun run dev:cld -- admin app-sign-in config get --json
bun run dev:cld -- admin app-sign-in config set --config '{"enabled":true,"origin":"http://localhost:4178","adminPairing":false}' --yes

Reuse the authenticator server if it is running, or start it with bun run --cwd pwas/pwa-auth dev. Follow Pair a device and Sign in with the app. Use a separate browser profile for the login being approved, and keep your working recovery method.

Use localhost, not a numeric IP, for passkey development. A phone's localhost does not reach your laptop: phone testing needs reachable HTTPS addresses for both websites. The development PWA does not register a service worker; use a production build to check installation and offline startup.

The documentation development server uses Bun hot reloading. After startup, transient source errors keep the last working HTTP handler available until a successful reload. The UI build marker still gates initial asset loading.

Start or refresh the local Fibel server from docs-site/compose.yml and point your development Cloud at it:

bash
docker compose -f docs-site/compose.yml up --build -d --wait --renew-anon-volumes
bun run dev:cld -- admin documentation set --url http://localhost:4187 --yes

--renew-anon-volumes replaces the container's node_modules volume, so a dependency update takes effect instead of the previous install being reused.

Reload Administration and open a Documentation link. Use the actual Fibel port if it differs. This address works only for browsers on the development machine. See local documentation MCP for server and agent setup.

Manage dependencies

Declare every dependency in the workspace that imports it. The isolated Bun linker intentionally prevents one package from relying on another package's installation.

Shared versions live in the root workspace catalog and private packages refer to them with catalog:. Keep one-off dependencies exact in the owning package. Published packages use concrete versions because their npm artifacts must not contain workspace catalog references; their peer dependencies remain explicit compatibility ranges.

bun install applies the three-day release-age gate when it resolves a new npm version. The first-party @k2b/fibel, @k2b/nessi, @k2b/stdlib, @k2b/sync, and its pinned NATS client packages are the exceptions so a coordinated Cloud update can use a new release immediately. @k2b/ssr and @k2b/ui are workspace packages and never come from npm here. Dependency lifecycle scripts are denied by default. Add no trusted package without verifying why its install script is required.

Commit the updated bun.lock with the manifest change; bun run check verifies both. See Dependency policy for root overrides and patches.

Use one Compose network

The development files use the implicit Compose project name. In the standard checkout, that name is cloud.

Applications resolve infrastructure by container name. Changing the Compose project name or passing a different -p value can put services on different networks.

Only the gateway publishes a host port. Do not publish each application.

Configure local NATS accounts

bun run dev prepares three NATS accounts under .local/nats before starting infrastructure:

  • DEV holds the application streams. Applications connect without credentials.
  • TEST holds the streams of integration tests. Tests authenticate with .local/nats/test.creds. The account may reserve at most 50 GiB of JetStream storage: enough for four parallel integration runs at their peak of about 8 GiB each, plus the streams of a killed test process. Test runs, and the streams they leave behind, therefore cannot exhaust the storage of DEV.
  • $SYS serves the Gateway Ops diagnostics. Its seed is mounted only in Gateway Ops.

NATS enables JetStream only when the TEST limit fits into its storage, by default 75% of the free disk space of its volume at startup. Keep about 67 GiB free for Docker; otherwise the NATS container stops with insufficient storage resources available.

The seeds and test.creds stay outside Git. Before invoking infrastructure Compose directly, run bun packages/gateway-ops/scripts/dev-nats.ts.

Application streams used to live in the global $G account. The first start of the NATS container with these accounts moves the existing JetStream store into DEV, so the streams keep their data.

Add a built-in application

A new application touches exactly three files: the workspaces.packages list in the root package.json, its service in compose.dev.yml, and its service in compose.prod.yml. Image names, the release set, and the CI matrix derive from scripts/workspace.ts; the app-set check in bun run check fails when the three files disagree.

The development service needs:

  • the shared environment;
  • APP_ID;
  • the Cloud source and script mounts;
  • its own source mount;
  • the shared stylesheet;
  • the Cloud preload script and Bun start command.

An HTTP application registers itself at startup. The gateway discovers it from the shared registry.

An application that is built on main before its release gets profiles: ["unreleased"] on its compose.prod.yml service. Its image is built and published, but a plain docker compose up, the release smoke, and the upgrade preflight leave it out. Compose starts it only with --profile unreleased or when the command names the service, and both are for test installations. Until its release it has no row in the deployment requirements and no catalog page. Chat (app-chat) is unreleased this way; in development it is an optional application, so start it with bun run dev:start chat or bun run dev:full. The mobile app (app-pwa) is unreleased the same way but starts with the default development stack. The release pull request removes the profile line.

A worker without HTTP routes should be a separate service. It should not register application routes.

Run checks

bash
bun run check
bun run test

Before a pull request, the focused package checks below are usually enough; see What CI runs.

bun run check runs the repository rules under scripts/checks/ (dependencies, import boundaries, package cycles, service API contracts, localization, capability presentation, CSS architecture, access editor mounting (access-editor-mounts), the application set, Help writing, formatting) and every package typecheck. A new rule is one module under scripts/checks/.

Typecheck runs on TypeScript 7, which has no stable compiler API before 7.1. Code that parses or checks TypeScript source, such as these rules, imports @typescript/typescript6; the main typescript entry exports only its version. The typescript package also no longer contains the tsserver that an editor's workspace TypeScript version used. Use an editor with TypeScript 7 support, such as the TypeScript 7 extension for VS Code, so its diagnostics match bun run check.

The root test command runs every workspace in a separate process. It uses each package's test script when one exists, preserving package-specific builds and preloads. Workspaces without a test script and root-owned tests still run in isolated Bun test processes. Each workspace's *.behavior.test.* files run in one more process with browser conditions and the Solid DOM preload; see Testing.

For a focused package:

bash
bun run --cwd packages/grids typecheck
bun run test --filter packages/grids

See Frontend testing for browser-facing checks.

Run integration checks

Integration suites gate themselves on CLOUD_TEST_* variables through scripts/fixtures/test-infra.ts. Without them, bun run test skips the integration files and reports which ones. With them, the suites run and fail loudly when a target is unreachable:

bash
CLOUD_TEST_DATABASE_URL='postgres://postgres:postgres@127.0.0.1:5432/cloud_{checkout}_test' \
CLOUD_TEST_NATS_SERVERS=nats://127.0.0.1:4222 \
CLOUD_TEST_NATS_CREDS_FILE=/path/to/cloud/.local/nats/test.creds \
CLOUD_TEST_VALKEY_URL=redis://127.0.0.1:6380 \
bun run test --integration

CLOUD_TEST_NATS_CREDS_FILE puts the tests into the TEST account (Configure local NATS accounts). It must be the absolute path of .local/nats/test.creds in the checkout that runs the development stack, also when the tests run from a worktree. Without it, a test connection lands in DEV, and the fixture refuses to run.

CLOUD_TEST_VALKEY_URL points at the tests' own Valkey on port 6380, not at the applications' Valkey on 6379. Cache keys such as settings:<key> carry no namespace, so a shared instance would mix the running stack's cached settings, for example app.url, with the tests' values. The test Valkey keeps no data across restarts.

If you changed the host ports, use them in these URLs. The expanded database name must end in _test; the fixture refuses anything else so a run can never touch the development database. {checkout} gives each worktree its own database, so forward-only migrations from one branch do not change another branch's test schema. bun run test --integration creates the database if missing before applying migrations; the PostgreSQL role needs permission to create databases. See Run integration tests for the database name token rules.

Files (filesv2), Grids PDF rendering, and Assistant Studio suites additionally read CLOUD_TEST_FILEGATE_URL, CLOUD_TEST_GOTENBERG_URL, and CLOUD_TEST_RSQL_URL. Testing lists every variable and what the pull request gate and the nightly run execute.

Mail's schema is a single baseline in packages/mail/src/schema.sql; its runner knows exactly one version, 1 (baseline). A development database created by the previous 1..126 migration chain is refused with an explicit error. Reset such a machine once with:

bash
psql "$DATABASE_URL" -c 'DROP SCHEMA mail CASCADE;'

Run Sync integration checks

The Compose cluster exposes NATS on 127.0.0.1:4222 (or CLOUD_DEV_NATS_PORT) and monitoring on 127.0.0.1:8222. Host-side clients set NATS_IGNORE_CLUSTER_UPDATES=true so they keep using the reachable seed address. Containers use the three ipa_nats_1 through ipa_nats_3 addresses instead.

After a dependency change, run bun install --frozen-lockfile and rebuild the affected applications. Restarting mounted source alone does not refresh the container's installed packages. Verify /_cloud/ready, an actual application route, and background job or schedule execution. A container marked healthy is only the first check.

With the full development stack running, verify the fleet inventory, Sync resources, schedules, authorization, and admin pages over HTTP:

bash
docker compose -f compose.dev.yml exec -T app-core bun packages/core/scripts/sync-dev-smoke.ts

This local-only check creates a temporary test account and session and removes them afterward. It does not invoke application jobs or provider operations.

For isolated broker recovery, run packages/cloud/scripts/sync-recovery-smoke.ts with prepare, then recover using the same unique --namespace cloud-recovery-smoke-<suffix>. Leave NATS running across the printed minute boundary. The check verifies a retained job, a missed scheduled tick, and their acknowledgments, then removes its own broker resources. It can bracket a full application restart, but does not replace recovery tests for each application's domain work.

K2B package and image migration

The repository is now k2b-dev/cloud. Update an existing checkout with git remote set-url origin https://github.com/k2b-dev/cloud.git.

Application releases use @k2b/cloud. When migrating an existing application, update its package dependency and all Cloud import prefixes together. @k2b/ui has its own package and independent release cycle.

Cloud-owned production images use ghcr.io/k2b-dev/cloud-*. Deploy one complete release with CLOUD_IMAGE_TAG=vX.Y.Z; retain the previous image digests until the upgraded installation is verified. External Filegate and Geo images keep their own repositories and release paths.

The CLI installer and updater verify releases signed by k2b-dev/cloud/.github/workflows/release.yml. No CLI GitHub releases existed under the former repository owner at transfer time.

Single-node development JetStream

The local stack uses ipa_nats_1 with SYNC_REPLICAS=1. Production keeps the platform default of three replicas. Set the same replica count on every process in one installation. A different replica count does not migrate existing streams; Sync reports configuration drift rather than silently rewriting durable state.

Before moving an existing three-node dev installation, stop the application fleet, back up all streams with their consumers, and restore into the new ipa_nats_data volume with one replica. Convert explicitly replicated consumer configurations to one replica as well. Preserve stream metadata, sequences and durable consumer acknowledgment state; verify before restarting applications. The old ipa_nats_1_data, ipa_nats_2_data, and ipa_nats_3_data volumes must remain available until migration verification succeeds. Do not use down -v. Use a separate three-node cluster for replication and failover tests.

Application-scoped SSR builds

Development and production build islands from two SSR component roots: the selected app's src/ and Cloud's framework src/. The build scripts resolve APP_DIR before loading the app configuration. The workspace root stays the base for island IDs and development assets. Other apps and UI test fixtures are not scanned. Production builds no longer compile every app and then discard unrelated island entries. Imported dependencies still participate in the selected app's bundle.

An SSR dependency update requires rebuilding affected application images; restarting source-mounted containers alone keeps their old installed dependency.

Connect a coding agent

The Cloud skill provides compact working instructions. MCP supplies exact current documentation when details matter.

CLI command
bunx skills add https://docs.example.com

This command installs the working instructions published by this website in the selected coding agent.

For exact, current details, also connect the MCP server through one of the agent tabs.

Installation uses the open-source Vercel Skills CLI.

The skill and MCP complement each other: the skill describes workflows, while MCP supplies current documentation.