Documentation

PulseBoard is a single F# binary that ingests metrics, logs, and traces; serves a live dashboard; and surfaces cost + AI insights over your own data. Everything below speaks plain HTTP.

Architecture in one minute

PulseBoard is one binary that plays two distinct roles. Pick the role at startup with command-line flags — you never need two different builds.

SurfaceWhat it isAudience
/, /docs, /pricing, /signup, /signin The public marketing site — static pages, unauthenticated. Same content for every visitor. Prospects, search engines.
/app The dashboard SPA — charts, queries, alerts. Authenticated with an API key (Bearer). A signed-in member of one specific tenant.
/admin The workspace admin — keys, members, plan, billing, audit. Same Bearer auth, requires the admin scope. A workspace owner / operator.
/ingest/*, /v1/*, /loki/*, /api/v1/write The data plane. Bearer-authenticated, RBAC-scoped. Your agents, SDKs, scrapers.

The dashboard and admin are per-workspace surfaces. In a real hosted deployment they live behind a tenant-specific URL (for example https://acme.pulseboard.cloud/app), and the public website at pulseboard.cloud never links to them directly — you reach your workspace by following the URL you got at sign-up, or by signing in with your API key.

Deployment modes

The same binary runs in two configurations:

Single-tenant (default)

dotnet run -- --port=8080

One workspace per process. No /api/signup — the operator provisions keys directly. Best for self-hosting one team or running locally for development. This is what you get when you clone the repo and run dotnet run with no flags.

Multi-tenant (the “edge”)

dotnet run -- --multi-tenant --port=8080 \
  --postgres="Host=db;Database=pulse;Username=pulse"

Many workspaces share one process, isolated by tenant ID. The public onboarding endpoint /api/signup is enabled and creates a new tenant + API key on demand. Recommended for a hosted product. The public marketing pages are served alongside, so a small deployment can keep “website” and “edge” in one process.

In a larger hosted deployment you would run the marketing pages on a dedicated pulseboard.cloud host that only serves /, /docs, /pricing, /signup, and proxies the actual POST /api/signup to a provisioning service that spins up a per-tenant workspace on a subdomain like https://acme.pulseboard.cloud. See self-hosting for the current recipe and the project PLAN.md for the hosted/provisioner roadmap.

Quickstart

Sign up gets you an API key. Use it as a Bearer on every data-plane call.

# 1. Create a tenant + key
curl -sS -X POST http://127.0.0.1:8080/api/signup \
  -H 'content-type: application/json' \
  -d '{"slug":"acme","email":"you@acme.com"}'
# → {"tenantId":"...","apiKey":"pk_xxx.yyy",...}

export PB_KEY="pk_xxx.yyy"

# 2. Push a metric
curl -sS -X POST http://127.0.0.1:8080/ingest/metrics \
  -H "Authorization: Bearer $PB_KEY" \
  -H 'content-type: application/json' \
  -d '[{"name":"payments.latency.p99","value":42.5}]'
# → {"accepted":1}
The plaintext API key is shown once at signup and cannot be recovered. Stash it in a secret manager immediately.

Authentication

Two paths:

RBAC scopes: ingest, query, admin. A key carries one or more scopes; admin endpoints require admin.

Ingest — metrics & logs

Metrics (native JSON)

POST /ingest/metrics

[
  {"name":"payments.latency.p99", "value":42.5, "ts": 1717000000},
  {"name":"payments.qps",         "value":1240}
]

Returns {"accepted":N} on success. The series name is the attribution key for cost transparency: by default PulseBoard groups by the first dot-segment, so payments.* rolls up under the payments team.

Logs (native JSON)

POST /ingest/logs

[
  {"ts": 1717000000, "level":"warn", "msg":"slow query", "service":"api"}
]

OTLP / Prometheus / Loki

The edge also accepts:

EndpointFormat
/v1/metricsOTLP HTTP/JSON
/v1/logsOTLP HTTP/JSON
/api/v1/writePrometheus remote_write (snappy/protobuf)
/loki/api/v1/pushLoki push API

All four honor the same Bearer auth and the same per-tenant quotas.

Query

GET /api/series?name=&from=&to=

Returns time-bucketed samples for the given series in the bearer's tenant. For full PromQL-style expressions see the in-app query console under /app.

Alerts & live dashboards

Alerts evaluate on a tick and fire via the configured Notify channels (SMTP, webhook, …). Live dashboards stream over WebSocket at /ws; the bundled /live page is a minimal viewer.

AI assist

POST /api/ai/explain

{
  "seriesName": "payments.latency.p99",
  "samples": [{"ts":1,"value":1.0},{"ts":2,"value":1.1},{"ts":3,"value":9.0}],
  "question": "why the spike?"
}

Returns:

{
  "provider": "echo",
  "summary":  "...mean=3.70, stddev=3.78, max=9.00 @ 3.
               Largest single-step jump was 7.90 at 3 (> 2× stddev)...",
  "annotations": { "mean":"3.70", "stdDev":"3.78", "spikeTs":"3" }
}

The provider is pluggable (IAiProvider). OSS ships EchoAiProvider: deterministic mean/stddev/jump analyzer that runs in-process — no network calls, no leaked data. The hosted edition can plug an OpenAI/Anthropic/local-vLLM adapter behind the same interface, gated by a per-tenant ai.enabled flag.

Cost transparency

Every accepted ingest call is attributed to its series. Two admin reads:

EndpointReturns
GET /api/admin/tenants/<id>/cost/series?top=N Top-N series by sample count, with estimated bytes and projected monthly USD at the Pro IngestBytes rate.
GET /api/admin/tenants/<id>/cost/teams Aggregated by team policy (default = first dot-segment of the series name; payments.*payments).

The numbers are projected at the public Pro rate of $0.50 / GiB ingest, so the "what would this cost me?" answer always matches the pricing calculator.

Pricing & billing

Two public, unauthenticated endpoints back the calculator UI:

Per-tenant usage and billing snapshots are admin-scoped:

GET /api/admin/tenants/<id>/billing/current
GET /api/admin/tenants/<id>/billing/rollup?from=&to=

Admin & RBAC

The /admin UI is API-key gated. It surfaces:

Self-hosting

git clone https://github.com/.../pulseboard
cd pulseboard/src/edge
dotnet run -- --multi-tenant --port=8080

# With Postgres (recommended for prod):
dotnet run -- --multi-tenant \
  --postgres="Host=db;Port=5432;Database=pulse;Username=pulse" \
  --port=8080

Useful env vars:

VarPurpose
PULSE_DATA_DIRLocal data dir (default: ./data)
PULSE_OIDC_AUTHORITYEnable OIDC SSO for humans
PULSE_OIDC_CLIENT_IDOIDC client
PULSE_NOTIFY_SMTP_*SMTP notify channel

License

PulseBoard core is released under MIT. See LICENSE in the source tree.