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
- 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.
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:The proposed solution uses a push model: providers receive a
UserIdRegistrationcallback 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
PrebidMobile.addExternalUserIdProvider(Id5PrebidProvider.create(config)))TargetingParams.setExternalUserIds()APIProposed Design
Technical details
Introduce an
ExternalUserIdProviderinterface with push-based semantics. When a provider is registered, the SDK calls itsinitialize(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.The existing static API becomes one such provider internally (
StaticExternalUserIdProvider), keeping full backwards compatibility.ExternalUserIdProviderinterfaceUserIdRegistrationinterfaceUserIdRegistry(internal, package-private)Thread-safe registry storing EIDs keyed by provider-chosen keys. Not exposed to publishers.
ConcurrentHashMap<String, ExternalUserId>(key → EID)ConcurrentHashMap<ExternalUserIdProvider, Set<String>>) for cleanup on disposecreateRegistration(provider)returns aBoundRegistration- a per-provider implementation ofUserIdRegistrationthat writes to the shared map and tracks its own keysremoveProvider(provider)removes all keys owned by that provider and marks itsBoundRegistrationas disposed (subsequent calls silently ignored)getAllUserIds()returns a snapshot of all values - called byBasicParameterBuilderat bid request timeRegistration API (PrebidMobile)
addExternalUserIdProvider()creates aUserIdRegistrationvia the registry and callsprovider.initialize(registration)synchronouslyremoveExternalUserIdProvider()callsprovider.dispose()and removes all its EIDs from the registryclearExternalUserIdProviders()disposes all providers and clears the registryBasicParameterBuildercallsuserIdRegistry.getAllUserIds()for the snapshotInternal Providers
TargetingParams. WhensetExternalUserIds(list)is called, pushes each EID to the registry keyed bysource. Buffers IDs set beforeinitialize(). Registered by default.initialize(), generates the SharedID and pushes it viaregistration.setUserId(). Registered/unregistered viaTargetingParams.setSendSharedId(true/false).In-Scope
ExternalUserIdProviderinterface (push-based) andUserIdRegistrationcallback interfaceUserIdRegistrywithConcurrentHashMap<String, ExternalUserId>storage (keyed by provider-chosen key)PrebidMobile(add/remove/clear providers)StaticExternalUserIdProviderandSharedIdProviderimplementationsTargetingParamsexisting APIBasicParameterBuilderat bid request timeOut of Scope
Prebid SDK Changes
ExternalUserIdProviderinorg.prebid.mobile.api.id(push-based)UserIdRegistrationinorg.prebid.mobile.api.idUserIdRegistry(package-private)StaticExternalUserIdProvider(package-private)SharedIdrefactored, becomesSharedIdProviderand implementsExternalUserIdProviderPrebidMobile-new provider registration methods + internal registryTargetingParams-delegates toStaticExternalUserIdProviderBasicParameterBuilder-reads snapshot fromUserIdRegistryinstead of directTargetingParamsaccessPrebid Server OpenRTB Changes
None.
Prebid Server Changes
None.
Other information
Backwards Compatibility
TargetingParams.setExternalUserIds(list)StaticExternalUserIdProvider.getInstance().setExternalUserIds(list)→registration.setUserId()for eachTargetingParams.getExternalUserIds()deperectatedStaticExternalUserIdProviderprovider) - open questionTargetingParams.setSendSharedId(true)SharedIdProvider→initialize()→ pushes SharedIDTargetingParams.setSendSharedId(false)removeExternalUserIdProvider(SharedIdProvider)→dispose()→ EIDs removed from registryTargetingParams.resetSharedId()SharedIdProvider.getInstance().resetIdentifier()→ pushes new SharedID via registrationDesign Properties
registration.setUserId()/removeUserId()when data is ready. No provider code executes in the bid request path.UserIdRegistrationis thread-safe (SDK guarantee). Provider does not need synchronization.removeUserId(key)removes an EID explicitly (e.g. consent revoked).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 insetUserId(key, eid). This means:removeUserId("my-key")removes exactly one EIDBoundRegistrationper-provider tracking and warned)Open Questions
StaticExternalUserIdProvider, or (b) full registry snapshot? Low priority sinceBasicParameterBuilderwon't use it.