Three levels and one attachment point. The attachment point — which workspaces reach which catalog — is where environment separation stops being a naming convention and becomes a boundary.
A metastore sits at the top of the hierarchy and is pinned to a cloud region. A workspace is attached to exactly one metastore, and in recent accounts a metastore is provisioned automatically for each region in which you have a workspace. Nothing prevents an account admin from creating a second metastore in a region — Unity Catalog allows it, Databricks recommends against it, and so a second one is almost always something that happened rather than something that was decided. This warehouse serves DE, AT, CH and NL from one European region, so the target state is one metastore and three catalogs.
Why
There is no cross-metastore query. You cannot join a table in one metastore to a table in another; the supported path is Delta Sharing, even between two metastores in the same account. So a second metastore converts every cross-domain join into a share with its own lifecycle, its own grants and its own latency.
Deliberate second metastores exist and are usually a data-residency requirement. The common version is accidental: someone spins up a workspace in a second region for a proof of concept, it is assigned a fresh empty metastore, and six weeks later there are tables nobody can join to the warehouse. Nothing errored at any point.
How
textThe securable hierarchy, and what sits beside it
metastore one per region, account-level
|
|-- catalog dwh_dev visible from: dev workspace (read-write)
|-- catalog dwh_test visible from: dev + CI workspaces (read-write)
|-- catalog dwh_prod visible from: prod workspace (read-write)
| | BI workspace (READ-ONLY)
| |-- schema bronze / silver / gold
| | '-- table · view · volume · function · model
|
'-- storage credential + external location
metastore-level securables. NOT inside a catalog — which is why
"who may create one" is an account question, not a catalog one.
Create the metastore without root storage and give each catalog its own managed storage location instead. Then dwh_dev and dwh_prod keep their managed files in different containers, and deleting a catalog deletes a bounded thing. A metastore with root storage puts every environment's managed data in one place, so the separation exists in Unity Catalog and nowhere underneath it.
Doing it
Count the regions you must serve, not the teams that want their own spaceOne metastore per region, no more.
Create the metastore with no root storage; set a managed storage location per catalog
Make metastore admin a group with two or three membersIt can grant anything on anything, so it is not a working account.
Before agreeing to a workspace in a second region, price the Delta Share and the loss of the direct join
Embedding it
Have the team draw the hierarchy from memory in week two, including where external locations sitWhoever puts them inside a catalog is the person who will later grant one wrongly.
Put the client's platform owner in the metastore-admin group on day oneYou work as a catalog owner from the start, which makes the handover a non-event.
Ask them what happens if another department stands up a workspace in another region next quarterThe right answer is an architecture conversation, not a shrug.
Defaults
One metastore per region; one region if the business allows it
No metastore root storage — managed storage declared per catalog
Metastore admin is a small group with a documented way in and out
Gotchas
A second metastore appears the day someone creates a workspace in a second regionNo error, no warning — just a fresh empty metastore whose tables can never be joined to the warehouse. It is discovered by an analyst writing a join, not by an alert, and the fix is a share or a migration.
Metastore root storage lands every catalog's managed data in one containerSo 'dev is separate from prod' is true in the catalog list and false at the storage layer, and cleanly deleting a decommissioned environment's data is no longer possible.
Using the metastore-admin account for daily workIt bypasses every ownership boundary below it, so grants it issues are invisible to a review built around catalog owners, and the audit trail records that an admin did it and nothing about why.
Assuming the metastore is per workspaceIt is account-level and shared by every workspace attached to it — so an external location or storage credential 'created in dev' exists for the production workspace as well. You find out when a dev engineer lists the production landing container and is surprised it worked.
Grants decide who may read a catalog. Binding decides which workspaces can see it at all. A catalog you create defaults to OPEN — every workspace on the metastore — so until someone changes it, the boundary between development and production is a set of grants and nothing else. There is one documented exception, and it is the misleading kind: the default catalog auto-created with a new workspace is bound to that workspace alone, and its owner is the workspace-local workspace admins group, which cannot be used from any other workspace. So the one catalog nobody designed looks properly isolated, and every catalog you designed does not.
None of this is SQL — and that is the more useful fact, not a smaller one. ALTER CATALOG has no isolation clause at all: the documented clauses are DEFAULT COLLATION, [SET] OWNER TO, SET/UNSET TAGS, {ENABLE|DISABLE|INHERIT} PREDICTIVE OPTIMIZATION, OPTIONS, SET MANAGED LOCATION and [SET] RETAIN DROPPED TO. The ALTER CATALOG … SET ISOLATION MODE = ISOLATED line that circulates in blog posts and answer threads is a parse error, not a permission error. Isolation mode is set from the CLI, from Catalog Explorer, from the REST API or from Terraform — and so is the workspace list.
Why
'Could someone in dev read production?' With grants alone, answering it means auditing every grant ever issued, and the answer is true until the next one. With binding it is one configuration you can put in front of an auditor: the catalog is not mounted in that workspace, so there is nothing to grant and nothing to review.
Read-only binding is the underrated half. The BI workspace runs refresh service principals that should never write to gold. Read-only binding makes that structural instead of depending on which privileges someone remembered not to grant.
How
bashStep one: take the catalog off the metastore-wide default
# dwh_prod stops being visible from every workspace on the metastore
databricks catalogs update dwh_prod --isolation-mode ISOLATED
# the same call over REST, for a pipeline with no CLI on it
# DATABRICKS_HOST carries the scheme: https://adb-....azuredatabricks.net
curl -sS -X PATCH \
-H "Authorization: Bearer $DATABRICKS_TOKEN" \
-H "Content-Type: application/json" \
"$DATABRICKS_HOST/api/2.1/unity-catalog/catalogs/dwh_prod" \
-d '{"isolation_mode": "ISOLATED"}'
That call alone leaves the catalog reachable only from the workspace that owns it — which is a reliable ten minutes of confusion, because the error looks like the catalog was dropped. The second call says which workspaces are on the list, and whether each gets read-write or read-only.
bashStep two: the workspace list, as code
# read-write for the production job workspace,
# read-only for the BI workspace that must never write to gold
# DATABRICKS_HOST carries the scheme: https://adb-....azuredatabricks.net
curl -sS -X PATCH \
-H "Authorization: Bearer $DATABRICKS_TOKEN" \
-H "Content-Type: application/json" \
"$DATABRICKS_HOST/api/2.1/unity-catalog/bindings/catalog/dwh_prod" \
-d '{
"add": [
{"workspace_id": 2468013579, "binding_type": "BINDING_TYPE_READ_WRITE"},
{"workspace_id": 1357924680, "binding_type": "BINDING_TYPE_READ_ONLY"}
]
}'
External locations and storage credentials take the same treatment through the same mechanism. A credential that can reach the production landing container does not need to be usable from the dev workspace.
Carry one consequence into the review. Every other control in this route is SQL you can read in the repository — an ALTER … OWNER TO, a GRANT, a COMMENT — and every one of them is visible in information_schema afterwards. Binding is neither. A reviewer who reads the deployment SQL and queries the catalog metadata sees a complete, tidy governance scheme and no evidence whatsoever that dev cannot read prod, because the answer lives in Terraform state or in a click nobody recorded. So make the evidence explicit: have the same pipeline that applies the binding capture GET /api/2.1/unity-catalog/bindings/catalog/dwh_prod and keep the response.
Doing it
Set every catalog to ISOLATED, then add workspaces explicitlyUse databricks catalogs update, the catalogs API or the Terraform resource. Default-open is the first thing to reverse, before there is anything worth reading.
Keep it out of the SQL migrations entirelyThere is no ALTER CATALOG clause for it, so a migration file is the one place the statement cannot live.
Bind the BI workspace read-only, and any analyst workspace too
Manage the workspace list in Terraform or the bundle deployA click is invisible to review and gets reproduced differently in the next environment.
Have the deploy capture GET /api/2.1/unity-catalog/bindings/catalog/dwh_prod and store the responseIt is the only artefact that proves the control exists.
Deal with the auto-created default workspace catalog deliberatelyRe-own it to an account group or drop it. Left alone it belongs to a workspace-local group that the rest of your model cannot address.
Embedding it
Make them prove it rather than assert itRun a SELECT against dwh_prod.gold.fct_order_line from the dev workspace and keep the failure. That screenshot is your access-review evidence six months later.
Ask them to demonstrate the binding from the repository aloneThey cannot — there is no SQL to grep and no information_schema column to query — and that dead end teaches why this one control needs its own captured evidence better than any slide.
Hand them the CLI and API calls, not the UI click path, so the control is something they can diff and re-apply
Add binding to the new-workspace checklist and let them run it unaided the second time
When someone asks for prod access in dev 'just to compare', have the team offer the alternative themselvesA read-only bound workspace — offered by them, instead of escalated to you.
Defaults
ISOLATED on every catalog; the workspace list held as code
Isolation mode and bindings applied by CLI, API or TerraformSQL expresses neither, so neither belongs in a migration.
Read-only binding for every workspace that only consumes
Binding on top of grants, never instead of themIt restricts where a catalog is visible, not who may read it there.
Binding reviewed as part of every new-workspace request, because a new workspace starts with none
The bindings API response kept as evidence, because the catalog metadata does not carry one
Gotchas
ALTER CATALOG dwh_prod SET ISOLATION MODE = ISOLATED in a migrationThe clause does not exist — it is a parse error, not a permissions failure, and no amount of privilege will make it run. The expensive part is not the red deploy; it is the runbook that records 'prod isolated in release 14' for a release in which nothing was ever isolated.
Reviewing binding from SQL, and reading the silence as a passThe DDL has no statement for it and information_schema has no column for it, so a repo-plus-catalog review returns nothing — and nothing reads as nothing wrong. Only the bindings API or the Terraform state answers the question.
Setting the isolation mode and stopping thereThe catalog vanishes from every workspace except its owner's — including the job workspace that needs it — and the failure reads as a missing catalog, so the first instinct is to check whether the deploy dropped it.
New workspaces inherit nothingA workspace created in month eight sees no isolated catalog and every open one, so the boundary you demonstrated in month one is now describing a workspace count that has changed.
The auto-created default workspace catalog, left as it cameIt is bound to its own workspace and owned by the workspace-local 'workspace admins' group, so it sits outside both the binding list you maintain and the account-group ownership scheme — and it is the catalog an engineer lands in by default when they open a notebook and forget to switch.
Switching a shared catalog to read-only binding during working hoursExisting jobs do not fail at the start — they read happily for twenty minutes and fail at the write, mid-run, leaving a half-loaded target.
Treating binding as the access modelBinding removes workspaces from the conversation; it removes no privileges. The failure looks like finished work — dwh_prod is ISOLATED, the diagram is clean, and every analyst in the bound workspace still holds the SELECT on gold they were given in month two, on the columns nobody has masked yet. Nothing surfaces it except an access review.