Persistence contract
Identity data model
Every identity field RustyAuth stores, how it is validated, and what each integration boundary is allowed to see.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.
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.
| Field | Stored type | Meaning | Exposure |
|---|---|---|---|
id | UUID | Stable account ID and WebAuthn user handle | HTTP, RPC, JWT sub |
profile | Object | Optional given, family and display names | HTTP and RPC; not JWT |
identifiers | 1–20 records | Canonical email/phone discovery and contact records | HTTP and RPC |
passkeys | 1+ records | Credential state plus private verification metadata | Metadata-only projections |
sessionVersion | Integer | Internal revoke-all generation copied into sessions | JWT claim only |
createdAt | Unix seconds | Account creation time | HTTP 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.
| Field | Type | Rule |
|---|---|---|
type | email | phone | Namespace used with the canonical value as a unique key |
value | String | ASCII-lowercase dot-atom email or normalized E.164 phone |
verified | Boolean | Control of the address or number has been confirmed by a trusted workflow |
verifiedAt | Timestamp? | Set when verification becomes true; legacy verified email may be null |
primary | Boolean | Exactly one identifier is primary for the whole account |
createdAt | Timestamp | When the identifier was attached |
Canonical values
- Email is trimmed, ASCII-lowercased and validated as the supported dot-atom form. Quoted local parts and internationalized addresses are not supported in 0.1.
- Phone input starts with
+; common visual separators are removed and the stored E.164 form contains 8–15 digits. - Lookup is exact after canonicalization. Making an identifier primary never makes it verified.
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 field | Meaning |
|---|---|
id | Unpadded Base64URL credential ID and uniqueness key |
label | 1–80 character presentation label |
counter | Last accepted authenticator signature counter |
createdAt / lastUsedAt | Registration and latest successful-use timestamps |
passkey | Opaque 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 field | Meaning |
|---|---|
id / userId | Session UUID and owning account UUID |
authMethod | passkey or development-only agent |
currentCredentialId | Passkey used to create the session, when applicable |
sessionVersion | Account generation captured when the session was issued |
createdAt / lastSeenAt | Authentication time and sliding idle activity |
absoluteExpiresAt | Non-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
| Boundary | Returned | Excluded |
|---|---|---|
GET /v1/account | UUID, profile, identifiers, creation time | Passkeys, sessions, internal generation |
GET /v1/credentials | Passkey ID, label, timestamps, current marker | Credential state, public key, counter |
Token response | Preferred email/phone, verification booleans, profile, JWT | Full identifier and credential records |
JWT | UUID, session, tenant and authentication claims | Names, contacts, verification, passkeys |
IdentityService | UUID, profile, identifiers, passkey metadata | Credential state, counters, sessions, tokens |
Event APIs | Sequence, type, subject UUID, tenant, timestamp | Contact 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.
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
- Use the UUID as the downstream foreign identity key.
- Treat contact identifiers as mutable and verification as independent from primary selection.
- Never authorize by email, phone or display name.
- Keep the identity RPC credential in a trusted service, never browser code.
- Validate JWT signature, issuer, audience, expiry, tenant and required authentication claims.
- 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.