Skip to content

Pluggable External User ID Providers #936

Description

@pkowalski-id5

Type of issue

Intent to implement

Description

The SDK provides only a static API for setting external user IDs via TargetingParams.setExternalUserIds(). All IDs must be known and fully resolved before the setter is called, and set before bid requests are made. This breaks down when:

  • IDs refresh during a session
  • multiple ID SDKs resolve concurrently (race conditions)
  • ID vendors want to ship plug-and-play integrations without requiring publisher glue code.

The proposed solution uses a push model: providers receive a UserIdRegistration callback and push ID updates whenever they become available. A central registry maintains EIDs keyed by a provider-chosen identifier. Bid requests read a snapshot of this registry.

Goals

  • Allow ID providers to push EID updates asynchronously at any time
  • Eliminate race conditions when multiple ID sources resolve concurrently
  • Enable ID SDK vendors to ship ready-made provider implementations (e.g. PrebidMobile.addExternalUserIdProvider(Id5PrebidProvider.create(config)))
  • Maintain full backwards compatibility with the existing TargetingParams.setExternalUserIds() API
  • Zero provider code execution in the bid request path -snapshot reads only

Proposed Design

Technical details

Introduce an ExternalUserIdProvider interface with push-based semantics. When a provider is registered, the SDK calls its initialize(UserIdRegistration) method, passing a callback object. The provider uses this callback to register or unregister individual EIDs by a provider-chosen key. The provider decides what identifies its EID (could be source, could be any string). The SDK maintains a thread-safe registry (ConcurrentHashMap<String, ExternalUserId>) where the key is chosen by the provider. At bid request time, the SDK reads a snapshot of the entire registry -no provider code executes in the request path.

                                  ┌───────────────────────────────────┐
Provider A ── setUserId()    ──▶  │                                   │
Provider B ── setUserId()    ──▶  │         UserIdRegistry            │  ◀── getAllUserIds()
Provider C ── removeUserId() ──▶  │   ConcurrentHashMap<key, EID>     │      (BasicParameterBuilder
                                  │                                   │       at bid request time)
                                  │   "id5"  → ExternalUserId{...}    │
                                  │   "uid2" → ExternalUserId{...}    │
                                  │   "lr"   → (removed)              │
                                  │                                   │
                                  └───────────────────────────────────┘

The existing static API becomes one such provider internally (StaticExternalUserIdProvider), keeping full backwards compatibility.

ExternalUserIdProvider interface

package org.prebid.mobile.api.id;

public interface ExternalUserIdProvider {

    /**
     * Called when this provider is registered. Use the registration
     * to push EID updates. Start async work here, not in the constructor.
     */
    void initialize(@NonNull UserIdRegistration registration);

    /**
     * Called when this provider is removed. Release resources.
     * Calls to the registration after this are silently ignored.
     */
    void dispose();
}

UserIdRegistration interface

package org.prebid.mobile.api.id;

/**
 * Thread-safe callback for pushing EID updates to the registry.
 * All methods can be called from any thread.
 */
public interface UserIdRegistration {

    /** Sets or overwrites an EID. Same key = overwrite. */
    void setUserId(@NonNull String key, @NonNull ExternalUserId userId);

    /** Removes an EID by key. No-op if key doesn't exist. */
    void removeUserId(@NonNull String key);
}

UserIdRegistry (internal, package-private)

Thread-safe registry storing EIDs keyed by provider-chosen keys. Not exposed to publishers.

  • Backed by ConcurrentHashMap<String, ExternalUserId> (key → EID)
  • Tracks which keys belong to which provider (ConcurrentHashMap<ExternalUserIdProvider, Set<String>>) for cleanup on dispose
  • createRegistration(provider) returns a BoundRegistration - a per-provider implementation of UserIdRegistration that writes to the shared map and tracks its own keys
  • removeProvider(provider) removes all keys owned by that provider and marks its BoundRegistration as disposed (subsequent calls silently ignored)
  • getAllUserIds() returns a snapshot of all values - called by BasicParameterBuilder at bid request time

Registration API (PrebidMobile)

