Skip to main content
InternalMarkdown

Identity

PlayerPal keeps one private player and issues unlinkable aliases per audience. Partners must not be able to correlate the same person by comparing PlayerPal identifiers.

Long-form: spec §3-§4, §15, §30, §36.6. Encoding background was first written in historical/id-architecture.md; the rules below are current. Buildable DDL: TypeORM migrations under services/api/src/db/migrations/ (ADR 0023). Analytics subject and partner views: ADR 0026.

Identifiers​

IdentifierRepresentsWho may see it
Internal UUIDv7Database primary keyPlayerPal only. Never exposed
pp_plr_...Player-visible PlayerPal identityPlayer and PlayerPal only
pp_plr_opr_...Player alias for one operatorThat operator and PlayerPal
pp_plr_prv_...Player alias for one provider-operator audienceThat integration and PlayerPal
pp_opr_... / pp_prv_... / pp_aff_...Party (organisation)Authorized parties
pp_int_...Configured integration / audienceAuthorized parties
pp_ses_...SessionRelevant client and PlayerPal
pp_anl_...Analytics subject for retained ActivityPlayerPal only. Never a player or partner API

Never use pp_opr_... or pp_prv_... as a player alias. They identify organisations. See ADR 0013.

A provider-global cohort (Account counts, numbers only) is allowed; a provider-global alias is not (ADR 0014, ADR 0026).

Example:

private player: pp_plr_7D4P6...
analytics subject: pp_anl_K4N2W... (PlayerPal only)
Operator A: pp_plr_opr_A91K7...
Operator B: pp_plr_opr_B38M2...
Provider A × Operator A: pp_plr_prv_X72Q4...
Provider A × Operator B: pp_plr_prv_M93R8...

Internal IDs: UUIDv7​

Database keys use UUIDv7 (128-bit, time-ordered, native database support). The formatted external IDs are separate from those keys.

internal player UUIDv7: 0198f2a4-8d7e-7c31-a8b2-4d6f91c7e201
player-visible PlayerPal ID: pp_plr_7D4P6...

Do not expose Base32(internal player UUIDv7). That would be reversible and globally linkable.

ULID is viable as an encoding of 128 bits, but PlayerPal keeps UUIDv7 internally and a random pairwise value externally.

External encoding: Crockford Base32​

Pairwise aliases are cryptographically random 128-bit values, encoded with Crockford Base32, then prefixed.

Crockford alphabet:

0123456789ABCDEFGHJKMNPQRSTVWXYZ

It omits I, L, O, U. URL-safe, case-insensitive, 26 characters for 128 bits (ceil(128 / 5) = 26).

The alias must not contain the internal UUID, partner IDs, PII, external account IDs, or business timestamps.

Random pairwise, not derived​

Rejected: pp_plr_<Base32(internal UUID)> (reversible, same value for every partner) and HMAC(secret, player + audience) (key rotation and coupling; a mapping table is needed anyway).

Accepted (ADR 0002): a fresh random 128-bit value per (player, audience), stored, encoded, prefixed. Generated when the one-time link/consent completes (ADR 0009), for audiences listed on that intent (ADR 0027), not at passive first exposure and not on Activity ingest. Later-enabled integrations mint through spec §37.5 resolve.

Recommended uniqueness:

UNIQUE(audience_id, external_alias)
UNIQUE(player_id, audience_id) WHERE revoked_at IS NULL

Parties and mappings​

Organisations use a generic party model internally (ADR 0001): player, operator, provider, affiliate, merchant, partner, and integration. Externally, keep typed prefixes. Never pp_party_....

Player identity tables are specialised (spec §15): players, authentication_identities (including opaque iam_subject for password reverse lookup), player_aliases, identity_links, integrations, player_sessions, webauthn_credentials. Live loyalty ledger may reference the internal player UUID. Partner analytics and studio reads do not: they are Audience-scoped numbers keyed by a Pairwise alias the caller was issued, or a provider-Party cohort of distinct Accounts (ADR 0026). Canonical DDL: TypeORM (ADR 0023).

External IDs issued by operators or providers (user_123, token_xyz) are stored separately, scoped to (owner, external_id, type). They are not PlayerPal aliases.

Do not encode relationships into identifiers (pp_plr_casino123_provider456_...). Relationships change; identifiers should not.

Player-facing verbs are only Unlink and Delete Account. Anonymisation is how Delete Account retains analytics, not a third control. Mutating calls live next to GET /v1/me, require recent reauthentication (same idea as recovery-code rotate), and take Idempotency-Key.

Unlink follows spec §36.6:

  1. Player reauthenticates to PlayerPal.
  2. Player chooses the exact operator/provider relationship (DELETE /v1/me/connections/:audienceId).
  3. PlayerPal records revoked_at / unlinked_at (spec §36.6 revoked_at/effective_at) and stops qualifying later events (occurred_at at or after that cutoff).
  4. Aliases remain reserved and auditable; they are not reassigned. Relink of that Audience mints a new Pairwise alias.
  5. A status delta may notify the partner asynchronously. Failure of that notice does not fail Unlink. Demo Bucks has no DELETE /v1/player-links/:playerRef; PlayerPal POSTs player-link-notices and Bucks may reconcile GET /v1/aliases/:playerAlias (ADR 0032). Operator link status stays linked only while the local alias row remains (stale-linked if notice failed and PlayerPal is down).
  6. Delayed activity before the revocation timestamp may still qualify; activity at or after it does not. Play at the partner continues; Points already on the ledger stay.

The Unlink target is the Audience the Player consented to. Operator-led → operator Party (cascade-revoke provider aliases for that operator’s integrations). Provider-led → that provider-operator integration only. GET /v1/me provider_operator rows include operator_audience_id (that operator Party public id, from integration config, never a partner payload). The Account groups those rows under the operator Party when that Party is also connected; confirm of operator Unlink names every Audience in that cascade.

Delete Account (DELETE /v1/me) is the erasure verb spec §36.6 does not name: revoke all aliases and links; delete Authentication identities, passkeys, sessions, and external ids; delete the mapped IAM subject in ZITADEL (DELETE /v2/users/:id) before anonymising the PlayerPal graph; destroy the UUID↔analytics-subject mapping; null player_id and player_alias on retained Activity; keep round_ref, amounts, occurred_at, integration, and analytics subject. Local demo grants the bootstrap login-client machine user ORG_USER_MANAGER via bash infra/scripts/ensure-login-client-user-manager.sh (also run from task up) so DeleteUser is allowed — do not promote login-client to IAM_OWNER (ADR 0022). If IAM delete fails, Delete Account returns 503 and leaves the Account active. PlayerPal does not command partner erasure. v1 does not crypto-shred backups (ADR 0026).

Rules from day one​

  1. Never expose internal player UUIDs.
  2. Never expose pp_plr_... to an operator or provider.
  3. Generate aliases from cryptographically random values.
  4. Never derive IDs from PII or partner identifiers.
  5. Never recycle or reassign IDs.
  6. Scope every partner-issued identifier to its issuer.
  7. Design for alias revocation and rotation even if unused at launch.
  8. Never put the analytics subject on a player or partner API.

See ADR 0013 (typed prefixes), ADR 0014 (provider-operator audience), ADR 0026 (analytics subject and cohort), ADR 0031 (Account connection groups), and ADR 0032 (demo Bucks unlink notice).