How-to guide
Register a database
Registering a database tells Workbench the shape of the machine your PostgreSQL runs on — its Target environment. Findings account for that shape, so a recommendation for a 4 vCPU managed instance differs from one for a 64-core bare-metal box. This is operator-declared metadata; Workbench never connects to the database to discover it.
This page covers how PostgreSQL targets are registered in Workbench and the per-database fields it tracks. Workbench stores identity, presentation, and routing metadata only — it never stores database connection credentials and never connects to your database. The credentials used to collect from a database belong to Signals; the analyzer reaches the database through Signals, not through Workbench.
Most databases register themselves: when the analyzer imports a snapshot, Workbench creates the database record automatically if one does not already exist. You can also register a database manually to pre-configure it before the first snapshot arrives.
What Workbench stores — and what it doesn't
Workbench keeps a database's identity, how you present it, and how its findings are routed. It holds no connection secret of any kind:
- No password, connection string, DSN, or TLS client key.
- No live connection test — Workbench never opens a connection to
a customer database. (Earlier releases carried a
database_connectionstable and a connection-test endpoint; both were removed.) - Collection credentials and connectivity are configured in Signals, which owns them end to end.
How a database gets registered
Automatically, from an analyzer import (the common path). When the
analyzer imports a snapshot and no matching record exists yet,
Workbench creates one. Identity is the collected cluster
(host:port) plus the PostgreSQL database name; a later import for
the same identity reuses the existing row rather than creating a
duplicate. A freshly auto-created row takes its alias from the
database name and an environment of other — both editable
afterwards.
Manually, before the first snapshot. Use New database
(/databases/new) to pre-register a target so you can set its alias,
environment, routing, and Target environment ahead of collection. The
manual form is identity-only; it has no connection fields, because
Workbench stores none.
Fields: editable vs collected
| Field | Source | Editable |
|---|---|---|
Alias (name) | you (defaults from the database name) | yes |
environment | you | yes |
| Routing / integration | you | yes |
| Target environment (CPU / RAM / storage / platform / workload) | you | yes — see below |
| PostgreSQL database name | collected | read-only |
Cluster identity (host:port) | collected | read-only |
Engine (postgresql) | collected | read-only |
| Version | collected from the import | read-only |
Collected fields are observed by Signals and arrive with the import; they identify the target and cannot be hand-edited.
What is the Target environment?
The Target environment is the operator-declared shape of the database the analyzer is reasoning about: CPU / RAM / storage class / IOPS / throughput, plus the platform (self-hosted, RDS, Aurora, Cloud SQL, Azure Flex, AlloyDB) and a workload hint (OLTP / OLAP / mixed).
Operators declare it on the /databases page; the analyzer
consumes it when it runs against the target. The contract is
deliberately operator-declared, never collected — Signals
collectors stay read-only PG-catalog observers. This is enforced
by the analyzer-side targetcontext spec
(TC-R001 (operator-declared, never collected)).
Every field is optional. When you don't declare a field, the analyzer falls back to a conservative generic recommendation rather than producing a precise but unsupported one — there's no penalty for leaving a field blank.
Editing through the UI
/databases is the read-only registry. Each row carries an
Edit target link that opens the form at
/databases/<id>/target/edit. The form has two ways to fill in
the values:
- Cloud preset (recommended for managed Postgres) — a
curated dropdown of ~25 popular RDS / Aurora / Azure Flex /
Cloud SQL / AlloyDB SKUs. Picking a SKU auto-fills the numeric
- platform + storage fields from the catalogue
(
web/data/cloud-presets.json). Every field stays editable after the dropdown applies, so operators can correct any drift between the curated value and their actual instance.
- platform + storage fields from the catalogue
(
- Manual entry — operators that don't see their SKU in the dropdown (self-hosted, exotic instance, or a SKU not in our curated list) leave the dropdown on "— Manual entry —" and fill in the fields directly. This is a first-class path; the dropdown is an ergonomics shortcut, not a gate.
Save updates the database's target row; the analyzer reads it
on the next run.
Editing through the API
For automation, use the target-only PATCH endpoint:
curl -fsS -X PATCH "$WORKBENCH/api/databases/$DB_ID/target" \
-b /tmp/wb.cookies \
-H 'Content-Type: application/json' \
-H "X-Workbench-CSRF: $(grep workbench_csrf /tmp/wb.cookies | awk '{print $7}')" \
-d '{
"cpu_cores": 8,
"ram_gb": 32,
"storage_type": "cloud_managed",
"iops": 12000,
"throughput_mbps": 750,
"platform": "rds",
"workload": "oltp",
"cloud_preset_sku": "db.m6i.2xlarge"
}'
Semantics:
- Empty body / all-empty target — clears the row. Useful for resetting a declaration back to "operator did not declare".
- Populated body — upserts. Fields not supplied keep their empty-string / NULL sentinel.
Validation rejects out-of-enum strings (storage_type,
platform, workload) with DATABASE_VALIDATION_ERROR. Numeric
fields accept wide-but-pragmatic ranges (1..4096 cores,
0..32 TB RAM, 1..10M IOPS) — the goal is to catch obvious
nonsense without second-guessing high-end servers.
Field reference
| Field | Type | Allowed values | Empty / unknown |
|---|---|---|---|
cpu_cores | integer | 1..4096 | absent / 0 |
ram_gb | float | > 0..32768 | absent / 0 |
storage_type | closed enum | nvme | ssd | hdd | cloud_managed | "" |
iops | integer | 1..10000000 | absent / 0 |
throughput_mbps | integer | 1..1000000 | absent / 0 |
platform | closed enum | self_hosted | rds | aurora | cloud_sql | azure_flex | alloydb | "" |
workload | closed enum | oltp | olap | mixed | "" |
cloud_preset_sku | string | ≤ 64 printable bytes (the dropdown's SKU label, e.g. db.m6i.xlarge) | "" |
The closed-enum strings here are the wire-and-storage contract.
A Vitest drift gate
(web/components/databases/__tests__/target-context-drift.test.ts)
pins the TypeScript constants against the Go source-of-truth
(internal/workbench/databases/validate.go); any drift fails CI.
Which rule consumes which field
The Target environment exists so analyzer rules can produce hardware-aware findings instead of generic ones. As of this release the consumer wiring is:
Currently consumed
-
storage_type— consumed byio.cost.calibration.v1(analyzer-side detector). The rule readsTargetContext.Storageto recommend a storage-class-appropriaterandom_page_cost— NVMe drives a lower value (typically1.1), SSD a moderate one (1.5..2.0), HDD the conservative default (4.0). Whenstorage_typeis blank, the rule only fires when the observed cache-hit ratio is high enough to recommend the safe middle-ground value without making a storage-class claim. Spec:io-cost-calibration(analyzer detection specification). -
platform— used by the analyzer's multi-platform DDL dialect: when a recommendation involves changing a parameter that's set via cloud-provider parameter groups (RDS, Aurora, Cloud SQL) the rule emits a DDL snippet in the right dialect rather than a genericALTER SYSTEMthat the operator can't apply.
Operator-declared today, consumed by future rules
The remaining fields are persisted on the analyzer envelope
(the analyzer-workbench-import-v1 specification
§ Target context, AWI-R250..R259) and surfaced to the analyzer,
but no named v0.1 detector consumes them yet. They're
operator-declared today so the data is in place when the rules
that need them ship:
cpu_cores— future parallelism / worker-count tuning.ram_gb— futureshared_buffers/work_mem/effective_cache_sizerecommendations.iops+throughput_mbps— futureeffective_io_concurrency+ checkpoint pacing rules, plus stricterio.cost.calibration.v1priors for cloud-managed elastic-IOPS volumes.workload— future OLAP-specific recommendations (parallel scan tuning, partitioning hints) versus OLTP-only ones.
Filling these in early costs you nothing — the analyzer ignores unknown fields cleanly — and means hardware-aware findings start appearing automatically as new rules ship.
Cloud-preset catalogue maintenance
The dropdown's catalogue lives at web/data/cloud-presets.json
and is curated on a quarterly review cadence (the file carries
its own last_reviewed field). The curated list is pragmatic
(~25 SKUs covering the popular tiers across RDS / Aurora / Azure
Flex / Cloud SQL / AlloyDB) — not an exhaustive cloud reference.
A missing SKU is not a registration failure: the manual-entry
fields below the dropdown are the canonical input surface.
Audit trail
Every target update emits a database.target_updated audit event
carrying user_id + database_id. The closed-enum allowlist
(internal/workbench/audit/audit.go) prevents any other field
leaking into the audit row.