Skip to content

Develop helixCore

A practical guide to developing Helix and its client extensions from a Honeywick workstation. The everyday loop is bare-metal, no container; the container is for testing and shipping deployments.

  • helixCore — the client-agnostic platform: a React SPA + a TypeScript backend (crud, dispatch) that wraps the proprietary Spiral optimiser (spiral.l64), backed by MongoDB.
  • Client extensions — per-client repos (e.g. AA) that plug into helixCore via the published @helix/client-sdk: domain/map config + React config/components. helixCore ships without any client baked in.
  • Spiral — the optimisation engine, built separately.

The split: anything client-specific (wording, behaviour, templates, icons, custom components) lives in the client repo / domain file — never hardcoded in helixCore.

~/Honeywick/
Helix/ # the helixCore platform repo
helixCore/
frontend/ # React SPA (Vite)
*.ts # backend: crud, dispatch, spiral, queue, crudObject, …
client-sdk/ # @helix/client-sdk source
startHelix # bare-metal + container launcher
config # env contract (ports, paths, domain/map)
release/helixctl # container launcher (host-side)
Dockerfile.release # client-agnostic runtime image
publishHelixCore.sh # build + push the image / SDK
AA -> ../Clients/AA/helix/custom # symlink to the active client's customisation (per client)
Clients/<CLIENT>/ # each client's repo (git)
helix/
custom/ # client extension SOURCE (= Helix/<CLIENT> via the symlink)
<CLIENT>.domain.json # Spiral/domain config (stopParams, resourceParams, helix.* blocks)
<CLIENT>.map.json # Spiral map
<CLIENT>.ui.json # UI params
config # client env overrides (sourced by helixCore/config)
frontend/clientConfig.tsx# the client extension (templates, helixOverrides, logo, editors)
vite.client.config.ts # builds the extension -> ../runtime/client
package.json # depends on @helix/client-sdk
runtime/ # launcher stub + version pins + built bundle (client/, served as /client)
scenarios/ # Helix-only client scenarios
tests/ # client tests + results (ROOT — shared with the Spiral-only world)
release/ # legacy Honeywick/Spiral artifacts
Spiral/ # the Spiral optimiser source (builds spiral.l64)

Each client provides Helix/<CLIENT> as a symlink to Clients/<CLIENT>/helix/custom — that’s what startHelix <CLIENT> resolves.

  • On PATH: node, npm, mongod, mongosh.
  • Environment (typically in ~/.bashrc):
    • SPIRAL_LICENCE — a Spiral command, e.g. {"set":{"licence":"<hex>"}} (not the raw key; Spiral reads this env var via an include environment directive). The app refuses to start without it.
    • GOOGLE_MAPS_API_KEY — for the map.
    • Optional: SPIRAL_JSON, SPIRAL_TRAVEL_CALCULATOR, AA_VEHICLE_API_KEY.
  • Install deps: (cd helixCore/frontend && npm install) and (cd Clients/<CLIENT>/helix/custom && npm install).

4. The everyday loop — bare-metal dev (no container)

Section titled “4. The everyday loop — bare-metal dev (no container)”

From the Helix repo:

Terminal window
./helixCore/startHelix <CLIENT> --dev # e.g. ./helixCore/startHelix AA --dev

This brings up, with dynamic ports (printed) and opens the browser:

  • mongod (data under ~/helix/<CLIENT>/logs/dev/mongoData),
  • dispatch + crud (the backend, wrapping Spiral),
  • a Vite dev server with HMR.

