Persistence contract

Identity data model

Every identity field RustyAuth stores, how it is validated, and what each integration boundary is allowed to see.
The durable anchor is a UUID

Email, phone, names and passkeys can change without changing the account ID or WebAuthn user handle. Downstream systems should key people by the RustyAuth UUID—not a contact value.

Durable aggregateAccountUUID · profile · identifiers · passkeys
+
Expiring security stateSessionsHashed bearer key · idle + absolute expiry
+
Single useCeremoniesServer-side WebAuthn state · five minutes

The account aggregate

auth:user:<uuid> is the authoritative identity record in SableDB. Account and index mutations use atomic pipelines. Every read validates identifier count, uniqueness, primary selection, verification consistency, phone canonicalization and safe profile text.

FieldStored typeMeaningExposure
idUUIDStable account ID and WebAuthn user handleHTTP, RPC, JWT sub
profileObjectOptional given, family and display namesHTTP and RPC; not JWT
identifiers1–20 recordsCanonical email/phone discovery and contact recordsHTTP and RPC
passkeys1+ recordsCredential state plus private verification metadataMetadata-only projections
sessionVersionIntegerInternal revoke-all generation copied into sessionsJWT claim only
createdAtUnix secondsAccount creation timeHTTP and RPC as RFC 3339

Two additional on-disk fields—email and emailVerified—mirror the preferred email for compatibility with the original email-only model. New code treats identifiers as the source of truth.

Email and phone identifiers

An identifier discovers an account and records contact/verification state. It is not an authenticator. Accounts can be email-only, phone-only or contain any mix up to 20 globally unique identifiers.

FieldTypeRule
typeemail | phoneNamespace used with the canonical value as a unique key
valueStringASCII-lowercase dot-atom email or normalized E.164 phone
verifiedBooleanControl of the address or number has been confirmed by a trusted workflow
verifiedAtTimestamp?Set when verification becomes true; legacy verified email may be null
primaryBooleanExactly one identifier is primary for the whole account
createdAtTimestampWhen the identifier was attached

Canonical values

Production browser flows create unverified identifiers and emit a verification-requested event. Delivery and challenge consumption are not implemented yet. The trusted private identity RPC can explicitly set or clear verification state.

Profile names

The profile contains only optional givenName (100 characters), familyName (100) and displayName (200). Updates replace the complete profile; blank values clear fields. RustyAuth trims values and rejects control, zero-width and directional-formatting characters, while preserving ordinary Unicode and case.

The display name labels WebAuthn registration, but it never replaces the UUID user handle. RustyAuth does not persist title, middle name, pronouns, birth date, address, locale, avatar, organization role or arbitrary attributes.

Passkeys

Stored fieldMeaning
idUnpadded Base64URL credential ID and uniqueness key
label1–80 character presentation label
counterLast accepted authenticator signature counter
createdAt / lastUsedAtRegistration and latest successful-use timestamps
passkeyOpaque webauthn-rs public-key credential state; private to RustyAuth

Only ID, label and timestamps cross HTTP/RPC metadata projections. New credential material must come from a WebAuthn ceremony. Duplicate IDs are rejected, a regressing non-zero counter fails authentication, and the final passkey cannot be revoked.

Sessions and step-up state

Stored fieldMeaning
id / userIdSession UUID and owning account UUID
authMethodpasskey or development-only agent
currentCredentialIdPasskey used to create the session, when applicable
sessionVersionAccount generation captured when the session was issued
createdAt / lastSeenAtAuthentication time and sliding idle activity
absoluteExpiresAtNon-extendable session deadline

The raw cookie is never stored: its SHA-256 digest forms the SableDB key. Agent sessions are read-only for identity and credential mutation. Recent-passkey operations additionally require a passkey session created within five minutes.

Registration and authentication ceremonies contain server-side webauthn-rs state, expire after five minutes and are atomically consumed. Additional-passkey ceremonies are bound to their purpose, account and exact initiating session. Ceremonies and agent handoffs are excluded from backups.

Indexes, events and recovery

Durable indexes map canonical identifiers and credential IDs to the owning UUID. Backup validation checks both directions, so an aggregate with a missing index—or an index that points to an account that does not own it—cannot be restored.

Identity events persist sequence, event UUID, type, optional subject UUID, tenant, timestamp and a redacted JSON object. They do not contain names, contact values, cookies, JWTs, passkey material or challenge payloads. Sessions are captured by logical backup, but restore skips them and increments account session generations by default.

Exposure by boundary

BoundaryReturnedExcluded
GET /v1/accountUUID, profile, identifiers, creation timePasskeys, sessions, internal generation
GET /v1/credentialsPasskey ID, label, timestamps, current markerCredential state, public key, counter
Token responsePreferred email/phone, verification booleans, profile, JWTFull identifier and credential records
JWTUUID, session, tenant and authentication claimsNames, contacts, verification, passkeys
IdentityServiceUUID, profile, identifiers, passkey metadataCredential state, counters, sessions, tokens
Event APIsSequence, type, subject UUID, tenant, timestampContact values and bearer/credential payloads

Deliberately not persisted

RustyAuth does not store passwords or hashes, recovery codes, raw session/handoff values, issued JWT strings, completed assertion/attestation payloads, email/SMS verification tokens, application roles, permissions, entitlements, billing state, resource ownership or custom claims.

Application data belongs with the application

Persist additional person or customer fields in your own data model keyed by the RustyAuth UUID. Do not overload names, passkey labels or events as an arbitrary metadata store.

Integration rules

  1. Use the UUID as the downstream foreign identity key.
  2. Treat contact identifiers as mutable and verification as independent from primary selection.
  3. Never authorize by email, phone or display name.
  4. Keep the identity RPC credential in a trusted service, never browser code.
  5. Validate JWT signature, issuer, audience, expiry, tenant and required authentication claims.
  6. Plan verified email/SMS delivery and account recovery before production adoption.

For exact request contracts, continue to the HTTP and RPC API. For key families, backup behavior and trust boundaries, see Architecture.