Adding A Data Source
This is one of the most important contributor workflows in Blackbox.
Understand The Current Model First
Today, built-in source types are defined centrally and include:
- A stable type string
- A scope:
agentorserver - Singleton behavior
- A human-readable name and description
- A mechanism label used by the catalog UI
Current built-ins include:
docker— virtual agent source (no DB row, always present)systemd— agent-scoped, requiresWATCH_SYSTEMD=trueon the agentfilewatcher— agent-scoped, requiresWATCH_PATHSconfigured on the agentwebhook_uptime_kuma— server-scoped webhook source (singleton)webhook_watchtower— server-scoped webhook source (singleton)webhook_komodo— server-scoped webhook source (non-singleton; one instance per Komodo deployment)
Design Questions To Answer
Before writing code, decide:
- Is the source agent-scoped or server-scoped?
- Is it a singleton per target?
- Does it require secrets?
- What config shape should be stored?
- What capability string, if any, should gate it?
- What event shapes will it emit?
- How should incidents use its evidence?
Touchpoint Map
Every source type touches at least six locations across the codebase. Use this as a checklist.
Backend
| File | What to change |
|---|---|
server/internal/handlers/admin_sources.go | Add entry to knownSourceTypes; add config validation case in validateSourceConfig; add redaction key in sensitiveKeysFor if storing secrets |
server/internal/models/data_source.go | Add the type string to agentScopedSingletonSourceTypes or serverScopedSingletonSourceTypes |
server/internal/handlers/datasource_migration.go | Add startup seeding logic if the source should be auto-created on first run or migrated from legacy config |
server/main.go | For singleton webhook sources: add a PrimeWebhookSecretCache call and a route using middleware.WebhookAuthFunc. For non-singleton webhook sources: add a PrimeWebhookSecretsCache call and a route using middleware.WebhookAuthFuncMulti |
Frontend
| File | What to change |
|---|---|
web/src/components/sourceIcons.ts | Add the type to SourceVisualType, SourceIconName, SOURCE_CARD_COLORS, and SOURCE_ICON_SPECS |
web/src/components/SourceIcon.tsx | Add a render branch for the new icon name; brand icons need a custom SVG component, generic icons need a Lucide mapping |
web/src/pages/SourceCatalog.tsx | Add a buildDefaultConfig case so the catalog pre-fills config when the user clicks Add |
web/src/pages/DataSourcesGroup.tsx | Add an entry to DOCS_URLS so the edit panel shows a link to the setup guide; without this the banner renders nothing |
Three Flows In Detail
Flow 1: Agent-Scoped Source (e.g. systemd, filewatcher)
Agent-scoped sources are collected by the agent process and pushed to the server. The UI only offers them when the agent advertises the capability.
Capability handshake. The agent builds its capability list in agent/main.go in buildCapabilities. Add your capability string there so the agent advertises it on heartbeat. The server stores capabilities on the Node row. SourceCatalog.tsx filters the add-source dialog at render time: types not in caps are shown as "unavailable" until the agent is updated. If you skip this step, the source will appear greyed out on every node even when the agent is running it.
Config flow for agent-scoped sources:
- Add the type to
knownSourceTypesinadmin_sources.gowithScope: "agent". - Add the type string to
agentScopedSingletonSourceTypesindata_source.go. - Add validation in
validateSourceConfig. If config needs normalization (likesystemd's unit-name appending), do it there so both create and update paths benefit. - Add a
buildDefaultConfigcase inSourceCatalog.tsxso the catalog dialog pre-fills the form. - Add source icon and colors in
sourceIcons.ts. - Implement the agent-side watcher or collector and push events via
/api/agent/push.
Startup seeding. If the source should be auto-created for every capable node on first startup (like filewatcher), add logic in datasource_migration.go. The migration runs inside a transaction on every startup and is gated by the data_sources_migrated app setting key so it only seeds once.
Flow 2: Server-Side Webhook Source
Webhook sources receive events over HTTP rather than from an agent. They require a secret for authentication. There are two sub-flows depending on whether the source is singleton or non-singleton.
Config and validation. Types with the webhook_ prefix automatically get secret validation (non-empty string required) and redaction (zeroed before the config is returned to the UI) because validateSourceConfig and sensitiveKeysFor both key off that prefix. New webhook types get both behaviors for free if they follow the naming convention.
Handler. Add a handler function (e.g. WebhookMyService) that parses the inbound payload and emits normalized timeline entries via eventHub.
Startup seeding. Webhook sources are only auto-seeded when WEBHOOK_SECRET was set at startup (legacy behavior). New installs create them explicitly via the catalog. Add a seeding block in datasource_migration.go only if you need parity with the existing webhook sources.
Flow 2a: Singleton Webhook (e.g. webhook_uptime_kuma, webhook_watchtower)
One instance per server. The secret lives in a single DataSourceInstance row.
Route wiring in server/main.go:
- A
PrimeWebhookSecretCachecall at startup so the secret is hot before the first inbound request. - A route using
middleware.WebhookAuthFuncwrappinghandlers.GetCachedWebhookSecretfor your source type. The cache invalidates automatically when the source is created, updated, or deleted via the admin API.
Singleton slice. Add the type to serverScopedSingletonSourceTypes in server/internal/models/data_source.go.
Flow 2b: Non-Singleton Webhook (e.g. webhook_komodo)
Multiple instances of the same type can coexist — each with its own secret, and optionally its own per-instance config (e.g. event filters, node mappings). Instances are distinguished by their secret at the auth layer.
Route wiring in server/main.go:
- A
PrimeWebhookSecretsCachecall at startup (note:SecretsCache, plural) to load all instance secrets into the multi-instance cache. - A route using
middleware.WebhookAuthFuncMultiwrappinghandlers.GetCachedWebhookSecrets(plural). The middleware checks the incomingX-Webhook-Secretheader against all enabled instance secrets and injects the matched source ID into the request context.
Handler. Call middleware.WebhookSourceIDFromContext(r.Context()) to retrieve the matched source ID, then load that instance's config from the DB to read per-instance settings.
Do not add the type to serverScopedSingletonSourceTypes. Non-singleton sources are intentionally absent from that slice — the singleton enforcement in CreateSource is skipped for them.
Flow 3: Virtual Source (e.g. docker)
Virtual sources have no DataSourceInstance row. They are always present on capable nodes and cannot be created or deleted via the catalog UI.
- Add to
knownSourceTypesas normal so the catalog shows the card. - Block creation in
CreateSourcewith a guard like the existingdockercheck. - Keep the type in
agentScopedSingletonSourceTypesforIsSingletonSourceTypecompatibility. - The catalog marks it as
virtualand renders a "Built-in" badge instead of an Add button.
Implementation Steps
1. Register The Source Type
Add the entry to knownSourceTypes in server/internal/handlers/admin_sources.go. The Type string becomes the stable identifier used everywhere.
2. Decide Scope And Singleton Rules
Add the type string to the correct slice in server/internal/models/data_source.go (agentScopedSingletonSourceTypes or serverScopedSingletonSourceTypes). The handler uses these slices to enforce singleton constraints at create time.
3. Add Config Validation
Add a case to validateSourceConfig in admin_sources.go. Cover:
- Required keys
- Allowed types
- Normalization rules (e.g. trimming, deduplication)
4. Add Redaction Rules If Needed
If the source stores credentials or tokens, add the key name to sensitiveKeysFor in admin_sources.go. Redacted fields are zeroed before the config is returned to the UI. The mergeSourceConfig function also uses this list to preserve existing secret values when a client sends a blank field on update.
5. Implement The Ingestion Path
Choose the correct side:
- Agent-side watcher or collector for node-local sources; push via
/api/agent/push - Server-side webhook handler for central sources; wire the route in
server/main.go
For singleton webhook sources, wire PrimeWebhookSecretCache at startup and add a route with middleware.WebhookAuthFunc as described in Flow 2a. For non-singleton webhook sources, use PrimeWebhookSecretsCache and middleware.WebhookAuthFuncMulti as described in Flow 2b.
6. Add Capability Advertising (Agent-Scoped Only)
Update buildCapabilities in agent/main.go to include your capability string when the source is active. Without this, the UI shows the source as unavailable on all nodes even when the agent is running it.
7. Emit Normalized Entries
New source entries should fit the timeline shape well:
- Stable
source - Useful
service - Useful
event - Clear
content - Structured
metadata
8. Update The Frontend
In web/src/components/sourceIcons.ts:
- Add the type to
SourceVisualTypeandSourceIconName - Add a color entry in
COLORSandSOURCE_CARD_COLORS - Add an icon spec in
SOURCE_ICON_SPECS— usekind: 'brand'for a custom SVG,kind: 'generic'for a Lucide icon
In web/src/components/SourceIcon.tsx:
- If
kind: 'brand', add anif (spec.name === '...')branch with the SVG component (seeDockerMark,UptimeKumaMark,WatchtowerMarkfor examples). Without this branch, the icon falls back to aCircleHelpat runtime. - If
kind: 'generic', add anif (spec.name === '...')branch with the Lucide icon component if you added a newSourceIconName. Existing Lucide-backed names (systemd→Cog,filewatcher→FileSearch) don't need a new branch.
In web/src/pages/SourceCatalog.tsx:
- Add a
buildDefaultConfigcase returning the pre-filled config shape for your type
In web/src/pages/DataSourcesGroup.tsx:
- Add an entry to
DOCS_URLSmapping your type string to the full URL of its docs page. The edit panel banner renders nothing if the entry is missing — no error, just no link.
9. Handle Startup Seeding And Migration
If the source should be auto-created on first run, add logic in server/internal/handlers/datasource_migration.go. The function runs inside a transaction and is safe to call on every startup. It uses the data_sources_migrated app setting key to ensure seeding only happens once.
10. Add Tests
At minimum, cover:
- Source type listing
- Create and update validation
- Secret preservation or redaction if used
- Ingestion behavior
- Incident or timeline side effects when relevant
11. Update Docs
Add or update:
- A user-facing page under
Data Sources - Any deployment or configuration pages touched by the change
- This contributor guide if the source workflow itself changed
Practical Advice
- Prefer a source model that operators can explain in one sentence.
- Keep config small and explicit.
- Reuse existing event shapes when possible instead of inventing completely new semantics.
- The
webhook_prefix convention gives you secret validation and redaction for free — use it for any HTTP-inbound source. - If your agent-scoped source needs an env-var gate (like
WATCH_SYSTEMD), add the check inbuildCapabilitiesso the UI reflects availability correctly. - Document capability expectations early. The capability handshake is easy to miss and causes confusing "unavailable" states in the UI.