Skip to content
SENS Platform Docs

Local Development

The Docker development stack started by pnpm dev contains:

  • TimescaleDB;
  • a one-shot database migration container;
  • platform-api with TypeScript build and restart watching;
  • web-console with 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.

  • Node.js compatible with the root engines field;
  • pnpm from the root packageManager field;
  • Docker with Compose;
  • Tailscale CLI connected to the development Tailnet for shared pnpm dev commands. The loopback-only pnpm dev:flow-studio command does not require Tailscale.
Terminal window
cp .env.example .env
pnpm install
pnpm dev

For local visual flow and block development, use:

Terminal window
pnpm dev:flow-studio

The 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.

ComponentLoopbackTailnet
web-consolehttp://127.0.0.1:5173http://<tailscale-ip>:5173
platform-apihttp://127.0.0.1:3000http://<tailscale-ip>:3000
TimescaleDB127.0.0.1:5432<tailscale-ip>:5432

Use Tailnet ACLs or grants to restrict these development ports.

Terminal window
pnpm dev:up:detached
pnpm dev:up:ingestion
pnpm dev:up:ingestion:detached
pnpm dev:flow-studio
pnpm dev:flow-studio:detached
pnpm dev:logs
pnpm dev:ps
pnpm dev:down
pnpm dev:rebuild
pnpm dev:reset
pnpm docker:delete:volumes

pnpm 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:

Terminal window
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-retention

The 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.

Set these values only in the ignored .env:

  • SENS_LOCAL_ADMIN_USERNAME
  • SENS_LOCAL_ADMIN_EMAIL
  • SENS_LOCAL_ADMIN_DISPLAY_NAME
  • SENS_LOCAL_ADMIN_PASSWORD

Then run:

Terminal window
pnpm dev:admin

The password is delivered to a short-lived process through standard input and is not injected into the long-running API container.

Start the Platform API dependencies with:

Terminal window
pnpm infra:up
pnpm db:migrate
pnpm --filter @sens-platform/platform-api dev

Start the permanent web console:

Terminal window
pnpm web:dev

After starting TimescaleDB and NATS, the flow processes can be run separately:

Terminal window
pnpm --filter @sens-platform/flow-worker dev
pnpm --filter @sens-platform/script-sandbox-worker dev
pnpm --filter @sens-platform/flow-action-worker dev

Their 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:

Terminal window
pnpm dev:up:flows:detached

dev: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:

Terminal window
pnpm --filter @sens-platform/web-console build
SENS_ENVIRONMENT=local \
SENS_WEB_CONSOLE_API_BASE_URL=http://127.0.0.1:3000 \
pnpm --filter @sens-platform/web-console preview

The preview server defaults to 127.0.0.1:8080 and exposes health, readiness, version, metrics, runtime configuration, static assets, and SPA fallback.

Terminal window
pnpm lint
pnpm typecheck
pnpm test
pnpm build
pnpm docs:check
pnpm format
docker compose --project-directory . \
-f infra/docker/compose.yaml \
-f infra/docker/compose.dev.yaml \
-f infra/docker/compose.tailscale.yaml config

The 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.

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:

Terminal window
pnpm dev

Use 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:

Terminal window
docker compose --project-directory . \
-f infra/docker/compose.yaml \
--profile ingestion up -d nats

Local 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.