Monorepo development
Develop a built-in application inside the Cloud monorepo.
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
bun install --frozen-lockfile
bun run devOpen 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:
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=58123The 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.
APP_URL=https://cloud.dev.exampleTest 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:
- Serve the stack through an HTTPS reverse proxy or tunnel, as described in Serve the stack under another address.
- Make sure the
app.urlsetting is that HTTPS address.APP_URLsets it only while no value is stored; otherwise, change the URL in Administration → Settings. Pairing and renewal accept only requests whoseOriginis this address. - Use the same address on the computer and on the phone. Pair the phone from
/me/appon 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:
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 checkThe 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):
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_URLThe 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:
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 statusUse 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:
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 --jsonThe 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
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:
bun run dev:cld -- apps list --json
bun run dev:cld -- notebooks lsThe 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:
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}' --yesReuse 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.
Preview documentation links locally
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:
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:
DEVholds the application streams. Applications connect without credentials.TESTholds 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 ofDEV.$SYSserves 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
bun run check
bun run testBefore 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:
bun run --cwd packages/grids typecheck
bun run test --filter packages/gridsSee 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:
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 --integrationCLOUD_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:
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:
docker compose -f compose.dev.yml exec -T app-core bun packages/core/scripts/sync-dev-smoke.tsThis 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.