Account Model
Account Model
How user identity is structured in core.auth — the master account, identity tracking, and data boundaries.
The Master Account
Every user in the UNVRSL ecosystem has exactly one master account stored in core.auth. This is the single source of truth for identity. No app stores credentials, email addresses, or personal data — only a reference to the master account.
core.auth (identity provider)
┌─────────────────────────────────────┐
│ users (master) │
├─────────────────────────────────────┤
│ id (PK, UUID) │
│ email (unique, verified) │
│ password_hash (bcrypt/argon2) │
│ display_name │
│ phone (optional, verified) │
│ avatar_url │
│ status (active/suspended/deleted) │
│ 2fa_enabled (bool) │
│ 2fa_secret (encrypted, nullable) │
│ 2fa_type (totp/webauthn/null) │
│ recovery_codes (encrypted, JSON) │
│ created_at │
│ updated_at │
│ last_login_at │
│ account_type_id (FK → account_types)│
│ account_type_expires_at (nullable) │
│ -- Quotas enforced via account_types│
│ -- + user_quota_usage (see L2) │
└─────────────────────────────────────┘Design Decisions
- UUID primary keys — avoids sequential ID exposure, safe for public-facing APIs
- Argon2id for password hashing — PHP's
PASSWORD_ARGON2IDdefault. Minimum 10 characters. No breach database checking. - Encrypted 2FA secrets — stored encrypted at rest (AES-256), decrypted only during verification
- Soft deletes —
deleted_attimestamp instead of hard deletes. Preserves referential integrity for app data. User can be restored within grace period. - Account types — users are assigned an
account_type_idreferencing theaccount_typestable. Four quota types: storage (MB), AI credits, hypermedia credits, and special general credits (overflow pool). Quotas are defined per type, not per user. Actual usage is tracked inuser_quota_usage. - Credit-based AI quotas — AI usage is measured in credits, not calls. Input tokens cost 100 credits each, output tokens 250, cached tokens 5. This allows fine-grained cost control across different models and operations.
- Special general credits — a universal overflow pool. When a user exhausts their storage, AI, or hypermedia quota, they can continue using the resource by consuming special credits. Conversion rates are defined per resource type. This lets users self-allocate based on their actual needs (e.g. heavy storage users can borrow from their credit pool).
- account_type_expires_at — nullable. For temporary tier assignments (promotions, trials). When expired, user reverts to
freetier.
Identity Data (Tracking)
Separate from the account itself, core.auth tracks identifying metadata for security purposes — device fingerprints, IP history, location data. This powers anomaly detection ("new device login" alerts) and session auditing.
core.auth SQL:
┌─────────────────────────────────────┐
│ ident_events │
├─────────────────────────────────────┤
│ id (PK) │
│ user_id (FK → users) │
│ event_type (login/refresh/revoke) │
│ ip_address │
│ user_agent │
│ geo_location (resolved from IP) │
│ device_fingerprint │
│ session_id (FK, nullable) │
│ created_at │
└─────────────────────────────────────┘What Gets Tracked
| Event | Logged When | Purpose |
login | Successful authentication | Session creation audit |
logout | User logs out | Session termination audit |
refresh | Access token refreshed | Activity tracking |
refresh_replay_detected | Revoked refresh token reused | Attack detection |
2fa_verify | 2FA challenge completed | Critical action audit |
password_change | Password modified | Security audit |
email_change | Email modified | Security audit |
Retention
ident_events accumulate fast. A cleanup job prunes events older than a configurable retention period (default: 90 days). Recent events (last 30 days) are always available for the "recent activity" view in the account dashboard.
Data Boundaries
The separation between core.auth and apps is strict:
| Data Type | Stored In | Accessed By |
| Email, password, 2FA | core.auth only | core.auth internally |
| Profile (name, avatar) | core.auth | Apps via API (with user consent) |
| Ident events (IP, device) | core.auth only | core.auth internally |
| App-specific data | App's own database | App only |
| Quotas | core.auth | core.services via API |
Why This Matters
- Breach isolation — if an app is compromised, no credentials leak. The app has no passwords to steal.
- Single source of truth — user changes email once in core.auth, all apps see the change via API. No stale data across apps.
- GDPR/data minimization — apps store only what they need for their functionality. Identity data lives in one place, deletable in one place.
- App independence — an app can be rebuilt, replaced, or decommissioned without touching user identity. The
linked_userstable is trivially portable.
Next: App Integration — how apps register and link users.