Follow a customer through the layers¶
In this tutorial, we will follow the fictional customer CUST-0001 from a readable Layer1 record
to a protected Layer3 dimension.
Along the way, notice that the business facts remain useful while direct identifiers become stable, versioned keys.
Before you begin¶
Complete Build and explore the demo. Run this lesson as the deployment identity, because it needs to call the internal pseudonymization wrapper while resolving the tutorial's fixed customer key.
Start with the source-shaped record¶
Layer1 casts the CSV values to useful types but deliberately keeps the source shape and readable Personal Data.
uv run dbt show --inline "
select
customer_id,
customer_ssn,
first_name,
email,
customer_segment,
source_operation
from {{ ref('stg_customer') }}
where customer_id = 'CUST-0001'
" --limit 10
You should see one UPSERT for CUST-0001, with the synthetic SSN 900-00-0001 and the email
customer01@example.invalid.
Note
Layer1 is a governed raw area. Keeping readable source values here is intentional; general analytical consumers cannot read it.
Find the pseudonymous mapping¶
The personal_data_key macro canonicalizes a value and calls the fixed v1 wrapper in the
priva_internal schema. Use it to locate the mapping without depending on a masked raw column:
uv run dbt show --inline "
with tutorial_customer as (
select customer_ssn
from {{ ref('stg_customer') }}
where customer_id = 'CUST-0001' and source_operation = 'UPSERT'
)
select
mapped.customer_key,
mapped.customer_id_key,
mapped.full_name_key,
mapped.email_key,
mapped.customer_segment
from {{ ref('fa_pd_customer') }} as mapped
inner join tutorial_customer as source
on mapped.customer_key =
{{ personal_data_key('source.customer_ssn', 'customer.ssn', 'ssn') }}
" --limit 10
The key columns begin with v1:. They are not interchangeable: the same input in a different
domain, such as customer.ssn or customer.email, produces a different key.
The mapping table also contains readable values, protected by Unity Catalog column masks. We do not use those masked columns for joins.
Move into protected Layer2¶
Layer2 projects the mapping into an analytical customer relation containing pseudonymous keys and non-identifying business attributes:
uv run dbt show --inline "
with tutorial_customer as (
select customer_ssn
from {{ ref('stg_customer') }}
where customer_id = 'CUST-0001' and source_operation = 'UPSERT'
)
select
protected.customer_key,
protected.customer_id_key,
protected.full_name_key,
protected.email_key,
protected.customer_segment,
protected.is_active
from {{ ref('int_customer_protected') }} as protected
inner join tutorial_customer as source
on protected.customer_key =
{{ personal_data_key('source.customer_ssn', 'customer.ssn', 'ssn') }}
" --limit 10
Compare this result with Layer1. The segment and active state remain directly useful. The SSN, customer ID, name, and email no longer appear as readable values.
Arrive in the Layer3 dimension¶
The current customer dimension preserves the protected customer grain: one row per active pseudonymous customer.
uv run dbt show --inline "
with tutorial_customer as (
select customer_ssn
from {{ ref('stg_customer') }}
where customer_id = 'CUST-0001' and source_operation = 'UPSERT'
)
select
customers.customer_key,
customers.customer_id_key,
customers.full_name_key,
customers.email_key,
customers.customer_segment,
customers.is_active
from {{ ref('dim_customer') }} as customers
inner join tutorial_customer as source
on customers.customer_key =
{{ personal_data_key('source.customer_ssn', 'customer.ssn', 'ssn') }}
" --limit 10
You should see the same protected identity and business attributes as Layer2. Layer3 changes the analytical shape of the project, not the Personal Data state of this customer.
What you have observed¶
The readable record exists in the raw boundary and the separately governed map. Protected
relations carry only stable keys for identifying attributes. Controlled case views can resolve a
narrow approved set of fields, but only for case_users and privacy_admins.
Next, trace an invalid invoice into quarantine.
Read Versioned, domain-separated keys for the
reason behind the key design, or consult the
priva_map model reference for exact columns.