This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
SmartBotic is a workflow automation and execution platform with a microservices architecture. The backend is C++20 with gRPC communication, and the frontend is React/TypeScript.
The existing build/ directory is configured with Ninja, not Make. Running
make in it prints "Nothing to be done" and builds nothing at all - it does not
fail, so an edit looks built when it has not been. Always build through cmake,
which uses whichever generator the directory was configured with:
# Everything
cmake --build build -j8
# One target - much faster when only one service changed
cmake --build build --target smartbotic-webserver -j8
cmake --build build --target smartbotic-runner -j8
Configuring a build directory from scratch:
# Debug
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
# Release
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
# With Address Sanitizer
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON
After building a service, restart it to actually run the new binary. Everything runs on zeus, which is Void Linux and uses runit, not systemd:
sudo sv restart smartbotic-webserver smartbotic-runner
sudo sv status smartbotic-webserver smartbotic-runner
tail -f /var/log/smartbotic-webserver/current
tail -f /var/log/smartbotic-runner/current
There is no systemctl on zeus at all - it answers "command not found", which
reads like a typo rather than the wrong init system. The systemd/user/*.service
units in this repo are leftovers from when the services ran on mulan.
./scripts/run-node-tests.sh # every node, through a running webserver + runner
./scripts/run-cpp-tests.sh # pure C++ logic, compiled in the build image
Node tests need the services up. When the runner and webserver are on different
hosts, a case whose NODE fetches the API needs SMARTBOTIC_RUNNER_API set to an
address the runner can reach - see the {{API}} note under Relocating a service.
cd webui
npm install
npm run dev # Dev server (port 3000, proxies to localhost:8090)
npm run build # Production build
npm run lint # ESLint
The database is not built from this repo - it is the standalone upstream
smartbotic-database daemon. It runs on zeus as the Docker container
smartbotic-db (moved there 2026-08-09; it and the SD.cpp REST API could not
share mulan's memory), so it is neither an APT package nor a system service on
the host any more. It is already up; the in-repo services connect to it:
# 1. Database daemon - Docker container `smartbotic-db` on 9004. Not from this repo.
docker ps --filter name=smartbotic-db
# 2. WebServer (HTTP 8090, WebSocket 8091, gRPC 9012 + 9013)
# 3. Runner (gRPC 9011)
sudo sv status smartbotic-webserver smartbotic-runner
No service may assume another is on the same host. Every address a service
needs is an env override, and both runit run scripts
(/etc/sv/smartbotic-*/run) state all of them explicitly even where the value
is the local default - so moving a service to another machine for load
balancing or failover is an edit of one file, not a hunt through the code.
| variable | service | what breaks when it is wrong |
|---|---|---|
DATABASE_ADDRESS |
both | no persistence at all; fails loudly |
WEBSERVER_ADDRESS |
runner | the runner never registers |
NODE_SYNC_ADDRESS |
runner | no node updates reach it |
CREDENTIAL_SERVICE_ADDRESS |
runner | credentials and workflow-control fail |
ADVERTISE_ADDRESS |
runner | fails silently - the runner registers as localhost:<port>, reports online, and every dispatch goes to the webserver's own host |
NODES_PATH |
runner | no built-in nodes |
WEBUI_PATH |
webserver | the UI 404s, the API still works |
config/*.json is tracked in git and its defaults are all localhost, so it
must not be edited per host. config/*.local.json is gitignored but nothing
reads it - the mechanism is ${VAR:default} substitution.
Tests must not assume co-location either. A case whose node fetches the API
writes {{API}}, which the harness substitutes from SMARTBOTIC_RUNNER_API.
Three cases hardcoded http://localhost:8090 and broke the day the runner moved.
SD.cpp stays on mulan (https://mulan:8077) because that is where the GPU is.
zeus needs its self-signed certificate in two stores - the trusted-certificate
store for host mulan (for the runner) and the system CA store (for the test
harness's Python probe).
Both the database address and the project namespace are configurable everywhere,
via config/webserver.json and config/runner.json:
| Key | Default | Env override |
|---|---|---|
database_address |
localhost:9004 |
DATABASE_ADDRESS |
database_project |
smartbotic-automation |
DATABASE_PROJECT |
One smartbotic-database instance is shared by several projects. database_project
is the multi-tenant namespace (upstream client >= 2.3): the client transparently
sends every collection as <project>:<collection>, so our data stays isolated from
other services on the same daemon. Never set it to default - that is the shared
back-compat namespace where projects collide.
Both persist through the external smartbotic-database daemon (upstream repo:
ssh://git@git.smartbotics.ai:10022/fszontagh/smartbotic-database.git), consumed
as the libsmartbotic-db-client-dev APT package. cmake/FindPackages.cmake locates
it via find_package(smartbotic-db-client CONFIG REQUIRED) → target smartbotic::db-client.
lib/storage/storage_client.{hpp,cpp} is a thin PIMPL adapter over
smartbotic::database::Client, so callers never touch the upstream API directly.
WebUI (React) → HTTP/WebSocket → WebServer → gRPC → smartbotic-database (external)
↓
gRPC → Runner(s) → executes workflows
lib/ - Shared libraries (common utilities, config loader, logging, storage client)proto/ - Protocol Buffer definitions for gRPC servicessrc/ - Microservice implementationsnodes/ - Built-in workflow node definitions (JavaScript modules)webui/ - React frontendconfig/ - Runtime JSON configuration filesWorkflow nodes are JavaScript modules in nodes/ with this interface:
module.exports = {
configSchema: { /* JSON Schema */ },
inputSchema: { /* JSON Schema */ },
outputSchema: { /* JSON Schema */ },
execute: async (config, input, context) => { /* returns output */ }
}
Nodes are loaded by the Runner service and executed in QuickJS. Hot-reload is supported.
See docs/nodes.md for complete node creation guide.
See docs/node-roadmap.md for nodes worth building next.
After creating or modifying nodes in nodes/, migrate them to the running database:
# 1. Login to get JWT token
TOKEN=$(curl -s http://localhost:8090/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin"}' | jq -r '.accessToken')
# 2. Migrate nodes from filesystem to database
curl -X POST http://localhost:8090/api/v1/nodes/migrate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"nodesPath": "./nodes"}'
# 3. Verify nodes are loaded
curl -s http://localhost:8090/api/v1/nodes \
-H "Authorization: Bearer $TOKEN" | jq '.nodes[] | "\(.id) - \(.name)"'
Runners automatically receive node updates via gRPC streaming - no restart required.
Services load JSON configs from config/ directory:
webserver.json - HTTP port, JWT settings, runner load balancing, database_addressrunner.json - Runner ID, max concurrent executions, hot reload settings, database_addressThe database daemon has its own config at /etc/smartbotic-database/config.json,
owned by the upstream package - not by this repo.
Environment variables can override config values using ${VAR_NAME:default} syntax.