Cross-Domain Identity
When a business operates multiple domains (e.g. example.com and example-shop.com), each domain has its own isolated cookie space. A visitor on example.com has one _dfid, and the same visitor on example-shop.com gets a completely different _dfid. Signal’s Identity Hub service bridges this gap, allowing a single visitor identity to persist across domains using encrypted, single-use tokens.
The Problem
Browser security prevents cookies from being shared across different domains. A cookie set on example.com is never sent to example-shop.com, even if both sites belong to the same business. This is a fundamental browser security property — not a bug.
Without cross-domain identity resolution, a visitor who browses products on example.com and then purchases on example-shop.com appears as two separate anonymous users. Attribution is broken, funnel analysis is incomplete, and vendor APIs receive disconnected events.
Without cross-domain identity:
example.com → _dfid = "aaa-111" → Redis: identity:aaa-111
example-shop.com → _dfid = "bbb-222" → Redis: identity:bbb-222
Same person, two unlinked identitiesThe Solution: Identity Hub
The Identity Hub is a standalone Go service (port 8083) that acts as a shared identity bridge between domains. It runs on a shared subdomain (e.g. id.company.com) and uses encrypted redirect tokens to transfer the anonymous ID from one domain to another.
With cross-domain identity:
example.com → _dfid = "aaa-111"
user clicks link to example-shop.com
→ link redirects through Identity Hub
→ Identity Hub reads "aaa-111", encrypts it into a token
→ redirects to example-shop.com with token
→ example-shop.com decrypts token, sets _dfid = "aaa-111"
Same person, same identityHow It Works
Step-by-Step Flow
1. User is on site-a.com (_dfid = "aaa-111")
2. User clicks a link to site-b.com
The link has been decorated by Datafly.js:
https://id.company.com/sync?from=site-a.com&to=https://site-b.com/products/headphones
3. Browser navigates to Identity Hub (id.company.com)
→ Cookie: _dfid=aaa-111 (first-party on id.company.com)
→ Identity Hub reads the _dfid value
4. Identity Hub generates an encrypted token:
→ Plaintext: { "anonymous_id": "aaa-111", "exp": 1708876603, "nonce": "xyz789" }
→ Encrypted with AES-256-GCM
→ Base64url-encoded: eyJhbm9ueW1vdXNfaW...
5. Identity Hub redirects to site-b.com:
HTTP 302 Location: https://site-b.com/products/headphones?_dft=eyJhbm9ueW1vdXNfaW...
6. Browser follows redirect to site-b.com
→ site-b.com's Ingestion Gateway receives the request
→ Finds _dft parameter in the URL
→ Decrypts the token with the shared AES-256-GCM key
→ Validates expiry (60-second TTL) and nonce (single-use)
→ Sets _dfid = "aaa-111" via Set-Cookie header
7. Visitor now has _dfid = "aaa-111" on both domainsSequence Diagram
Browser Identity Hub site-b.com Gateway
│ (id.company.com) │
│ │ │
│ GET /sync?from=... │ │
│ Cookie: _dfid=aaa-111 │ │
│───────────────────────>│ │
│ │ │
│ 302 Redirect │ │
│ → site-b.com?_dft=... │ │
│<───────────────────────│ │
│ │ │
│ GET /products/headphones?_dft=... │
│──────────────────────────────────────────────>│
│ │ │
│ │ Decrypt token │
│ │ Validate nonce │
│ │ Set _dfid cookie │
│ │ │
│ 200 OK │ │
│ Set-Cookie: _dfid=aaa-111 │
│<──────────────────────────────────────────────│Token Security
The cross-domain token (_dft) is protected by multiple layers:
AES-256-GCM Encryption
The token payload is encrypted using AES-256-GCM, an authenticated encryption algorithm that provides both confidentiality and integrity. The encryption key is shared between the Identity Hub and all Ingestion Gateways in the deployment.
Token structure (before encryption):
{
"anonymous_id": "aaa-111",
"exp": 1708876603,
"nonce": "xyz789abc123",
"from": "site-a.com"
}
Encrypted → Base64url-encoded → appended as _dft parameterThe encryption key is configured via the DATAFLY_IDENTITY_KEY environment variable and must be a 32-byte (256-bit) value.
60-Second TTL
Tokens expire 60 seconds after generation. This limits the window during which a token can be used, even if intercepted:
Token generated at: 2026-02-25T14:30:00Z
Token expires at: 2026-02-25T14:31:00ZIf the Ingestion Gateway receives a token after its expiry time, it is rejected and the visitor receives a new _dfid instead.
Single-Use Nonce
Each token contains a cryptographically random nonce. When the Ingestion Gateway decrypts and validates a token, it records the nonce in Redis:
SETEX dft_nonce:xyz789abc123 120 "used"If the same nonce is presented again (replay attack), the gateway rejects it. The nonce record in Redis has a 120-second TTL (2x the token TTL) to ensure coverage.
Cross-domain tokens are single-use. If a user bookmarks a URL containing a _dft parameter and revisits it later, the token will be expired and the nonce will be invalid. The user will simply receive a new _dfid on the destination domain. This is the expected behaviour — tokens are not meant to be persistent.
Origin Validation
The token includes the from domain, and the Identity Hub validates that the redirect target (to parameter) is a configured domain in the cross-domain group. This prevents an attacker from using the Identity Hub to redirect users to arbitrary domains.
Configuration
Cross-domain identity is configured per organisation in the Management UI or via the Management API:
{
"cross_domain": {
"enabled": true,
"hub_url": "https://id.company.com",
"domains": [
"example.com",
"example-shop.com",
"example-blog.com"
]
}
}| Field | Type | Description |
|---|---|---|
enabled | boolean | Enable cross-domain identity resolution |
hub_url | string | URL of the Identity Hub service |
domains | string[] | List of domains in the cross-domain group |
The Identity Hub must be deployed on a domain that is accessible from all participating domains. A common pattern is to use a shared corporate subdomain (e.g. id.company.com) or a dedicated identity domain.
The Identity Hub domain should have its own SSL certificate and DNS configuration. It does not need to share a root domain with the participating sites — it just needs to be accessible via HTTPS.
Automatic Link Decoration
Datafly.js can automatically decorate outbound links to configured cross-domain destinations. When enabled, any <a> tag pointing to a domain in the cross-domain group is rewritten to route through the Identity Hub:
<!-- Original link -->
<a href="https://example-shop.com/products/headphones">Shop now</a>
<!-- After Datafly.js decoration -->
<a href="https://id.company.com/sync?from=example.com&to=https%3A%2F%2Fexample-shop.com%2Fproducts%2Fheadphones">Shop now</a>Link decoration is enabled in the Datafly.js configuration:
<script
src="https://data.example.com/datafly.js"
data-pipeline-key="dk_live_abc123..."
data-cross-domain="true"
></script>When data-cross-domain is set to true, Datafly.js:
- Fetches the list of cross-domain domains from the source configuration
- Observes the DOM for matching outbound links (using
MutationObserverfor dynamically added links) - Rewrites
hrefattributes to route through the Identity Hub - Preserves the original destination path and query parameters
Programmatic Decoration
For JavaScript-driven navigation (e.g. single-page app route changes, window.location assignments), you can use the Datafly.js API to generate a decorated URL:
const decoratedUrl = datafly.crossDomainUrl('https://example-shop.com/checkout')
// Returns: "https://id.company.com/sync?from=example.com&to=https%3A%2F%2Fexample-shop.com%2Fcheckout"
window.location.href = decoratedUrlIdentity Hub Infrastructure
The Identity Hub is a lightweight Go service with minimal dependencies:
| Property | Value |
|---|---|
| Service | identity-hub |
| Port | 8083 |
| Dependencies | Redis (nonce tracking), PostgreSQL (configuration) |
| Endpoints | GET /sync (redirect flow), GET /health (health check) |
| Stateless | Yes — all state is in Redis and PostgreSQL |
The Identity Hub can be horizontally scaled behind a load balancer. It maintains no in-memory state; all nonce tracking and configuration is stored in Redis and PostgreSQL respectively.
DNS Configuration
The Identity Hub needs a DNS record pointing to the service:
id.company.com → A record or CNAME to Identity Hub load balancerIn Kubernetes deployments, this is typically an Ingress resource with TLS termination:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: identity-hub
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- id.company.com
secretName: identity-hub-tls
rules:
- host: id.company.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: identity-hub
port:
number: 8083Edge Cases
Visitor Has No _dfid on the Hub Domain
If a visitor reaches the Identity Hub without a _dfid cookie (first interaction with the hub), the hub cannot transfer an identity. In this case:
- The hub generates a new
_dfidfor the hub domain - Sets it as a cookie on the hub domain for future syncs
- Redirects to the destination with a token containing the new ID
- The destination sets this new ID as its
_dfid
On subsequent cross-domain navigations, the hub’s _dfid cookie is present and the full sync flow works as expected.
Destination Already Has a _dfid
If the destination domain already has a _dfid cookie (the visitor has been there before), the Ingestion Gateway must decide which identity to keep. The rules are:
- If the existing
_dfidmatches the token’sanonymous_id— no action needed - If they differ — the token’s
anonymous_idtakes precedence, and the gateway merges the identity records in Redis
Identity merging combines all vendor IDs and click IDs from both anonymous IDs into a single record, preserving the most recent value for any conflicting fields.
JavaScript-Disabled Browsers
Cross-domain identity works without JavaScript because the mechanism is based entirely on HTTP redirects and cookies. The only requirement is that the browser follows redirects and accepts cookies — both of which are standard browser behaviour.
However, automatic link decoration requires JavaScript (Datafly.js). For JavaScript-disabled visitors, links must be pre-decorated in the server-rendered HTML.