Skip to content

Run PULSE locally

There are two ways to run PULSE on your own machine. The CORE loop runs the whole stack: backend, inference, and the Flutter web app. The direct binary runs only the backend, which is enough to drive and test every non-AI RPC.

Prerequisites

  • Go 1.26 (see grpc/go.mod).
  • Flutter, for the client.
  • The sibling repositories checked out next to pulse/: core, inference, security, mesh, store, ui, and billing. The backend does not build without them, because grpc/go.mod points at them with replace directives.
  • grpcurl, to call the API by hand.

Option 1: the CORE loop

From the core repository:

cd ../core/grpc                 # CORE's Go module; set PULSE_REPO to an absolute path for `dev up`
PULSE_REPO=/abs/path/to/pulse go run ./cmd/core dev up pulse

This builds the backend and starts it on a free port. It starts the shared inference server when the model weights are present, then builds and serves the Flutter web app pointed at the backend’s real port. CORE provisions the inference binaries and weights under ../core/artifacts/.

Option 2: the backend binary

Build

Generated protobuf code is committed under grpc/api/, so you do not need to run buf to build.

cd grpc && go build -o bin/pulse-server . && cd ..

Start the server

cd grpc
PULSE_ADMIN_PASSWORD='<choose-a-password>' \
PULSE_JWT_SECRET="$(openssl rand -hex 32)" \
PULSE_GRPC_REFLECTION=true \
  ./bin/pulse-server serve --port 52000 > /tmp/pulse.log 2>&1 &

Three variables matter here:

  • PULSE_JWT_SECRET is required. Without it serve exits with refusing to start: no session signing key configured. Any value you generate works locally. It must never be a value that has been committed to a repository.
  • PULSE_ADMIN_PASSWORD seeds a break-glass admin with username admin. Without it the log says skipping admin seed; break-glass login unavailable, and every login fails.
  • PULSE_GRPC_REFLECTION=true turns on gRPC server reflection so grpcurl can list services. It is off by default. Never set it on a deployed instance.

With no --port (and no PORT variable), the server picks a free port.

Don’t stop the server with pkill -f 'pulse-server serve'. That pattern also matches the shell running the command, so it kills your own command and looks like a server crash. Kill the process by its PID instead.

Wait for health

until curl -s localhost:52000/health | grep -q '"status":"ok"'; do sleep 2; done
curl -s localhost:52000/health

The response looks like {"status":"ok","model_ready":false,"port":52000}. model_ready stays false until an inference server has loaded its weights. The HTTP status is always 200. PULSE serves every non-AI feature without a model.

Sign in and call an RPC

TOK=$(grpcurl -plaintext -d '{"username":"admin","password":"<your-password>"}' \
      localhost:52000 pulse.v1.IdentityService/Login | jq -r '.accessToken')
grpcurl -plaintext -H "authorization: Bearer $TOK" -d '{}' \
      localhost:52000 pulse.v1.CampaignService/ListCampaigns

The token is in the accessToken field. The repository also ships a smoke driver that runs health, login, whoami, and list calls in one go:

PULSE_PASSWORD='<your-password>' go run .claude/skills/run-pulse/smoke.go 52000

Without a model

A bare machine has no mistralrs-server and no model weights. The server still starts and serves login, every read, and every create or update. AI calls fail with inference server is not running. That is the expected result, not a bug.

Run the Flutter client

cd flutter
flutter run -d chrome     # web
flutter run -d linux      # desktop
flutter run               # a connected Android device

The client reads its backend address from PULSE_BACKEND_URL, a --dart-define that defaults to http://localhost:50051. For the web build it is fixed at compile time, which is why core dev up rebuilds the bundle when the backend port changes.

Run the tests

cd grpc && go test ./...
cd flutter && flutter analyze