Table of Contents

Getting started

This walks the full local lab: bring up SQL Server + Elsa (Server + Studio), author workflows in Studio, run the broker, and push a request through Elsa end-to-end.

Prerequisites

  • .NET 9 SDK — on this machine it lives under ~/.dotnet (the system SDK is 8.0 and can't target net9.0). Put it first on PATH, and set DOTNET_ROOT so the global tools resolve the 9.0 runtime:
    export PATH="$HOME/.dotnet:$HOME/.dotnet/tools:$PATH"
    export DOTNET_ROOT="$HOME/.dotnet"
    
  • Docker Desktop. On Apple Silicon, enable Settings ▸ General ▸ "Use Rosetta for x86_64/amd64 emulation" — SQL Server is amd64-only and needs it (ADR-0005). The Elsa image is multi-arch and runs native arm64.

All commands below run from src/services/ElsaBroker/.

1. Bring up the lab (SQL + Elsa)

docker compose up -d
docker compose ps          # wait for elsa-broker-sql = healthy

This starts:

Service Where What
elsa-broker-sql localhost:1433 SQL Server — broker DB + MassTransit SQL transport
elsa-broker-elsa http://localhost:13000 Elsa Server + Studio; loads ./workflows at /app/Workflows

2. Author workflows in Elsa Studio

  1. Open http://localhost:13000 and sign in with admin / password (default dev credentials — change for anything non-local).
  2. Create the workflows in Studio. The official server+studio image is DB-backed — it does not auto-load the mounted workflows/ folder (that feature requires a custom Elsa server with the file provider). So author them here: build broker-dispatch to the contract in workflows/README.md (ack 202 → DispatchWorkflow the target with WaitForCompletionSendHttpRequest the result to callbackUrl with header X-Callback-Secret), and one workflow per request type (e.g. invoice-process with definitionId = invoice-process).
  3. Export each workflow's JSON into workflows/ (the broker scans that folder to register requestType → definitionId; declare customProperties.requestType on each request-type workflow). The broker re-registers on next start.

The folder is the broker's source of truth for request-type → workflow mapping; the Elsa server's source of truth is its own DB (authored in Studio). They are linked by matching definitionIds, not auto-synced. To make the folder drive Elsa too, run a custom Elsa server (file provider) instead of the demo image — see Elsa integration.

Each request-handling workflow must declare customProperties.requestType; see Elsa integration for the convention.

3. Certificates (one-time)

The Queue API requires client certificates signed by an internal CA:

cd ElsaBroker.CertTools
dotnet run -- ca                # internal CA (run once) — note the thumbprint
dotnet run -- server localhost  # server cert for the Queue
dotnet run -- client ClientA    # one client cert per authorized caller — note the thumbprint
cd ..

Then:

  • Put the CA thumbprint in ElsaBroker.Queue/appsettings.jsonMtls:CaThumbprint.
  • Add the client thumbprint to ElsaBroker.Queue/ClientAllowlist.json.
  • Copy ca.crt next to the Queue binary so chain validation finds it:
    mkdir -p ElsaBroker.Queue/bin/Debug/net9.0/certs
    cp ElsaBroker.Queue/certs/ca.crt ElsaBroker.Queue/bin/Debug/net9.0/certs/
    

Details: security model.

4. Build + run the broker

dotnet build                 # all projects (0 warnings)
dotnet test                  # xUnit + coverage

# terminal 1 — Queue API: mTLS ingress :5001, internal callback listener :5080
cd ElsaBroker.Queue && dotnet run

# terminal 2 — Processor: consumes, dispatches to Elsa, awaits callback
cd ElsaBroker.Processor && dotnet run

On startup the Processor logs how many request types it registered from workflows/. EF migrations and the SQL transport schema are applied automatically.

The shared callback secret must match in both ElsaBroker.Queue/appsettings.json and ElsaBroker.Processor/appsettings.json (Elsa:CallbackSecret) and in the X-Callback-Secret header the broker-dispatch workflow sends. The default dev value is dev-callback-secret-change-me.

5. Submit a request (end-to-end through Elsa)

macOS curl (SecureTransport) can't present a PEM client cert, so use the bundled Python client:

python3 docs/scripts/smoke-client.py

It submits an InvoiceProcess request over mutual TLS and polls until terminal. The lifecycle:

Queued → Processing → (broker dispatches to Elsa → broker-dispatch runs the target workflow)
        → callback finalizes → Completed | Faulted

Generate a migration

Migrations are scaffolded against the design-time factory, so no host needs to boot:

dotnet ef migrations add <Name> --project ElsaBroker.Data

Tear down

docker compose down          # keep the SQL volume
docker compose down -v       # also drop the SQL data volume