Live reload covers both sides:

  • editing the helixCore frontend (helixCore/frontend/src/**) hot-reloads,
  • editing the client extension (Clients/<CLIENT>/helix/custom/frontend/**, resolved via the @client alias) hot-reloads too.

Domain/map are read from Clients/<CLIENT>/helix/custom/<CLIENT>.domain.json + .map.json.

Logs (everything you need to debug): ~/helix/<CLIENT>/logs/dev/crud.log, dispatch.log, spiral.log, crud2spiral.log, vite.log, mongo.

  • --solo — single node, no persistence, no Vite HMR (logs in …/logs/solo-<pid>).
  • --spiral <runfile> — read-only Spiral streaming: feed a run file straight to Spiral; no entity edits.
  • --master / --replicate <primaryHost> / --readOnly <primaryHost> — cluster nodes (mongo replica set).

This is the primary Honeywick workflow — every run mode is testable here with no container.

  • clientConfig.tsx exports config: ClientConfigtemplates (entity defaults via createTemplates), helixOverrides (custom helix.* functions), about, clientLogo, and optional data-extensions / full-control editors.
  • @helix/client-sdk provides the types + helpers (ClientConfig, createTemplates, StopState, colors, …). In Honeywick dev it resolves to the helixCore source via a Vite alias; a standalone client build resolves the published package from the GitLab npm registry.
  • Domain config drives the UI. stopParams entries carry a helix block that the frontend reads — icons, lineLetter, warning templates, the stop templates (option generator), add/morph-stop menus, state colours, etc. Change behaviour/wording there, not in the frontend.
  • helix.* parameters are string expressions (e.g. "helixColorDelay()") bound to clientModule functions and client helixOverrides, evaluated with this = the stop/resource (StopContext, with typed getInStop()/getOutStop() accessors). {prop|default} placeholders interpolate values. Resolution chains client helixOverrides first, then core clientModule, and a function returning undefined defers to the previous override — so overrides can handle only the cases they care about.
  • Icons live in helixCore/frontend/public/icons/*.png (core) plus client icons, named via the icon registry (icons.tsx).
  • The backend is the root *.ts in helixCore/ (crud.ts, dispatch.ts, spiral.ts, queue.ts, crudObject.ts, crudHandlers.ts, …).
  • crud spawns spiral.l64 and talks to it over stdin/stdout, persisting to MongoDB; dispatch tails Mongo change streams and broadcasts to the SPA over WebSockets.
  • Tags correlate a command with Spiral’s per-command “done” echo (used for REST replies, progress, and run-file completion). Input and output run on two independent async queues so command delivery isn’t throttled by output processing.
  • Client backend hooks (crudClientModule.ts core + the client’s crudClientModule.js, merged in crud.ts): the crud calls module functions named by the domain — stopParams[type].helix.check (validation, input path, crudObject.ts), resourceParams[type].helix.onCreate/onDelete (entity lifecycle, input path, live mutations only), and stopParams[type].helix.onState[from][to] (stop transitions, Spiral-output path, crudHandlers.ts updateStopTable). Each gets this = entity/stop and a CrudHookInfo first arg; info.now = ctx.now() (wall-clock + Spiral’s timeDelta). null matrix keys mark create/delete edges. Only entity types with a params block (currently resource) can carry entity hooks. The contract types are exported from @helix/client-sdk.
  • Functional tests are run files: Clients/<CLIENT>/tests/regression/*.run.json (repo root; shared with the Spiral-only world; replayed through the CRUD pipeline). There is no unit-test framework.
Terminal window
(cd helixCore/frontend && npx tsc --noEmit) # frontend — aim for 0 errors
(cd helixCore && npx tsc --noEmit) # backend — aim for 0 errors

Run both before committing. The regression .run.json files are the functional safety net.

8. Container development & deployment (helixctl)

Section titled “8. Container development & deployment (helixctl)”

The container is for deployment (and testing it) — not the daily loop. The launcher helixctl is client-agnostic; the client name is always explicit (mirrors startHelix <CLIENT>).

Build/refresh the image locally (when testing the container path):

Terminal window
./publishHelixCore.sh <ver> # build + push; HELIX_OFFLINE_TAR=1 to save a local tar instead

Fetch the launcher from the image (registry-only):

Terminal window
podman run --rm --entrypoint cat <image> /app/helixctl > helixctl && chmod +x helixctl

Container client dev (live rebuild, browser refresh — no HMR). To develop a client extension the way a client does (no helixCore source, no bare metal), use the launcher that ships in the client repo’s helix/runtime/ and run from that repo’s root:

Terminal window
cd Clients/<CLIENT> # the client repo root (holds helix/ + tests/)
./helix/runtime/helixctl pull
./helix/runtime/helixctl <CLIENT> dev # mounts helix/ (+ root tests/), runs `vite build --watch`
# edit client source -> ~1s rebuild (helix/runtime/logs/dev/clientbuild.log) -> refresh the browser

See Develop a client extension for the full client-side flow.

Deploy to a node:

Terminal window
(cd Clients/<CLIENT>/helix/custom && npm run build:client) # or publish-<client>.sh <ver> -> a bundle tar
helixctl deploy <bundle.tar> # unpacks client/ + helix-config/
helixctl <CLIENT> run | spiral | master | replicant | readOnly
  • run = writable single node; cluster modes need HELIX_PUBLIC_HOST (and HELIX_PRIMARY for replicant/readOnly).
  • HELIX_LOG_DIR_HOST=<dir> bind-mounts the container’s /app/logs to the host so logs survive --rm (defaulted for dev).

9. Testing client modes from Honeywick (no registry needed)

Section titled “9. Testing client modes from Honeywick (no registry needed)”
  • Bare-metal already exercises every mode: startHelix <CLIENT> --dev | --solo | --spiral <file> | --master | --replicate | --readOnly. Start here.
  • Local container loop to validate the deployed path: build the image locally (HELIX_OFFLINE_TAR=1 ./publishHelixCore.sh <ver>), build the client bundle, then point helixctl at the local image (HELIX_IMAGE=<local-ref> helixctl <CLIENT> dev … or … deploy && … run). No publish required.
  • helixCore image./publishHelixCore.sh <ver> → registry. Each build pushes three tags: :<ver>, :latest, and the floating :sdk-<MAJOR.MINOR> (e.g. :sdk-0.1) computed from helixCore/client-sdk/package.json. The SDK version is also baked into the image (/app/SDK_VERSION + helix.sdk label). :sdk-0.1 always points at the newest core on SDK 0.1.x — that’s what clients pin.
  • @helix/client-sdk → GitLab npm registry; bump the version when the SDK contract changes. A breaking bump to 0.2.x starts a fresh :sdk-0.2 line and leaves :sdk-0.1 frozen at the last 0.1 core.
  • Client bundlepublish-<client>.sh <ver> → tar (client/ + helix-config/) → helixctl deploy on the node. Clients pin helix/runtime/VERSION = sdk-0.1 (not an exact core tag), so helixctl pull auto-upgrades the core within the SDK line; helixctl deploy enforces minor-compatibility against the image’s baked SDK.
  • Domain-driven: client wording/behaviour lives in <CLIENT>.domain.json (stopParams.helix.*), not the frontend.
  • spiral.l64 is a built artifact (from ~/Honeywick/Spiral) — it is not maintained/committed from the Helix repo.
  • Logs: bare-metal under ~/helix/<CLIENT>/logs/{dev,solo-<pid>}; container via the HELIX_LOG_DIR_HOST bind mount.
  • Repos are separate: Helix (platform) and each Clients/<CLIENT> are independent git repos — commit each on its own; don’t cross-commit.
  • Commit only when asked; finish commit messages with the agreed Co-Authored-By trailer.