# 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](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](decisions/0026-analytics-subject-and-provider-cohort.md).

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

```text
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.

```text
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:

```text
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](decisions/0027-alias-provisioning-at-link.md)), not at passive first exposure and not on Activity ingest. Later-enabled integrations mint through spec §37.5 resolve.

Recommended uniqueness:

```text
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](decisions/0026-analytics-subject-and-provider-cohort.md)). Canonical DDL: TypeORM ([ADR 0023](decisions/0023-typeorm-data-layer.md)).

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:

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](decisions/0032-unlink-partner-notice.md)). 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](decisions/0022-official-login-app-fork.md)). 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](decisions/0026-analytics-subject-and-provider-cohort.md)).

## 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).