public static void addExternalUserIdProvider(ExternalUserIdProvider provider);
public static void removeExternalUserIdProvider(ExternalUserIdProvider provider);
public static void clearExternalUserIdProviders();
  • addExternalUserIdProvider() creates a UserIdRegistration via the registry and calls provider.initialize(registration) synchronously
  • removeExternalUserIdProvider() calls provider.dispose() and removes all its EIDs from the registry
  • clearExternalUserIdProviders() disposes all providers and clears the registry
  • Internally, BasicParameterBuilder calls userIdRegistry.getAllUserIds() for the snapshot

Internal Providers

  • StaticExternalUserIdProvider - package-private singleton. Takes over EID storage from TargetingParams. When setExternalUserIds(list) is called, pushes each EID to the registry keyed by source. Buffers IDs set before initialize(). Registered by default.
  • SharedIdProvider - package-private singleton. On initialize(), generates the SharedID and pushes it via registration.setUserId(). Registered/unregistered via TargetingParams.setSendSharedId(true/false).

In-Scope

  • ExternalUserIdProvider interface (push-based) and UserIdRegistration callback interface
  • Internal UserIdRegistry with ConcurrentHashMap<String, ExternalUserId> storage (keyed by provider-chosen key)
  • Provider-to-keys tracking for cleanup on dispose
  • Registration API on PrebidMobile (add/remove/clear providers)
  • Internal StaticExternalUserIdProvider and SharedIdProvider implementations
  • Backwards-compatible delegation from TargetingParams existing API
  • Snapshot read in BasicParameterBuilder at bid request time
  • Unit tests for provider registration, push updates, invalidation, disposal, snapshot reads, key-based overwrite, and backwards compatibility

Out of Scope

  • Concrete third-party provider implementations

Prebid SDK Changes

  • New interface: ExternalUserIdProvider in org.prebid.mobile.api.id (push-based)
  • New interface: UserIdRegistration in org.prebid.mobile.api.id
  • New class: UserIdRegistry (package-private)
  • New class: StaticExternalUserIdProvider (package-private)
  • Modified class: SharedId refactored, becomes SharedIdProvider and implements ExternalUserIdProvider
  • Modified class: PrebidMobile -new provider registration methods + internal registry
  • Modified class: TargetingParams -delegates to StaticExternalUserIdProvider
  • Modified class: BasicParameterBuilder -reads snapshot from UserIdRegistry instead of direct TargetingParams access

Prebid Server OpenRTB Changes

None.

Prebid Server Changes

None.

Other information

Backwards Compatibility

Existing API Internal delegation
TargetingParams.setExternalUserIds(list) StaticExternalUserIdProvider.getInstance().setExternalUserIds(list) → registration.setUserId() for each
TargetingParams.getExternalUserIds() deperectated Returns registry snapshot (all EIDs from StaticExternalUserIdProvider provider) - open question
TargetingParams.setSendSharedId(true) Registers SharedIdProvider → initialize() → pushes SharedID
TargetingParams.setSendSharedId(false) removeExternalUserIdProvider(SharedIdProvider) → dispose() → EIDs removed from registry
TargetingParams.resetSharedId() SharedIdProvider.getInstance().resetIdentifier() → pushes new SharedID via registration

Design Properties

  • Push-based - provider calls registration.setUserId() / removeUserId() when data is ready. No provider code executes in the bid request path.
  • Thread safety on SDK side - UserIdRegistration is thread-safe (SDK guarantee). Provider does not need synchronization.
  • Explicit invalidation - removeUserId(key) removes an EID explicitly (e.g. consent revoked).
  • Constant latency at request time - getAllUserIds() is a map snapshot read, independent of provider count or complexity.

Registry Key Design

EIDs are keyed by a provider-chosen identifier (not automatically derived from source). The provider passes the key explicitly in setUserId(key, eid). This means:

  • Provider decides identity -the key can be the source domain, a custom string, or anything meaningful to the provider
  • Granular control -a provider can manage multiple EIDs (multiple keys) independently
  • Simple invalidation -removeUserId("my-key") removes exactly one EID
  • Cross-provider conflicts possible -two providers using the same key will overwrite each other (deferred: can be detected via BoundRegistration per-provider tracking and warned)

Open Questions

  1. TargetingParams.getExternalUserIds() return value -Should this deprecated method return (a) only static IDs from StaticExternalUserIdProvider, or (b) full registry snapshot? Low priority since BasicParameterBuilder won't use it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions