IdentityCross-Domain Identity

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 identities

The 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 identity

How 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 domains

Sequence 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 parameter

The 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:00Z

If 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"
    ]
  }
}
FieldTypeDescription
enabledbooleanEnable cross-domain identity resolution
hub_urlstringURL of the Identity Hub service
domainsstring[]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.

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:

  1. Fetches the list of cross-domain domains from the source configuration
  2. Observes the DOM for matching outbound links (using MutationObserver for dynamically added links)
  3. Rewrites href attributes to route through the Identity Hub
  4. 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 = decoratedUrl

Identity Hub Infrastructure

The Identity Hub is a lightweight Go service with minimal dependencies:

PropertyValue
Serviceidentity-hub
Port8083
DependenciesRedis (nonce tracking), PostgreSQL (configuration)
EndpointsGET /sync (redirect flow), GET /health (health check)
StatelessYes — 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 balancer

In 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: 8083

Edge 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:

  1. The hub generates a new _dfid for the hub domain
  2. Sets it as a cookie on the hub domain for future syncs
  3. Redirects to the destination with a token containing the new ID
  4. 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:

  1. If the existing _dfid matches the token’s anonymous_id — no action needed
  2. If they differ — the token’s anonymous_id takes 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.