A status-page and observability platform built around a versioned-app registry. Bump tracks app versions, ingests RFC 7807 problem reports from client applications, probes services for uptime and latency, and publishes per-owner public status pages with outages, announcements, and email subscribers. Owners, environments, and servers mirror the infrastructure rosters in daniel-miller/infra/README.md.
- App version management. Register an app, read its current version, and atomically bump major, minor, or patch.
- Problem reporting. Ingest RFC 7807 problem reports from clients, store them, and email periodic digests.
- Uptime monitoring. Probe HTTP services on a fixed interval, record per-bar uptime and latency, and roll up daily summaries.
- Outages and announcements. Publish outage timelines and scheduled announcements, scoped globally or per owner.
- Public status pages. Compose per-owner or global status, served by a bundled React SPA.
- Subscribers. Confirmed-opt-in email subscribers per owner, with per-board caps and one-click unsubscribe.
- Accounts. Cookie-session login with CSRF, password reset, email change, and TOTP MFA + recovery codes.
Built on .NET 10 (ASP.NET Core MVC), Dapper, Npgsql, Newtonsoft.Json, Serilog, and Mailgun. Frontend is React + Vite + Tailwind, bundled into the API's wwwroot at publish time.
| Project | Description |
|---|---|
Bump.Api |
Web API and host for the SPA. Runs DB migrations on boot and exposes all /api/** endpoints. |
Bump.Sdk |
Client library for reporting unhandled exceptions from .NET applications to /api/problems. |
Bump.Worker |
Background service: monitor probing, alert digests, announcement scheduling, idempotency sweep. |
web/ |
React + Vite + Tailwind SPA. Built into web/dist and staged into src/Bump.Api/wwwroot. |
- .NET SDK 10.0 or later (pinned via
global.json). - PostgreSQL 13 or later.
- Node.js 24+ and npm (pinned via
.nvmrc). - PowerShell 7+ (for the release build script).
- A Mailgun account (optional; required for password resets, subscriber confirmations, and alert digests). Both hosts start without it and log a warning at boot; nothing is delivered until it is set.
Copy config/appsettings.work.example.json to config/appsettings.work.json and fill it in. The example carries every key both hosts require, so you find out what is missing once instead of one restart at a time.
Bump.Api and Bump.Worker both read Bump:Database:ConnectionString. Both projects link config/appsettings.json (committed defaults) and config/appsettings.work.json (gitignored local secrets) at build time. Edit config/appsettings.work.json or override via environment variable:
export Bump__Database__ConnectionString="Host=localhost;Port=5432;Database=bump;Username=postgres;Password=YOUR_LOCAL_PASSWORD"Environment variables override config/appsettings.json, but not config/appsettings.work.json - that file is added last and wins. Production ships without it, so the variables above apply there.
Schema migrations in db/migrations/*.sql are applied automatically at API startup by Migrator. No manual psql step is required for new databases — but you can apply the files manually if you prefer:
psql -U postgres -d bump -f db/migrations/001-create-server.sql
# ...etcBump uses three auth schemes, summarized in the Swagger description:
Both bearer keys ship empty in config/appsettings.json and the API refuses to start until they are set. That is deliberate: an empty Apps list silently 401s every deploy pipeline, and an empty Problems key silently drops /api/problems back to session-only so every SDK consumer stops reporting. Neither looks like a configuration error at the point it happens.
-
Apps bearer key —
/api/apps/**. Pre-shared keys fromBump:Api:Security:Apps:ClientSecrets(array).{ "Bump": { "Api": { "Security": { "Apps": { "ClientSecrets": [ "generate-a-long-random-string-per-client" ] } } } } } -
Problems bearer key —
POST /api/problems. Single pre-shared key fromBump:Api:Hosting:ClientSecret. Same key everyBump.Sdkconsumer presents, so it is named identically on both sides of the exchange.{ "Bump": { "Api": { "Hosting": { "ClientSecret": "change-this-to-a-real-key" } } } } -
Session cookie —
/api/auth/**,/api/accounts/**, and admin surfaces under/api/admin/**(/api/admin/owners,/api/admin/services,/api/admin/outages,/api/admin/announcements,/api/admin/apps). Established viaPOST /api/auth/login. State-changing requests must includeX-Bump-Csrfmatching thebump_csrfcookie. JWT signing key inBump:Api:Security:Jwt:Key; cookie domain/SameSite/Secure inBump:Api:Security:Cookie.
Public surfaces (/api/health, /api/status/**, /api/subscribers/confirm, /api/subscribers/unsubscribe, /swagger) require no auth.
The API CLI generates a password hash and the matching seed SQL:
dotnet run --project src/Bump.Api/Bump.Api.csproj -- hash 'your-password'Paste the printed INSERT INTO account ... into psql. See also db/seed-admin.sql.
dotnet run --project src/Bump.Api/Bump.Api.csproj
dotnet run --project src/Bump.Worker/Bump.Worker.csprojThe two hosts listen on different ports, set per host by Bump:Api:Hosting:Urls and Bump:Worker:Hosting:Urls. The example work config puts the API on 5135 (what web/vite.config.ts proxies to) and the worker on 8080; committed defaults are 8080 and 8081 for deployment. tools/start.ps1 runs both and writes pid files.
In another terminal, run the SPA in dev mode:
cd web && npm install && npm run devThe Vite dev server serves at http://localhost:5173; the API allows it via Bump:Api:AllowedOrigins. Swagger UI is at /swagger — click Authorize and paste a bearer key (no Bearer prefix) to exercise the bearer-protected endpoints.
All routes are prefixed with /api. Full request/response shapes are in Swagger (/swagger).
| Method | Route | Description |
|---|---|---|
| POST | /api/apps |
Create an app. Optional version (e.g. "0.0.4"); defaults to 0.0.1. |
| GET | /api/apps |
List all apps. |
| GET | /api/apps/{handle} |
Get one app. |
| DELETE | /api/apps/{handle} |
Delete an app. |
| GET | /api/apps/{handle}/version |
Get the current version string. |
| PATCH | /api/apps/{handle}/version |
Set any subset of major, minor, patch to absolute values; unspecified parts unchanged. |
| POST | /api/apps/{handle}/version/bumps |
Body { "component": "major"|"minor"|"patch" }. Creates the app if missing. |
| Method | Route | Auth | Description |
|---|---|---|---|
| POST | /api/problems |
Problems bearer | Ingest a problem report. Optional appHandle links the report to a Bump-managed app. |
| GET | /api/problems |
Session | Query stored reports. Filters: environment, application, fingerprint, from, to, limit, offset. |
| GET | /api/problems/{id} |
Session | Get one stored report. Accept: text/markdown renders it for pasting into a bug tracker. |
| POST | /api/problems/{id}/resolve |
Session | Mark one report resolved. |
| POST | /api/problems/{id}/unresolve |
Session | Clear the resolved flag on one report. |
| DELETE | /api/problems/{id} |
Session | Permanently delete one report. |
| POST | /api/problems/delete |
Session | Body { "problemKeys": [1, 2] }. Permanently delete a batch, 500 keys max, in one statement. Returns { "deleted": n }; keys already gone are not counted. |
| Method | Route | Description |
|---|---|---|
| POST | /api/auth/login |
Email + password (+ TOTP if enrolled). Sets session and CSRF cookies. |
| POST | /api/auth/logout |
Revoke the current session. |
| POST | /api/auth/password-resets |
Request a password reset email. Rate-limited per IP. |
| POST | /api/auth/password-resets/confirm |
Confirm a reset using the emailed token. |
Profile read/update, email change with confirmation, password change, and TOTP MFA setup / verify / disable, with recovery codes. See Swagger for shapes.
| Group | Routes |
|---|---|
| Owners | GET/POST /api/admin/owners, GET/PATCH/DELETE /api/admin/owners/{handle}, GET/DELETE .../subscribers. (Public POST /api/owners/{handle}/subscribers is the subscribe-form endpoint.) |
| Services | GET/POST /api/admin/services, GET/PATCH/DELETE /api/admin/services/{handle}, POST .../pause, POST .../resume, GET .../uptime, GET .../latency. |
| Outages | GET/POST /api/admin/outages, GET/PATCH /api/admin/outages/{id}, POST .../updates, POST .../resolve. |
| Announcements | GET/POST /api/admin/announcements, PATCH/DELETE /api/admin/announcements/{id}. |
| Apps (read-only) | GET /api/admin/apps — admin-UI listing; bearer-keyed mutations live at /api/apps. |
| Method | Route | Description |
|---|---|---|
| GET | /api/status |
Global status (cross-owner rollup). |
| GET | /api/status/owners/{handle} |
Per-owner status (services, outages). |
| GET | /api/status/global/announcements |
Active global announcements. |
| GET | /api/status/owners/{handle}/announcements |
Active per-owner announcements. |
| Method | Route | Description |
|---|---|---|
| POST | /api/owners/{handle}/subscribers |
Subscribe to an owner board (sends confirmation). |
| GET | /api/subscribers/confirm |
Confirm a subscription via the emailed token. |
| POST | /api/subscribers/unsubscribe |
Unsubscribe via the emailed one-click token. |
| Method | Route | Description |
|---|---|---|
| GET | /api/health |
Liveness probe (200 healthy / 503 unhealthy). |
All non-2xx responses use RFC 7807 application/problem+json:
{
"title": "App not found",
"status": 404,
"detail": "No app with handle 'does-not-exist'."
}Fields not relevant to a particular error are omitted.
Per-bearer-key fixed-window buckets. 429 responses include Retry-After.
/api/apps/*— 120 requests per minute per API key./api/problems/*— 600 requests per minute per API key.
POST endpoints accept an optional Idempotency-Key header. Resending the same key with the same body replays the cached response (Idempotent-Replayed: true) instead of re-running the handler.
- The key is any string up to 255 characters; a UUID is fine.
- Cached responses are kept for 24 hours;
Bump.Workersweeps expired rows. - Reusing a key with a different body is rejected with
422 Unprocessable Entity.
Idempotency applies to:
POST /api/appsPOST /api/apps/{handle}/version/bumpsPOST /api/problemsPOST /api/admin/outages,POST /api/admin/outages/{id}/updates,POST /api/admin/outages/{id}/resolvePOST /api/admin/announcementsPOST /api/owners/{handle}/subscribers
- Request body: 4 KB for app endpoints, 64 KB for problem reports.
- String fields are capped to match the column widths in
db/migrations/*.sql. Out-of-range input returns422 Unprocessable Entity. - Handles: lowercase letters, digits, single hyphens; start and end with a letter or digit; max 50 characters.
The probe HTTP client in Bump.Worker re-resolves DNS on every connection and rejects any address flagged as private, loopback, link-local, CGNAT, ULA, or multicast (ProbeAddressGuard). Auto-redirect is disabled so a 30x to an internal host cannot bypass the guard. URL validation in MonitorsController is best-effort because DNS can change between create and probe — the connect-time check is the authoritative barrier.
Bump follows Semantic Versioning 2.0.0. The version/bumps endpoint applies the SemVer reset rules atomically: bumping major resets minor and patch to 0; bumping minor resets patch to 0.
Bump expects to own a hostname and serve from the root, e.g. https://bump.example.com/api/.... Running behind a reverse proxy is fine, as long as it forwards to the root.
Sub-path hosting - mounting Bump at https://example.com/bump/... - is not supported, and there is no setting for it. Two reasons, in order of importance.
It puts Bump on a shared origin. Anything else served from the same hostname is same-origin with Bump, and path does not divide that boundary. An XSS in a neighboring app can read Bump's DOM and its CSRF cookie, which is deliberately not HttpOnly so the SPA can read it. A subdomain per app keeps each one in its own origin.
Two assumptions are baked into the build. web/vite.config.ts sets no base, so the bundled SPA fetches /assets/* from the root, and the session and CSRF cookies hardcode Path = "/". Prefix-aware routing alone would fix neither, so a partial fix would produce a blank status page and cookies visible to every other app on the hostname.
If sub-path hosting ever becomes a requirement, it is deliberate work - a Vite base, cookie paths derived from the mount point, and prefix-aware routing - not a configuration value.
Keys in config/appsettings.json / config/appsettings.work.json. Each key sits under the process that reads it - Bump:Api:* for the API, Bump:Worker:* for the worker - and only genuinely shared values stay at the Bump: root. Look at the path and you know which host restarts when you change it.
Defaults are not repeated at call sites. Each section binds to a class in src/Bump.Api/BumpSettings.cs whose property initializers are the defaults, so a value has one home and a section that loses a key during deploy-time substitution cannot silently fall back to a stale literal.
Deploy-time facts, read by both hosts:
| Key | Purpose |
|---|---|
Release:Environment |
Deployment label (work, demo, test, live). Not ASPNETCORE_ENVIRONMENT, which switches behavior. The API refuses to start when empty. |
Release:Version |
Deployed semver, surfaced in the probe user agent and on the About page. Stamped into the published appsettings.json by build/build.ps1; do not hand-edit. |
Serilog:MinimumLevel |
Log level and per-namespace overrides. Raise a level without cutting a release. |
Shared by the API and the worker:
| Key | Purpose |
|---|---|
Bump:Database:ConnectionString |
Postgres connection string. |
Bump:Mailgun:* |
Mailgun API key, domain, From, Region (us or eu). Optional; empty disables outbound email with a warning at boot. |
Bump:Services:* |
Probe interval, timeout, degraded-latency threshold, history bars, abuse contact, maintenance windows. |
Bump:Web:BaseUrl |
Public status-page URL, embedded in outgoing emails and the probe user agent. Both hosts refuse to start when empty. |
API only:
| Key | Purpose |
|---|---|
Bump:Api:LogPath |
Serilog file directory. Defaults to tmp/logs/api when empty. |
Bump:Api:Hosting:Urls |
Kestrel listen address. Refuses to start when empty. |
Bump:Api:Hosting:ClientSecret |
Bearer key for POST /api/problems. Same string every Bump.Sdk consumer presents. Refuses to start when empty. |
Bump:Api:AllowedOrigins |
SPA origin allowlist. |
Bump:Api:Security:Apps:ClientSecrets |
Bearer keys for /api/apps/**. Refuses to start when empty. |
Bump:Api:Security:Jwt:{Key,Issuer,Audience} |
JWT signing key, issuer, audience. |
Bump:Api:Security:Cookie:* |
Session cookie domain, SameSite, Secure. |
Bump:Api:Security:Tokens:* |
Password-reset and email-change link lifetimes, in hours. |
Bump:Api:RateLimits:* |
PermitLimit and WindowMinutes per policy: Apps, Problems, Auth, AuthLogin, Subscribe, Status. Both must be greater than zero. |
Bump:Api:Subscribers:MaxPerOwner |
Cap on confirmed subscribers per owner. |
Worker only:
| Key | Purpose |
|---|---|
Bump:Worker:LogPath |
Serilog file directory. Defaults to tmp/logs/worker when empty. |
Bump:Worker:Hosting:Urls |
Health-endpoint listen address. Refuses to start when empty. |
Bump:Worker:Alerts:PollSeconds |
Poll cadence; health turns unhealthy after 3× this without a tick. |
Bump:Worker:Alerts:Contact |
Recipient of alert-digest emails. |
Bump:Worker:Announcements:TickSeconds |
Announcement scheduler tick interval. |
pwsh build/build.ps1 -Version 1.2.3This:
- Builds the SPA with
npm ci && npm run build. - Stages
web/distintosrc/Bump.Api/wwwroot. - Publishes
Bump.Api,Bump.Sdk, andBump.Workerin Release mode. - Produces one
dist/<project>.<version>.zipper project.
bump/
├── bump.sln
├── src/
│ ├── Bump.Api/ # Web API + SPA host (packages, problems, auth, monitoring, status)
│ ├── Bump.Sdk/ # Client library for exception reporting
│ └── Bump.Worker/ # Probes, alert digests, announcement scheduler, idempotency sweep
├── tests/
│ └── Bump.Api.Tests/ # xUnit tests for Bump.Api
├── web/ # React + Vite + Tailwind SPA
├── build/
│ └── build.ps1 # Builds SPA + publishes dist/<project>.<version>.zip
├── db/
│ ├── migrations/ # SQL migrations (applied automatically on API boot)
│ ├── export-schema.ps1 # Regenerates schema.sql + schema.dot + schema.svg from the live DB
│ ├── schema.sql # Starting schema (generated)
│ ├── schema.dot # GraphViz ER diagram (generated)
│ ├── schema.svg # Rendered ER diagram (generated)
│ └── seed-admin.sql # Admin account seed template
├── docs/
│ └── exception-reporting.md # SDK exception-reporting guide
├── tools/ # Dev-loop scripts (start, stop, reset-database, restore-database)
├── dist/ # Release artifacts (gitignored)
└── README.md
Released under the MIT License.