Skip to main content

Tutorial: Tenant Carve-out

This walkthrough covers the tenant carve-out variant of a platform migration — extracting a single tenant's data from a shared Snowflake platform into a dedicated BigQuery project, rather than migrating the whole platform. It assumes you know the standard migration flow from the Platform Migration tutorial; here we focus only on what the carve-out adds.

The carve-out is a variant, not a separate release type. Audits, inventory, strategy, dbt migration, equivalency, and cutover all run as normal. Four extra commands and some tenant scoping make the difference.

Statement of Work

**Rittman Analytics × Meridian Retail**
**Engagement**: Regional-tenant carve-out, Snowflake → BigQuery
**Type**: Fixed price, staged

Meridian Retail runs a shared multi-tenant Snowflake platform. One regional
business unit (`tenant_id = 1042`) is being divested and must move to its own
BigQuery project, isolated from every other tenant's data. The carve-out
covers ~3 years of history. Reporting is on Metabase.

Stage 1 deliverables (gated, part-paid): region tagging, and the GDPR /
data-residency assessment including a legal review of the historical window.

Setting up the carve-out

At /wire:new, choose Platform Migration, then answer the scope question:

Migration scope?
1. Full migration (default)
2. Tenant carve-out
> 2

Tenant predicate (the WHERE clause / tenant key that scopes the tenant):
> tenant_id = 1042

Reporting / BI tool? > Metabase

This writes to status.md:

migration:
scope: tenant_carveout
tenant_predicate: "tenant_id = 1042"
reporting_tool: metabase

Every migration command now reads migration.scope. With full_migration (or the field absent) nothing below changes. With tenant_carveout the predicate threads through equivalency and the security chain, and the four carve-out commands become part of the flow.

Step 1 — Region tagging (after the audits)

Once the five audits are approved, classify every in-scope item by whether it belongs to the regional tenant being carved out.

/wire:region-tagging-generate 01-tenant-carveout --region north

This reads the audit CSVs and writes migration/region_tags.csv, sorting each item into one of three buckets:

BucketSignalExample
confident-regionname suffix / destination / WHERE-clause matchstg_orders_north, a sync to the regional Salesforce
shared-row-levelcarries the tenant key but serves all tenantsdim_customers keyed by tenant_id
global-deferredno market tag at alldim_date, currency reference tables

The command produces candidates, not decisions — it never emits an include/exclude flag and never removes anything. Validation confirms all three buckets are populated and every in-scope item is classified exactly once:

/wire:region-tagging-validate 01-tenant-carveout
✓ Check 1 — all three buckets populated
✓ Check 2 — 412 in-scope items, each classified exactly once

-review is the human adjudication gate. The reviewer works the pile — every shared-row-level item gets a lineage trace and a row sample, then a ruling: carve in (with the row-level predicate), split, or defer.

/wire:region-tagging-review 01-tenant-carveout

Step 2 — Data-residency assessment (alongside strategy)

This is the Stage 1 contractual deliverable. RA prepares it as data processor — it structures the GDPR and residency questions and the legal review of the ~3-year window, and flags every legal determination for the client's DPO.

/wire:data-residency-assessment-generate 01-tenant-carveout

data_residency_assessment.md leads with a processor-not-counsel banner, then: GDPR scope and lawful basis, residency constraints for the target region, the historical-window review, the processor safeguards RA implements, and a consolidated list of [CLIENT DPO/LEGAL] items. RA does not assert the lawful basis — -validate fails if it does, or if the required-client-input section is empty:

/wire:data-residency-assessment-validate 01-tenant-carveout
✓ all seven sections present and non-empty
✓ processor-not-counsel framing present
✓ lawful basis and retention ruling flagged [CLIENT DPO/LEGAL], not asserted

-review is the client DPO/legal sign-off gate. RA cannot self-approve — the lawful basis and the retention ruling on the historical window are the controller's to make.

Step 3 — Metabase reporting layer

Because reporting_tool: metabase, audit and migrate the reporting layer alongside the warehouse work.

/wire:metabase-audit-generate 01-tenant-carveout
/wire:metabase-migration-generate 01-tenant-carveout

The audit catalogues collections, dashboards, cards (with their SQL), database connections, and permission groups. The migration translates native-SQL card dialect to BigQuery, remaps permission groups, validates on a throwaway decoy collection against a frozen baseline, then repoints the Metabase database connection from Snowflake to BigQuery in two stages with per-stage rollback. It will not run without a client-supplied query inventory.

Step 4 — Bulk copy, in place of re-ingestion

A carve-out copies the tenant's existing history rather than re-ingesting it. bulk-copy-migration replaces ingestion-migration in the flow.

/wire:bulk-copy-migration-generate 01-tenant-carveout

The runbook copies each in-scope table via the BigQuery Data Transfer Service or a GCS-staged path, every extract filtered by tenant_id = 1042. It runs under a service account scoped to the tenant's target project only, with a tenant guard that refuses any extract missing the predicate or pointed at a production project. The copy is two-stage with an equivalency gate between:

Stage 1 — pilot partition (one month) → equivalency check 1 (row count) + check 6 (checksum)
gate: both pass, tenant-scoped, on source and target
Stage 2 — remainder

-review is a safety gate: written approval authorises the first copy execution, Stage 1 only.

Step 5 — Logical-access UAT (before cutover)

Before cutover, prove the isolation actually holds.

/wire:logical-access-uat-generate 01-tenant-carveout --region north

The plan derives its tests from the IAM boundaries in target_setup_scripts/04_security.sql — tenant-scoped grants, the RLS predicate, the scoped service account, PII masking. Every boundary gets a positive test and at least one negative test:

| Test | Boundary | Role | Type | Expected |
| T-04 | tenant grant | tenant_north_analyst | negative | query another tenant's project → permission denied |
| T-07 | RLS predicate | tenant_north_analyst | negative | shared table → only this tenant's rows, zero other-tenant rows |

-validate is strict: it fails unless every IAM boundary in 04_security.sql has at least one negative test.

/wire:logical-access-uat-validate 01-tenant-carveout
✓ every IAM boundary has ≥1 negative test

-review executes the matrix, captures evidence, and takes the three-attestation sign-off. A negative test that returns another tenant's data fails the gate regardless of how many positives pass, and routes back to target-setup to fix the boundary.

How equivalency changes

You do not run a different equivalency command. The existing checks gain the tenant predicate on both source and target, so the carve-out validates only the carved-out tenant's rows. Row count, value sampling, freshness, checksum, and aggregate control totals all scope to tenant_id = 1042. Schema stays structural and unchanged — there is no row data for a predicate to act on. No new check types were added.

Where the carve-out lands in the sequence

audits → region-tagging → inventory
→ strategy + data-residency-assessment
→ target-setup (tenant-scoped GRANTs + RLS in 04_security.sql)
→ bulk-copy-migration (in place of ingestion-migration)
→ dbt-migration → metabase-migration
→ equivalency (tenant-scoped)
→ logical-access-uat → cutover → migration-report

Everything not listed here runs exactly as in the standard Platform Migration tutorial.