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
| Identifier | Represents | Who may see it |
|---|---|---|
| Internal UUIDv7 | Database primary key | PlayerPal only. Never exposed |
pp_plr_... | Player-visible PlayerPal identity | Player and PlayerPal only |
pp_plr_opr_... | Player alias for one operator | That operator and PlayerPal |
pp_plr_prv_... | Player alias for one provider-operator audience | That integration and PlayerPal |
pp_opr_... / pp_prv_... / pp_aff_... | Party (organisation) | Authorized parties |
pp_int_... | Configured integration / audience | Authorized parties |
pp_ses_... | Session | Relevant client and PlayerPal |
pp_anl_... | Analytics subject for retained Activity | PlayerPal 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.
Unlink and Delete Account
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:
- Player reauthenticates to PlayerPal.
- Player chooses the exact operator/provider relationship (
DELETE /v1/me/connections/:audienceId). - PlayerPal records
revoked_at/unlinked_at(spec §36.6revoked_at/effective_at) and stops qualifying later events (occurred_atat or after that cutoff). - Aliases remain reserved and auditable; they are not reassigned. Relink of that Audience mints a new Pairwise alias.
- 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 POSTsplayer-link-noticesand Bucks may reconcileGET /v1/aliases/:playerAlias(ADR 0032). Operator link status stayslinkedonly while the local alias row remains (stale-linked if notice failed and PlayerPal is down). - 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
- Never expose internal player UUIDs.
- Never expose
pp_plr_...to an operator or provider. - Generate aliases from cryptographically random values.
- Never derive IDs from PII or partner identifiers.
- Never recycle or reassign IDs.
- Scope every partner-issued identifier to its issuer.
- Design for alias revocation and rotation even if unused at launch.
- 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).