Skip to content

Signals DPG Setup

The Signals DPG is the network-aware backend (API + UI). It runs standalone — no other DPG required. Its local-setup/ brings up everything it depends on: Postgres, Redis, Keycloak and Mailpit — plus, optionally, signals-search for relevance ranking.

Pick a track: A — Docker-only (fastest, one command) or B — hybrid dev (run the API/UI from source with hot-reload).

Terminal window
git clone https://github.com/Blue-Dots-Economy/signals-dpg.git
cd signals-dpg/local-setup
docker login dhi.io # app images build FROM dhi.io
cp .env.example .env # ships working dev values for both secrets
# Rotate them in place (macOS/BSD sed; on Linux drop the '' after -i).
sed -i '' "s|^INSTANCE_SHARED_SECRET=.*|INSTANCE_SHARED_SECRET=$(openssl rand -hex 32)|" .env
sed -i '' "s|^SIGNALS_PII_KEY=.*|SIGNALS_PII_KEY=$(openssl rand -base64 32)|" .env
docker compose --profile keycloak up -d --build

This builds a Postgres image with pgvector + PostGIS (the extensions db:init needs), applies the schema, starts Keycloak and imports the realm, then starts the API and UI.

The --profile keycloak flag is required: Keycloak, Mailpit and the realm-import job are profiled services, so a plain docker compose up -d skips them and login will not work.

There is a second profile, search, which adds relevance-ranked discover and match scores. Without it the stack works but returns results in recency order — see Adding search below. Profiles combine:

Terminal window
docker compose --profile keycloak --profile search up -d --build
Open thisURL
Signals UIhttp://localhost:5173 (must be :5173 — CORS)
Signals APIhttp://localhost:2742 (/api/reference = Swagger)
Keycloakhttp://localhost:8080 (login screen + admin console)
Mailpithttp://localhost:8025 (catches login OTP emails)
Signals Searchhttp://localhost:3100 (only with --profile search)

Run the backing services from local-setup/, then the API + UI from source:

Terminal window
cd signals-dpg/local-setup && cp .env.example .env
sed -i '' "s|^INSTANCE_SHARED_SECRET=.*|INSTANCE_SHARED_SECRET=$(openssl rand -hex 32)|" .env
docker compose --profile keycloak up -d \
postgres redis keycloak keycloak-init mailpit # backing services only
cd .. # repo root
pnpm install
cp .env.example .env # point at the Docker DB/Redis (see the guide)
# Root .env is a separate file from local-setup/.env — rotate its secrets too:
# SIGNALS_PII_KEY=<openssl rand -base64 32> # must decode to exactly 32 bytes
sed -i '' "s|^SIGNALS_PII_KEY=.*|SIGNALS_PII_KEY=$(openssl rand -base64 32)|" .env
pnpm db:push:api && pnpm db:init:api # schema + extensions/tables
pnpm dev:api # API on :2742 (terminal 1)
pnpm dev:ui # UI on :5173 (terminal 2)

Full env values, resets, and troubleshooting are in the local-setup/LOCAL_SETUP.md guide.

Because the model is schema-driven, you add item types and forms through network.json schemas rather than code.

link: /guides/installation/local-setup/local-stack/ label: “Path 8 of 10: Local Stack”

Out of the box, discover returns results in recency order and match scores are unavailable — the UI shows “Showing basic matches — relevance ranking is temporarily unavailable”. Adding the search profile fixes both:

Terminal window
cd signals-dpg/local-setup
cp .env.search.example .env.search # then mint an apikey (below)
docker compose --profile keycloak --profile search up -d

This adds the search query API on :3100, an ingestion worker, and a TEI embedding server running BAAI/bge-m3 — the same model production uses, baked into the image so nothing downloads it at runtime. All images are pulled from public GHCR; no extra checkout, no registry login.

Two required steps that are easy to miss:

  1. Mint an apikey. Search authenticates against the apikey table in the shared Signals database — there is no API-key environment variable. Run docker compose run --rm signals-bootstrap sh -lc "pnpm --filter api db:seed:services", which prints an sk_signals_… key on first run only, and put the raw value in .env.search.
  2. Set both variable sets. Relevance-ranked discover reads SIGNALS_SEARCH_URL; match score reads SIGNALS_SEARCH_ENDPOINT — the same URL under a different name — plus MATCH_SCORE_PROVIDER=signals_search. Setting only one leaves the other silently broken.

/health on the search API is unauthenticated and checks neither the database nor the embedder, so a 200 from it does not mean the stack can answer a query. Test with a real POST /v1/search carrying x-api-key.

Full reference — the embedding-dimension rules, network.json requirements and a troubleshooting table keyed by the exact error message — is §7 of local-setup/LOCAL_SETUP.md.