Local Development
Topology
Section titled “Topology”The Docker development stack started by pnpm dev contains:
- TimescaleDB;
- a one-shot database migration container;
platform-apiwith TypeScript build and restart watching;web-consolewith Vite HMR;- pinned NATS JetStream and the stream bootstrap;
raw-storage-worker;mqtt-ingestion-worker.
There is no mock MQTT broker. The three flow workers are not part of the default Compose topology. Start them either as local TypeScript processes for focused pipeline work or through the complete Docker flow-studio topology described below.
pnpm dev:flow-studio is the loopback-only Docker topology for visual flow
development. It adds NATS, stream bootstrap, script-sandbox-worker,
flow-worker, flow-action-worker, and Mailpit to the core API and web-console
stack. Images are built from the current checkout, so no Alpha image or remote
deployment is involved.
Requirements
Section titled “Requirements”- Node.js compatible with the root
enginesfield; - pnpm from the root
packageManagerfield; - Docker with Compose;
- Tailscale CLI connected to the development Tailnet for shared
pnpm devcommands. The loopback-onlypnpm dev:flow-studiocommand does not require Tailscale.
cp .env.example .envpnpm installpnpm devFor local visual flow and block development, use:
pnpm dev:flow-studioThe web console is available at http://127.0.0.1:5173. In another terminal,
run pnpm dev:admin after setting the four SENS_LOCAL_ADMIN_* values if the
local platform administrator does not exist yet.
The regular pnpm dev command detects the host’s Tailscale IPv4 address and
publishes each development endpoint on loopback and that exact address. The
pnpm dev:flow-studio command publishes on loopback only. Neither command uses
0.0.0.0 as a host-published bind address.
| Component | Loopback | Tailnet |
|---|---|---|
| web-console | http://127.0.0.1:5173 | http://<tailscale-ip>:5173 |
| platform-api | http://127.0.0.1:3000 | http://<tailscale-ip>:3000 |
| TimescaleDB | 127.0.0.1:5432 | <tailscale-ip>:5432 |
Use Tailnet ACLs or grants to restrict these development ports.
Lifecycle
Section titled “Lifecycle”pnpm dev:up:detachedpnpm dev:up:ingestionpnpm dev:up:ingestion:detachedpnpm dev:flow-studiopnpm dev:flow-studio:detachedpnpm dev:logspnpm dev:pspnpm dev:downpnpm dev:rebuildpnpm dev:resetpnpm docker:delete:volumespnpm dev, pnpm dev:up, and their detached variants build the required images
before starting. Docker reuses unchanged layers and automatically installs new
dependencies after package manifests, the lockfile, or a Dockerfile changes.
The NATS configuration is registered as a Compose config. Changes to
infra/docker/nats.conf therefore recreate only the NATS container on the next
start while preserving the nats-data volume and its JetStream state.
If a previously running container still reports the old JetStream storage
ceiling, recreate only NATS before running a guarded stream migration:
docker compose --project-directory . \ -f infra/docker/compose.yaml \ -f infra/docker/compose.dev.yaml \ --profile flows --profile actions \ up -d --force-recreate nats
docker compose --project-directory . \ -f infra/docker/compose.yaml \ -f infra/docker/compose.dev.yaml \ --profile flows --profile actions \ run --rm --no-deps raw-ingestion-bootstrap \ node dist/bootstrap.js --migrate-raw-stored-retentionThe migration aborts unless the legacy consumer is fully drained. Do not
remove nats-data; the forced recreation replaces the container while
preserving its volume.
Use pnpm dev:rebuild to prepare the core development image without starting
the stack. Source changes below the mounted app, service, and package directories
are watched without rebuilding.
In development mode, Vite resolves @sens-platform/contracts directly to the
mounted packages/contracts/src/index.ts entry. The production build continues
to use the package’s compiled export. This keeps contract changes watchable
without copying ignored dist/ artifacts into the development image.
pnpm dev:reset removes the local TimescaleDB volume and is destructive.
pnpm docker:delete:volumes stops the complete local Compose project and removes
all volumes declared across its profiles. This currently deletes both
timescaledb-data and nats-data, but no volumes belonging to other Docker
Compose projects. The command leaves .env and every other local configuration
file unchanged.
The next pnpm dev recreates empty volumes and runs the database migrations.
Because the previous platform administrator was stored in the deleted database,
run pnpm dev:admin afterward to recreate it from the unchanged
SENS_LOCAL_ADMIN_* values in .env.
Local administrator
Section titled “Local administrator”Set these values only in the ignored .env:
SENS_LOCAL_ADMIN_USERNAMESENS_LOCAL_ADMIN_EMAILSENS_LOCAL_ADMIN_DISPLAY_NAMESENS_LOCAL_ADMIN_PASSWORD
Then run:
pnpm dev:adminThe password is delivered to a short-lived process through standard input and is not injected into the long-running API container.
Individual processes
Section titled “Individual processes”Start the Platform API dependencies with:
pnpm infra:uppnpm db:migratepnpm --filter @sens-platform/platform-api devStart the permanent web console:
pnpm web:devAfter starting TimescaleDB and NATS, the flow processes can be run separately:
pnpm --filter @sens-platform/flow-worker devpnpm --filter @sens-platform/script-sandbox-worker devpnpm --filter @sens-platform/flow-action-worker devTheir startup schemas require the database, NATS, sandbox, action, and optional SMTP values documented under Runtime Configuration. Keep actions disabled unless a local SMTP sink has been configured deliberately.
The Tailnet-capable Compose command exposes the Flow profile separately:
pnpm dev:up:flows:detacheddev:up:flows:detached starts NATS, stream bootstrap,
script-sandbox-worker and flow-worker, and enables the Platform API sandbox
requester with distinct local-only NATS identities. For the complete loopback
workspace, dev:flow-studio enables both the flows and actions profiles and
does not load the Tailscale Compose overlay. Mailpit listens on SMTP port 1025
and its local browser UI on http://127.0.0.1:8025; it is never part of Helm or
a production topology. Use only synthetic recipients locally.
The Flow workload and Action consumer still start fail-closed unless their explicit enable flags and required dependencies are present. The local profiles do not bypass checksum, sandbox, tenant, revision, idempotency or replay rules.
Vite serves http://127.0.0.1:5173 and proxies /api to
SENS_WEB_CONSOLE_DEV_API_PROXY_TARGET. The matching browser origin must be in
SENS_PLATFORM_API_ALLOWED_ORIGINS.
Build and preview the deployable static server:
pnpm --filter @sens-platform/web-console buildSENS_ENVIRONMENT=local \ SENS_WEB_CONSOLE_API_BASE_URL=http://127.0.0.1:3000 \ pnpm --filter @sens-platform/web-console previewThe preview server defaults to 127.0.0.1:8080 and exposes health, readiness,
version, metrics, runtime configuration, static assets, and SPA fallback.
Validation
Section titled “Validation”pnpm lintpnpm typecheckpnpm testpnpm buildpnpm docs:checkpnpm formatdocker compose --project-directory . \ -f infra/docker/compose.yaml \ -f infra/docker/compose.dev.yaml \ -f infra/docker/compose.tailscale.yaml configThe Netmore ingestion contract uses versioned anonymized fixtures and a gated real-NATS/TimescaleDB integration test. No permanent mock broker is part of this development baseline.
Raw ingestion
Section titled “Raw ingestion”The receiver fails closed without explicit Netmore credentials. Set
SENS_NETMORE_MQTT_USERNAME, SENS_NETMORE_MQTT_PASSWORD, and
SENS_NETMORE_MQTT_CLIENT_ID in the ignored .env, then start the complete
local stack:
pnpm devUse pnpm dev:up:ingestion:detached to run it in the background. Both commands
load the development overlays and activate the ingestion profile.
Without all three values, receiver configuration fails before connecting. NATS, migrations, bootstrap, and raw storage can be started individually without the receiver:
docker compose --project-directory . \ -f infra/docker/compose.yaml \ --profile ingestion up -d natsLocal NATS exposes 127.0.0.1:4222. Worker operation endpoints use ports 3001
and 3002. The profile uses distinct local placeholder users for bootstrap,
publish, and consume. These values are local-only and must never be promoted.
See Netmore Raw Ingestion for the gated real-NATS and TimescaleDB integration-test variables, stream contract, rollout, and recovery procedure.