Skip to content

Alias

Try the interactive example ↓

Canonical identity resolution across fragmented external accounts. One internal user carries many linked identities - Discord accounts, wallet addresses, domain handles, game characters, hashed recovery contacts, scoped anonymous personas - and any of them resolves back to that one user in a single lookup.

When to reach for it: turning a Discord id, wallet address, or partner account id into the internal user it belongs to; recording proof that a user owns an external handle; giving an agent a pseudonymous persona scoped to one world; folding a duplicate signup into the surviving account.

When not to: signing a user in, sessions, and OAuth provider accounts belong to Auth; a user list owned by an external directory belongs to SCIM; allocating a unique display name under a charset and reservation policy belongs to Moniker. An external-id-to-user map with no verification, revocation, or merge semantics is one KV key.

Concepts

  • One prime user, many aliases. Every alias points at a single internal user id - the caller's JWT subject unless an admin names another.
  • Normalization decides identity. platform and external_id fold into a normalized_id unique across the tenant: wallet lowercases and hex-checks, domain strips the scheme and trailing slash, email lowercases then SHA-256 hashes, discord/game/partner keep the id verbatim, anything else is lowercased. The platform name always is.
  • Verification is evidence, not a flag. A new link is pending_verification; attestations promote it to verified. An attestation may carry an expiry, and once every one has lapsed the alias falls back to pending_verification.
  • Two independent gates. visibility controls who may read the alias record; scopes controls who may resolve through it. Resolve deliberately ignores visibility.
  • Primary is per scope. One alias is primary per (user, scope) pair; omitting a scope uses a single global slot, and promoting inside one scope demotes only that scope's previous holder.
  • Ghost proxies are one-way. An anonymous proxy issues a random handle bound to an audience and a purpose. The prime user id comes back only to an admin or a caller holding identity.proxy_inspect.

Linking an external account

snug alias link --platform discord --id docs4-903115 --handle docs4-nova --scope community.rewards
snug alias link --platform wallet --id 0xA1B2C3D4E5F6DEADBEEF
snug alias link --platform domain --id https://docs4.example.com/ --handle docs4.example.com

Over HTTP the same call is POST /api/v1/alias/link:

curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"platform":"discord","external_id":"docs4-903115","handle":"docs4-nova"}' \
  http://localhost:4000/api/v1/alias/link
{
  "status": 201,
  "msg": "Created",
  "data": {
    "alias_id": "0199e100-0000-7000-8000-000000000001",
    "user_id": "docs-wave",
    "normalized_id": "discord:docs4-903115",
    "verification_status": "pending_verification"
  }
}

Relinking the same normalized id as the same owner is idempotent - it returns the original alias_id, still under a 201 Created. If a different user holds it, the call fails with 409 alias_conflict naming that user, the signal to open a merge. Linking on behalf of someone else needs a platform admin token.

Resolving and listing

snug alias resolve docs4-903115 --platform discord --scope community.rewards
snug alias resolve discord:docs4-903115
snug alias resolve 0199e100-0000-7000-8000-000000000001
snug alias list --user docs-wave --filter 'verification_status[eq]=verified' --filter 'platform[eq]=discord'

Resolution answers with the prime user_id, the alias, its verification status, and a deliberately narrow visible_metadata - display_name, handle, and avatar_url/locale when present. A bare external id is only ever tried as a normalized id, so snug alias resolve docs4-903115 is 404 alias_not_found while --platform discord or the full discord:docs4-903115 succeeds.

Scopes gate resolution: asking for a scope the alias does not list is 403 insufficient_permissions, but an alias with an empty scopes list satisfies every request - list scopes on anything you mean to restrict. Visibility is not consulted here, so any authenticated caller can turn a private alias into a user id.

Listing speaks the standard list contract. It filters on platform (eq, ne, in, prefix), verification_status, identity_kind, and visibility (eq, ne, in), is_primary (eq, ne), and linked_at and created_at (gt, gte, lt, lte); it sorts by created_at (the default, newest first), linked_at, or platform, with alias_id breaking ties. An unknown field, operator, or enum value is 400 invalid_request naming what the list accepts. A caller who is not the user, an admin, or a support.lookup holder sees only public aliases, and page.total (with total=true) counts only those. Revoked aliases are listed unless filtered out.

Verification and the primary identity

snug alias attest --alias-id als_cJxB... --method social_proof \
  --issuer social-proof --challenge chal_docs4nova --confidence 0.95
snug alias primary set --alias-id als_cJxB...                  # global slot
snug alias primary set --alias-id als_cJxB... --scope display  # display slot

Attesting flips verification_status to verified and appends the attestation with its issuer, proof, and confidence. Promoting a revoked alias is refused with 400 invalid_request. Because each scope holds its own primary, an alias primary anywhere reports is_primary: true - the flag does not say which scope.

Anonymous proxies

snug alias proxy create --audience world:docs4-forest-01 \
  --purpose agent_world_interaction --scope world.chat --expires-in 3600

You get back a proxy_id, a generated handle such as phantom-3u4w, the audience and purpose, status: active, and an expires_at. The response omits user_id entirely for ordinary callers; only an admin or identity.proxy_inspect sees it. Resolving a live handle refuses to deanonymize and answers 409 alias_conflict. Once the TTL passes the handle stops resolving at all (404), and within one sweep interval the background worker marks the proxy expired and frees the handle - both confirmed against a one-second proxy.

Recovery contacts

snug alias recovery add --email docs4-user@example.com --scope account.recovery
snug alias recovery verify --recovery-id rcm_zMRY... --email docs4-user@example.com

Only a salted hash is persisted; the raw contact never comes back out, and verification re-hashes what you submit and compares. A mismatch is 400 recovery_challenge_failed. Listing someone else's contacts with snug alias recovery list --user <id> needs a platform admin token or the account.recovery scope. The list is a list, oldest first, filtering on contact_kind, verification_status, and created_at.

Aggregated metadata

snug alias metadata --user docs-wave collects fields across a user's aliases with the source alias, platform, and timestamp attached - newest write wins, and a primary alias overrides display_name. Aggregation skips private and revoked aliases, and private is the default visibility, so this stays empty until you link something with "visibility":"public". The key version inside metadata is reserved by the storage layer and is replaced with an integer.

Revoking

snug alias revoke --alias-id als_cJxB... --reason compromised

Revocation is a tombstone, not a delete: it releases the lookup entry, clears the alias out of every primary scope, and stamps revoked_at with the reason, while the document survives for audit. Resolution stops immediately, GET still works, and the external id becomes linkable again as a brand-new alias with a new id. Revoking twice is a no-op.

Limits and configuration

100 aliases and 25 active proxies per user, 250 aliases per merge, 32 KB of metadata, and a seven-day default proxy TTL - all tunable, along with the expiry sweep interval, through the ALIAS_* variables.

Reference

  • Alias API - every endpoint, callable
  • Identity merges - folding a duplicate user into the survivor
  • Related: Auth for accounts, sessions, and OAuth linking, SCIM for directory-driven provisioning, Moniker for allocating unique names

Identifiers

Alias, merge, proxy and recovery record IDs are opaque UUIDv7 strings. User subjects, normalized external identities and public proxy handles retain their own formats.

Interactive example

Connect external names to an internal identity. Resolve a linked handle without making each external system its own user record.

An illustrative example that runs in your browser. No account or API calls required.