Read the roster from a registry table
Module 1 read the catalog. Module 2 read your config. Both work great — as long as the database names tell you who belongs on the roster. But some fleets don’t work that way. fleet_tenant_001 and fleet_tenant_005 sit side by side in the catalog, spelled identically, and one’s a live paying customer while the other’s a decommissioned trial nobody’s dropped yet. The name doesn’t know the difference. The truth lives somewhere else — in a table your platform team already keeps.
Hey folks. I’m Forge Barrett, master of the Content Forge here at SchemaSmith.
Today we read the roster off a control-plane registry — a real table, Tenants(DbName, Active), that names who’s live and who isn’t. The names aren’t stamped on the metal; they’re written in the ledger. And we point discovery straight at that ledger. Let’s stoke the fire.
Point discovery at the ledger
Section titled “Point discovery at the ledger”Discovery runs a query. So far that query hit the catalog. Now we aim it at a table you own instead. Two lines in Template.json do it:
"DatabaseIdentificationScript": "SELECT DbName FROM dbo.Tenants WHERE Active = 1 ORDER BY DbName","IdentificationDatabase": "{{ControlDb}}"DatabaseIdentificationScript is the roster query — pull every active tenant out of Tenants. IdentificationDatabase says which database to run that query in — the one holding your registry, named by a token so each environment can point at its own. The token resolves from the package default the moment the run kicks off:
Version: 2.6.0.0 Product Script Tokens: ControlDb: FleetRegistry_DevControlDb: FleetRegistry_Dev — discovery will read the roster from that registry. Quench it:
schemaquench --ConfigFile:quench.settings.json[localhost,11433].[fleet_tenant_001] Dispatching work unit (source: db=DatabaseIdentificationScript, schema=(regular template))[localhost,11433].[fleet_tenant_002] Dispatching work unit (source: db=DatabaseIdentificationScript, schema=(regular template))[localhost,11433].[fleet_tenant_003] Dispatching work unit (source: db=DatabaseIdentificationScript, schema=(regular template))[localhost,11433].[fleet_tenant_004] Dispatching work unit (source: db=DatabaseIdentificationScript, schema=(regular template))Four tenants dispatched — and source: db=DatabaseIdentificationScript names where each one came from: the registry query, not a config list. Count them: four, not five. fleet_tenant_005 exists on the server, but its registry row is marked Active = 0, so the roster query never returned it. The catalog would’ve handed you all five. The ledger handed you the four that are actually live. That’s the whole point — the registry decides, and the database names don’t get a vote.
What IdentificationDatabase moves — and what it doesn’t
Section titled “What IdentificationDatabase moves — and what it doesn’t”Here’s the part to keep straight, because it trips people who assume the setting does more than it does. IdentificationDatabase re-targets one query: the enumeration that discovers your roster. That’s it. It doesn’t redirect the rest of the run.
Provisioning a tenant database that doesn’t exist yet, the existence checks that decide whether to create one, and SchemaIdentificationScript — the schema-discovery query — all still run against the template’s init database, exactly as they always have. Only the roster query moves to the registry. So the mental model is: “go read the guest list from over there, then do all the actual work right here.” One query travels to the registry; everything downstream stays home. (The Schema Packages reference is the authoritative word on which queries IdentificationDatabase retargets and which it leaves on the init database — keep it open the first time you wire one up.)
Miss that boundary and you’ll go hunting for a registry table in the wrong place, or expect schema discovery to follow the roster query across. It doesn’t. Enumeration moves. Nothing else.
On PostgreSQL, this is the only way
Section titled “On PostgreSQL, this is the only way”On SQL Server, MySQL, and MariaDB, IdentificationDatabase is a convenience — you could three-part-name the registry right inside the roster query and skip the setting. PostgreSQL doesn’t give you that out. A PostgreSQL connection is bound to a single database; there’s no cross-database query. So if your registry lives in fleetregistry_dev and your connection landed in postgres, the query simply can’t see the table.
Watch what happens when you blank IdentificationDatabase out ("") on PostgreSQL — the roster query falls back to the init database, postgres, where no registry table exists:
schemaquench --ConfigFile:quench.settings.json --PreviewTargetsPre-flight diagnostics for Shop (--PreviewTargets)Locate Databases To Quench (localhost)[localhost] Database enumeration FAILED for template 'Main': 42P01: relation "public.tenants" does not exist ERROR: matched 0 targets for required template 'Main' - no databases or schemas were discovered42P01: relation "public.tenants" does not exist — the table’s right there in fleetregistry_dev, but this connection is in postgres and can’t reach across to it. Restore IdentificationDatabase: "{{ControlDb}}" and enumeration runs inside fleetregistry_dev, the table’s local, and the roster resolves. That’s why on PostgreSQL IdentificationDatabase isn’t a nicety — it’s the only door into a registry table at enumeration time.
One package, two registries
Section titled “One package, two registries”Now the payoff of naming the registry with a token. {{ControlDb}} isn’t a constant — it’s a dye you swap per environment. Dev leans on the package’s own default — FleetRegistry_Dev; prod’s config overrides the token to FleetRegistry_Prod. One canonical package, two registries, two different rosters — and the package never learns which one it’s talking to.
Point the same build at prod and preview the targets:
schemaquench --ConfigFile:quench.settings.prod.json --PreviewTargetsTemplate: Main [required] db: fleet_tenant_001 db: fleet_tenant_002 db: fleet_tenant_003 db: fleet_tenant_004 db: fleet_tenant_005Five this time. Prod’s registry marks fleet_tenant_005 active, so it’s on the roster here even though dev dropped it. Same tables, same roster query, same package you tested in dev — the only thing that moved is which registry the token pointed at. Define once, deploy everywhere, and let each environment’s ledger name its own fleet.
Take a tenant offline without touching the package
Section titled “Take a tenant offline without touching the package”Because the roster lives in a table, changing it is an UPDATE — not a redeploy, not a config edit, not a new build. Flip a tenant’s Active flag and the next run honors it. Take fleet_tenant_004 offline in FleetRegistry_Dev:
UPDATE dbo.Tenants SET Active = 0 WHERE DbName = 'fleet_tenant_004';Re-run against the same dev config — nothing about the package changed — and the roster shrinks on its own:
[localhost,11433].[fleet_tenant_001] Dispatching work unit (source: db=DatabaseIdentificationScript, schema=(regular template))[localhost,11433].[fleet_tenant_002] Dispatching work unit (source: db=DatabaseIdentificationScript, schema=(regular template))[localhost,11433].[fleet_tenant_003] Dispatching work unit (source: db=DatabaseIdentificationScript, schema=(regular template))Three tenants now. 004 didn’t fail, didn’t error, didn’t get skipped with a warning — its row said Active = 0, so the roster query never named it, and the run never reached for it. Onboarding and offboarding become one-row edits in a table your platform team already governs. (On PostgreSQL the same edit is UPDATE public.tenants SET active = false WHERE dbname = 'fleet_tenant_004' — lowercase, boolean.)
One caution, and it’s a good one. An empty registry is not a quiet do-nothing. Point the run at a registry table with zero rows and SchemaSmith refuses out loud:
schemaquench --ConfigFile:quench.settings.empty.jsonNo database targets discovered for template 'Main' (RequireAtLeastOneTarget: true)Exit code 2, nothing deployed. A roster query that comes back empty is almost never what you meant — a truncated registry, a token pointed at the wrong database, a WHERE Active = 1 that matched nobody. RequireAtLeastOneTarget turns that into a hard stop instead of a green run that touched zero tenants and left you thinking it worked. An empty ledger is an error worth shouting about, and SchemaSmith shouts.
Check yourself: Your run finishes clean, but a tenant you expected got nothing — the deploy just skipped right past it. The roster query is returning rows, so discovery clearly ran. Where do you look?
Not in the package — the package is canonical and doesn’t name tenants. Look at the registry. Two prime suspects: that tenant’s row is marked Active = 0 (or false on PostgreSQL), so the roster query never returned it; or the {{ControlDb}} token in this environment’s config resolved to the wrong registry — you read a real roster, just not the one you meant. Both live in the control plane, not the build. Check the tenant’s Active flag first, then confirm which database ControlDb pointed at. The package did exactly what it was told; the ledger told it something you didn’t expect.
Reading from the ledger
Section titled “Reading from the ledger”Module 1 let the catalog name the fleet. Module 2 let your config name it. This module lets a table you own name it — the registry, read by DatabaseIdentificationScript and pointed at with IdentificationDatabase, resolved per environment through a single token. Remember the boundary: only the enumeration query travels to the registry; provisioning and schema discovery stay on the init database. And remember why PostgreSQL leaves you no other road — one connection, one database, so the registry setting is the only way in. Onboard, offboard, or fence off a whole environment with a one-row UPDATE, and let an empty roster stop the run cold instead of sliding by.
A smith working a big commission doesn’t carry the order in his head, and he doesn’t read it off the blanks stacked by the forge — they all look the same cold. He keeps a register: who ordered what, which pieces are still live, which got cancelled. He works from the book, not the pile. That’s a registry-driven fleet — the names live in the ledger, the ledger says who’s active, and every run reads the current truth before a single hammer falls.
Running a control-plane table your deployments should be reading but aren’t yet? Email me at forgebarrett@schemasmith.com — tell me how your platform tracks live tenants today, and I’ll show you where IdentificationDatabase plugs into it.
Until then, keep the ledger honest, and let every name it holds come off the fire hardened.
— Forge