
Here you can read our guide for connecting your Microsoft Fabric Warehouse with Churney. In this guide, we go over the steps for giving Churney access to your data with hashed views and minimal permissions, using Entra ID identities only. No passwords, client secrets, SAS tokens or storage keys are exchanged; only short-lived federated tokens are used.
Churney exports data through a pipeline in your tenant rather than querying it from outside. On a schedule agreed with you, Churney's sync:
Everything Churney does in your tenant is done as one Entra ID application that you register. The pipeline itself runs as your export workspace's own identity. Files in your container expire automatically after 1 day.
The setup is two hand-overs:
Before you start, make sure that:
az login). The commands below use it; everything can also be done in the Azure portal and the Fabric portal.Throughout the guide, replace the values in angle brackets. Fabric REST API calls use a token from your own login:
TOKEN=$(az account get-access-token --resource https://api.fabric.microsoft.com --query accessToken -o tsv)The short answer is as much as possible. The long answer is that Churney requires data about:
Additionally, we need to know the Azure region of your Fabric capacity.
If you would like Churney's help with the hashed views, connect to your Warehouse and run the following query, where <your_schema_name> is the schema of the tables you wish to share with Churney:
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = '<your_schema_name>';Download the output as CSV and share it with Churney. We will come back to you with a script that creates the hashed views. Otherwise, continue with the example below.
For context, Facebook has a guide on how to hash contact information for their conversion API: https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/customer-information-parameters. Google has a similar guide for enhanced conversions: https://developers.google.com/google-ads/api/docs/conversions/enhance-conversions. We want to hash columns that would allow one to determine the identity of a user: first name, last name, birthday, street address, phone number, email address, IP address.
The specific contact information columns normalization patterns are as follows:
email (Meta) — lowercase, leading and trailing spaces removed → john.doe+promo@gmail.comgoogle_email (Google) — same, and for gmail.com / googlemail.com also remove every period and any + suffix before the @ → johndoe@gmail.comphone (Meta) — digits only, country code kept, leading zeros removed, no + → 442071838750google_phone (Google) — the same digits with a + prefix → +442071838750Also, please be aware of the following requirements:
Phone numbers must include a country code. Meta and Google both match on the country code, and Google rejects a number without one instead of repairing it. Always store the country code, even when every customer is in one country. Churney cannot add it later, because a SHA-256 hash cannot be reversed.
Drop the national trunk prefix. A UK number written 020 7183 8750 is +44 20 7183 8750, not +44 020 7183 8750. The SQL below removes leading zeros and the 00 international prefix, but it cannot find a trunk zero in the middle of a number.
Do not hash an empty value. SHA256('') is a valid 64-character hash that every customer without a phone number would share. Churney rejects it. The SQL below returns NULL instead. Do the same for placeholder strings such as none or null.
Hashes must be lowercase hex, 64 characters, with no 0x prefix.
Two columns per identifier. Meta and Google need different normalization, so share both email and google_email, and both phone and google_phone.
Create a schema churney_views in your Warehouse (or in the Lakehouse's SQL analytics endpoint) and, for each table you share, a view that hashes the personally identifiable columns with SHA-256. Fabric uses T-SQL:
CREATE SCHEMA churney_views;
GO
CREATE VIEW churney_views.users AS
WITH cleaned AS (
SELECT
*,
NULLIF(LOWER(TRIM(CONVERT(VARCHAR(320), email))), '') AS email_normalized,
NULLIF(REPLACE(TRANSLATE(CONVERT(VARCHAR(64), phone), ' ()-.+/', ' '), ' ', ''), '')
AS phone_all_digits
FROM raw_data.users
),
parts AS (
SELECT
*,
LEFT(email_normalized, NULLIF(CHARINDEX('@', email_normalized), 0) - 1) AS email_local_part,
SUBSTRING(email_normalized, NULLIF(CHARINDEX('@', email_normalized), 0) + 1, 320) AS email_domain,
NULLIF(SUBSTRING(phone_all_digits,
PATINDEX('%[^0]%', phone_all_digits + 'x'),
LEN(phone_all_digits)), '') AS phone_digits
FROM cleaned
)
SELECT
user_id,
LOWER(CONVERT(VARCHAR(64), HASHBYTES('SHA2_256', email_normalized), 2)) AS email,
LOWER(CONVERT(VARCHAR(64), HASHBYTES('SHA2_256',
CASE
WHEN email_domain IN ('gmail.com', 'googlemail.com')
OR email_domain LIKE 'gmail.co.%'
OR email_domain LIKE 'googlemail.co.%'
THEN REPLACE(
CASE WHEN CHARINDEX('+', email_local_part) > 0
THEN LEFT(email_local_part, CHARINDEX('+', email_local_part) - 1)
ELSE email_local_part END,
'.', '') + '@' + email_domain
ELSE email_normalized
END), 2)) AS google_email,
LOWER(CONVERT(VARCHAR(64), HASHBYTES('SHA2_256', phone_digits), 2)) AS phone,
LOWER(CONVERT(VARCHAR(64), HASHBYTES('SHA2_256', '+' + phone_digits), 2)) AS google_phone,
LOWER(CONVERT(VARCHAR(64), HASHBYTES('SHA2_256',
NULLIF(LOWER(TRIM(CONVERT(VARCHAR(200), full_name))), '')), 2)) AS full_name,
LOWER(CONVERT(VARCHAR(64), HASHBYTES('SHA2_256', CONVERT(VARCHAR(8), birthday, 112)), 2)) AS birthday,
country_code, signup_date, created_at
FROM parts;LOWER(CONVERT(..., 2)) renders the hash as lowercase hex without the 0x prefix, which matches the hashes Churney receives from other sources. Keep the LOWER(). Churney accepts lowercase hex only.
Each value is cast to VARCHAR before hashing. HASHBYTES hashes the raw bytes of its argument, so an NVARCHAR input hashes UTF-16 bytes and produces a different hash for the same text than every other warehouse does.
TRANSLATE removes spaces, parentheses, hyphens, periods, plus signs and slashes from the phone number. T-SQL has no regular expressions, so it cannot remove every non-digit character. If your phone column can contain letters or other symbols, clean it before the view.
Only the hashed views are shared with Churney. The raw tables stay private.
Two things to keep in mind for the views:
DATETIME2) that Churney can use for incremental loads, for example event_time or created_at.IN ('gmail.com', 'googlemail.com') and LIKE 'gmail.co.%', are case-sensitive for the same reason. They work only because LOWER() is applied to the whole address first, so do not remove it.
Churney's pipeline writes Parquet files to an Azure Data Lake Storage Gen2 container. Create it in the same region as your Fabric capacity, with hierarchical namespace enabled, and let files expire after 1 day.
az group create --name churney-export --location <region>az storage account create \
--name <storageaccount> --resource-group churney-export --location <region> \
--sku Standard_LRS --kind StorageV2 --enable-hierarchical-namespace true \
--allow-shared-key-access falseaz storage container create --account-name <storageaccount> --name churney-unload --auth-mode loginaz storage account management-policy create --account-name <storageaccount> --resource-group churney-export --policy '{
"rules": [{"enabled": true, "name": "expire-churney-unload", "type": "Lifecycle",
"definition": {"actions": {"baseBlob": {"delete": {"daysAfterModificationGreaterThan": 1}}},
"filters": {"blobTypes": ["blockBlob"], "prefixMatch": ["churney-unload/"]}}}]}'--allow-shared-key-access false disables storage account keys entirely; every access in this guide uses Entra ID.
The pipeline Churney runs lives in its own workspace, so that Churney never needs any role in the workspace that holds your data. Create the workspace on your capacity and give it a workspace identity.
CAPACITY_ID=$(curl -s -H "Authorization: Bearer $TOKEN" https://api.fabric.microsoft.com/v1/capacities \
| jq -r '.value[] | select(.displayName=="<your capacity name>") | .id')EXPORT_WS=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
https://api.fabric.microsoft.com/v1/workspaces \
-d "{\"displayName\":\"churney-export\",\"capacityId\":\"$CAPACITY_ID\"}" | jq -r .id)curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Length: 0" \
"https://api.fabric.microsoft.com/v1/workspaces/$EXPORT_WS/provisionIdentity"The last call is asynchronous. After about ten seconds, check the result in the Fabric portal under the export workspace → Workspace settings → Workspace identity. It shows the identity's service principal ID; keep it as <identity-sp-id>. If the state shows Failed the first time an identity is created in a tenant, delete it, wait five minutes and create it again.
Grant the workspace identity write access to the container:
az role assignment create --assignee-object-id <identity-sp-id> --assignee-principal-type ServicePrincipal \ --role "Storage Blob Data Contributor" \ --scope "/subscriptions/<subscription>/resourceGroups/churney-export/providers/Microsoft.Storage/storageAccounts/<storageaccount>/blobServices/default/containers/churney-unload"The export workspace's identity needs to read churney_views, and nothing else. Access is opt-in: share the Warehouse with the identity without data access, then grant the one schema in SQL.
churney-export (the workspace identity has the same name as the export workspace). Leave every checkbox unticked, in particular "Read all data using SQL", and select Grant. This lets the identity connect to the Warehouse but see no tables.
GRANT SELECT ON SCHEMA::churney_views TO [churney-export];Do not give the identity a workspace role; Viewer would grant read access to every table in the workspace. Sharing an item is a portal action; Fabric's REST API for item role assignments is not yet enabled for warehouses.
The pipeline Churney runs uses two connections, both authenticated with the workspace identity: one to your Warehouse's SQL endpoint, one to the container. The warehouse connection uses Fabric's generic SQL connection type over the Warehouse's TDS endpoint, not the Fabric-specific Warehouse connector: only the generic type supports workspace identity as the credential, the Warehouse connector supports signed-in users only. Find the SQL endpoint under your Warehouse (or SQL analytics endpoint) → Settings → SQL connection string; it looks like <guid>.datawarehouse.fabric.microsoft.com.
SQL_CONN=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
https://api.fabric.microsoft.com/v1/connections -d '{
"connectivityType":"ShareableCloud","displayName":"churney-warehouse",
"connectionDetails":{"type":"SQL","creationMethod":"SQL","parameters":[
{"dataType":"Text","name":"server","value":"<sql-endpoint>"},
{"dataType":"Text","name":"database","value":"<warehouse-name>"}]},
"privacyLevel":"Organizational",
"credentialDetails":{"singleSignOnType":"None","connectionEncryption":"Encrypted","skipTestConnection":false,
"credentials":{"credentialType":"WorkspaceIdentity"}}}' | jq -r .id)ADLS_CONN=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
https://api.fabric.microsoft.com/v1/connections -d '{
"connectivityType":"ShareableCloud","displayName":"churney-unload",
"connectionDetails":{"type":"AzureDataLakeStorage","creationMethod":"AzureDataLakeStorage","parameters":[
{"dataType":"Text","name":"server","value":"https://<storageaccount>.dfs.core.windows.net"},
{"dataType":"Text","name":"path","value":"churney-unload"}]},
"privacyLevel":"Organizational",
"credentialDetails":{"singleSignOnType":"None","connectionEncryption":"Encrypted","skipTestConnection":false,
"credentials":{"credentialType":"WorkspaceIdentity"}}}' | jq -r .id)Print the two connection ids; you share them with Churney:
echo "$SQL_CONN $ADLS_CONN"Churney creates the pipeline itself, named churney_unload, in the export workspace using these two connections, and keeps its definition up to date on every sync. It is a single Copy activity with two parameters, query and sink_path; it has no schedule and no query of its own. You can open it in the Fabric portal to inspect it. Manual edits to it are overwritten by the next sync.
Churney authenticates as an Entra ID application in your tenant. Instead of a client secret, the application trusts two Google Cloud identities through federated credentials. Use the two ids Churney sent you: <churney-sync-id> for the sync itself and <churney-transfer-id> for the file transfer.
APP_ID=$(az ad app create --display-name churney-sync --sign-in-audience AzureADMyOrg --query appId -o tsv)
SP_ID=$(az ad sp create --id $APP_ID --query id -o tsv)
az ad app federated-credential create --id $APP_ID --parameters '{
"name": "churney-sync",
"issuer": "https://accounts.google.com",
"subject": "<churney-sync-id>",
"audiences": ["api://AzureADTokenExchange"],
"description": "Churney sync: runs the export pipeline and reads table metadata"}'
az ad app federated-credential create --id $APP_ID --parameters '{
"name": "churney-transfer",
"issuer": "https://accounts.google.com",
"subject": "<churney-transfer-id>",
"audiences": ["api://AzureADTokenExchange"],
"description": "Google Storage Transfer Service: reads the churney-unload container"}'Grant the application the permissions it needs:
# Use the two connections in the pipeline definition
for CONN in $SQL_CONN $ADLS_CONN; do
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.fabric.microsoft.com/v1/connections/$CONN/roleAssignments" \
-d "{\"principal\":{\"id\":\"$SP_ID\",\"type\":\"ServicePrincipal\"},\"role\":\"User\"}"
done
# Create, update and run the pipeline in the export workspace
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
"https://api.fabric.microsoft.com/v1/workspaces/$EXPORT_WS/roleAssignments" \
-d "{\"principal\":{\"id\":\"$SP_ID\",\"type\":\"ServicePrincipal\"},\"role\":\"Contributor\"}"
# Read the exported files
az role assignment create --assignee-object-id $SP_ID --assignee-principal-type ServicePrincipal \
--role "Storage Blob Data Reader" \
--scope "/subscriptions/<subscription>/resourceGroups/churney-export/providers/Microsoft.Storage/storageAccounts/<storageaccount>/blobServices/default/containers/churney-unload"
# Use the two connections in the pipeline definition
for CONN in $SQL_CONN $ADLS_CONN; do curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ "https://api.fabric.microsoft.com/v1/connections/$CONN/roleAssignments" \ -d "{\"principal\":{\"id\":\"$SP_ID\",\"type\":\"ServicePrincipal\"},\"role\":\"User\"}"doneThe application also needs to read column metadata (names and types) from the hashed views, but not the rows. Share the Warehouse with churney-sync without data access (Warehouse → Share, no checkboxes ticked), then grant metadata visibility only:
GRANT VIEW DEFINITION ON SCHEMA::churney_views TO [churney-sync];With this, the Churney application can list the views and their columns but cannot SELECT from them; rows are only ever read by the pipeline, as your workspace identity.
Finally, add the application to the security group that the tenant setting "Service principals can use Fabric APIs" is scoped to.
When the steps above are done, send Churney:
churney-sync | echo $APP_ID, or Azure portal → Microsoft Entra ID → App registrations → churney-sync → Application (client) IDChurney fills these into your sync configuration and runs the first sync. That run creates the churney_unload pipeline in your export workspace; nothing else needs to happen on your side.
Once you have shared the details above, Churney:
churney_views and checks the result with you.Churney holds no credentials for your tenant at any point: no client secret, no storage key, no SAS token. Access can be revoked by deleting the churney-sync app registration or the export workspace.
Every connection uses Entra ID and short-lived tokens:
churney-sync application through the federated credential you created. This is Entra ID's workload identity federation; the token is valid for about an hour and is never stored. The same token is used to start the pipeline and to read column metadata from the hashed views. The application has no SELECT permission, so it cannot read rows.churney-sync application through the second federated credential, with read-only access to the container.The sync is incremental: rather than re-copying entire tables each time, we only pull rows that are new or have changed since the last successful sync, using the timestamp column you designate for each view. We also apply a configurable safety-margin window so that late-arriving or recently-updated records are not missed. Each table is exported by its own pipeline run and processed independently, so if one table fails, the others still complete and the specific failures are reported clearly.
Each run writes to a fresh folder in the container, named after the run, and the lifecycle rule deletes it after 1 day. Pipeline runs consume capacity units on the capacity the export workspace is assigned to.
Your data warehouse has incredible value. Our causal AI helps unlock it.