Database Schemas
Database Schemas
Complete SQL DDL for all core.auth tables and the minimal app-side schema. PostgreSQL dialect.
core.auth Tables
users
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
email_verified BOOLEAN DEFAULT FALSE,
password_hash VARCHAR(255) NOT NULL, -- Argon2id (PASSWORD_ARGON2ID)
display_name VARCHAR(100),
phone VARCHAR(20),
phone_verified BOOLEAN DEFAULT FALSE,
avatar_url VARCHAR(500),
status VARCHAR(20) DEFAULT 'active', -- active, suspended, deleted
account_type_id INT REFERENCES account_types(id),
account_type_expires_at TIMESTAMP, -- for temporary tiers (promotions, trials); reverts to 'free' on expiry
-- 2FA
two_fa_enabled BOOLEAN DEFAULT FALSE,
two_fa_type VARCHAR(20), -- totp, webauthn, null
two_fa_secret TEXT, -- encrypted
recovery_codes TEXT, -- encrypted JSON array
-- Quotas (enforced via account_types + user_quota_usage — see below)
-- Timestamps
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
last_login_at TIMESTAMP,
deleted_at TIMESTAMP -- soft delete
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_status ON users(status);sessions
The sessions table is the single source of truth for active logins. A session row existing means the user is authenticated. Deleting it logs the user out.
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
app_id INT NOT NULL REFERENCES registered_apps(id),
ip_address INET,
user_agent TEXT,
geo_location VARCHAR(255),
device_fingerprint VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
last_activity TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL,
revoked_at TIMESTAMP,
revoke_reason VARCHAR(50) -- logout, compromise, admin, expiry
);
CREATE INDEX idx_sessions_user ON sessions(user_id);
CREATE INDEX idx_sessions_expires ON sessions(expires_at);
CREATE INDEX idx_sessions_active ON sessions(user_id, revoked_at, expires_at);Note: There is no
refresh_tokens table. The session row itself is what the refresh endpoint checks. There is no token rotation, no family tracking, no replaced_by chain. The session exists or it doesn't.registered_apps
CREATE TABLE registered_apps (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
slug VARCHAR(50) UNIQUE NOT NULL,
client_id VARCHAR(64) UNIQUE NOT NULL,
client_secret VARCHAR(255) NOT NULL, -- hashed
redirect_uris JSON NOT NULL,
allowed_scopes JSON NOT NULL,
webhook_url VARCHAR(500),
-- Token configuration
access_token_ttl INT DEFAULT 900, -- 15 min (seconds)
session_ttl INT DEFAULT 2592000, -- 30 days (seconds)
hard_session_cap INT DEFAULT 2592000, -- 30 days from login
-- Status
status VARCHAR(20) DEFAULT 'active', -- active, inactive, revoked
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);user_app_access
CREATE TABLE user_app_access (
user_id UUID NOT NULL REFERENCES users(id),
app_id INT NOT NULL REFERENCES registered_apps(id),
role VARCHAR(20) DEFAULT 'user', -- admin, user, viewer
granted_at TIMESTAMP DEFAULT NOW(),
granted_by UUID REFERENCES users(id),
PRIMARY KEY (user_id, app_id)
);
CREATE INDEX idx_uaa_user ON user_app_access(user_id);
CREATE INDEX idx_uaa_app ON user_app_access(app_id);token_blacklist
Used for mid-window revocation of advanced/critical scope operations. Entries are keyed by session ID (
sid from JWT payload), not by a separate JTI.CREATE TABLE token_blacklist (
id SERIAL PRIMARY KEY,
token_jti VARCHAR(64) UNIQUE NOT NULL, -- session ID (sid from JWT)
user_id UUID NOT NULL REFERENCES users(id),
reason VARCHAR(50) NOT NULL, -- logout, compromise, admin
ip_address INET,
blacklisted_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL -- auto-cleanup after JWT TTL
);
CREATE INDEX idx_blacklist_jti ON token_blacklist(token_jti);
CREATE INDEX idx_blacklist_expires ON token_blacklist(expires_at);ident_events
CREATE TABLE ident_events (
id SERIAL PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
event_type VARCHAR(30) NOT NULL,
ip_address INET,
user_agent TEXT,
geo_location VARCHAR(255),
device_fingerprint VARCHAR(255),
session_id UUID REFERENCES sessions(id),
metadata JSON,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_ident_user ON ident_events(user_id);
CREATE INDEX idx_ident_type ON ident_events(event_type);
CREATE INDEX idx_ident_created ON ident_events(created_at);webauthn_credentials
Stores registered passkeys/security keys for WebAuthn authentication.
CREATE TABLE webauthn_credentials (
id SERIAL PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
credential_id VARCHAR(512) UNIQUE NOT NULL, -- base64url
public_key TEXT NOT NULL, -- COSE-encoded public key
sign_count BIGINT DEFAULT 0, -- clone detection
aaguid VARCHAR(36), -- authenticator identifier
nickname VARCHAR(100), -- "My iPhone", "Work laptop"
transports JSON, -- ["internal", "usb", "nfc"]
attestation_format VARCHAR(50),
created_at TIMESTAMP DEFAULT NOW(),
last_used_at TIMESTAMP
);
CREATE INDEX idx_webauthn_user ON webauthn_credentials(user_id);
CREATE INDEX idx_webauthn_credential ON webauthn_credentials(credential_id);recovery_codes
Single-use recovery codes for 2FA fallback. Stored as bcrypt hashes.
CREATE TABLE recovery_codes (
id SERIAL PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
code_hash VARCHAR(255) NOT NULL, -- bcrypt hash
used_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_recovery_user ON recovery_codes(user_id);
CREATE INDEX idx_recovery_unused ON recovery_codes(user_id) WHERE used_at IS NULL;Account Types & Quotas
The Four Quota Types
Every account type defines four resource quotas:
| Quota | Unit | What It Covers |
| Storage | MB | Total file/object storage across all apps |
| AI Credits | Credits/month | All AI operations — chat, completions, image generation, embeddings |
| Hypermedia Credits | Credits/month | Media processing — conversion, compression, enhancement jobs |
| Special General Credits | Credits/month | Overflow pool — usable for any resource type when the primary quota is exhausted |
#### AI Credit Costs
AI usage is credit-based, not call-based. Different token types cost different amounts:
| Token Type | Cost (credits per token) | Notes |
| Input token | 100 | User-provided text, system prompts |
| Output token | 250 | Model-generated text |
| Cached token | 5 | Previously processed context (prompt caching) |
Example: A 1000-input + 500-output token conversation costs
(1000 × 100) + (500 × 250) = 225,000 credits.> Phase 2+: Dedicated cost tables will define per-model and per-operation costs (image generation, embeddings, audio transcription, etc.). The above are baseline multipliers.
#### Special General Credits (Overflow)
The special general credits pool acts as a universal overflow buffer. When a user exhausts their primary quota for storage, AI, or hypermedia, they can continue using the resource — but every unit consumed draws from special general credits instead.
Conversion examples (illustrative — final rates TBD):
| Overflow Usage | Special Credits Cost |
| 1 MB storage beyond quota | TBD credits/MB |
| 1 AI input token beyond quota | TBD credits/token |
| 1 hypermedia job beyond quota | TBD credits/job |
This means a user who primarily uses storage can "borrow" from their special credits to store more, at the cost of having fewer credits available for AI or hypermedia overflow. It's a flexible system that lets users self-allocate resources based on their actual needs.
Key behavior:
- Special general credits reset monthly (same cycle as AI and hypermedia)
- Overflow consumption is tracked separately so users can see how much overflow they used
- When special credits hit zero, hard quota enforcement kicks in — no more overflow
account_types
Defines quota tiers. Admin-managed. Users are assigned an account type; quotas are enforced by comparing actual usage against the type's limits.
CREATE TABLE account_types (
id SERIAL PRIMARY KEY,
name VARCHAR(50) UNIQUE NOT NULL,
-- Storage
storage_quota_mb INT NOT NULL DEFAULT 1024,
-- AI credits (monthly)
ai_credits_monthly BIGINT NOT NULL DEFAULT 0,
-- Hypermedia credits (monthly)
hypermedia_credits_monthly BIGINT NOT NULL DEFAULT 0,
-- Special general credits (monthly, overflow pool)
special_credits_monthly BIGINT NOT NULL DEFAULT 0,
-- Metadata
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Seed tiers (illustrative credit values — calibrate before launch)
INSERT INTO account_types
(name, storage_quota_mb, ai_credits_monthly, hypermedia_credits_monthly, special_credits_monthly, description)
VALUES
('free', 1024, 4000000, 100000, 500000, 'Free tier — basic limits'),
('light', 5120, 20000000, 500000, 2000000, 'Light tier — for regular users'),
('pro', 20480, 80000000, 2000000, 8000000, 'Pro tier — for power users'),
('max', 51200, 200000000, 5000000, 20000000, 'Max tier — near-unlimited'),
('admin', NULL, NULL, NULL, NULL, 'Admin — unlimited (no quota enforcement)'),
('promotions', 20480, 80000000, 2000000, 8000000, 'Promotional tier — same as pro, temporary assignment');Notes:
admintier: NULL quotas = no enforcement. Used for platform administrators and service accounts.promotionstier: same limits as pro, but assigned temporarily (e.g. trial, event giveaway). Expiration is handled at the user level (seeaccount_type_expires_aton the users table).- Credit values above are illustrative placeholders. Final calibration happens during load testing and pricing design.
is_activeallows disabling a tier without deleting it (prevents assigning new users to it).
ai_resource_costs
Defines the credit cost per unit for each AI resource operation. Admin-managed. Used by the quota enforcement engine to calculate credit deductions.
CREATE TABLE ai_resource_costs (
id SERIAL PRIMARY KEY,
resource_type VARCHAR(50) NOT NULL, -- 'input_token', 'output_token', 'cached_token', 'image_generation', 'embedding', 'audio_transcription'
cost_per_unit BIGINT NOT NULL, -- credits per unit
model_name VARCHAR(100), -- NULL = applies to all models, or specific model override
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE (resource_type, model_name)
);
INSERT INTO ai_resource_costs (resource_type, cost_per_unit, model_name, description)
VALUES
('input_token', 100, NULL, 'Base cost per input token (all models)'),
('output_token', 250, NULL, 'Base cost per output token (all models)'),
('cached_token', 5, NULL, 'Cost per cached/prompt-cached token');> Phase 2+: Add per-model overrides (e.g. a premium model might cost 2× the base rate). Add image generation, embedding, and audio costs as those features come online.
hypermedia_resource_costs
Defines the credit cost per unit for hypermedia operations.
CREATE TABLE hypermedia_resource_costs (
id SERIAL PRIMARY KEY,
job_type VARCHAR(50) NOT NULL, -- 'image_resize', 'pdf_convert', 'video_compress', 'audio_convert'
cost_per_job BIGINT NOT NULL, -- credits per job
cost_per_mb BIGINT DEFAULT 0, -- additional credits per MB of input
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);> Phase 2+: Populate with concrete costs once hypermedia pipeline operations are finalized.
special_credit_conversion_rates
Defines how many special general credits equal one unit of overflow for each resource type.
CREATE TABLE special_credit_conversion_rates (
id SERIAL PRIMARY KEY,
resource_type VARCHAR(30) NOT NULL, -- 'storage_mb', 'ai_input_token', 'ai_output_token', 'hypermedia_job'
credits_per_unit BIGINT NOT NULL, -- special credits consumed per unit of overflow
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE (resource_type)
);> Phase 2+: Populate once pricing/overflow strategy is finalized. These rates should be higher than the primary quota rates (overflow is intentionally more expensive to discourage abuse).
user_third_party_auths
Stores linked third-party OAuth accounts. A user can link multiple providers (Google, Apple, Facebook).
CREATE TABLE user_third_party_auths (
id SERIAL PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider VARCHAR(30) NOT NULL, -- 'google', 'apple', 'facebook'
provider_user_id VARCHAR(255) NOT NULL,
provider_email VARCHAR(255),
provider_name VARCHAR(255),
provider_avatar_url VARCHAR(500),
access_token TEXT, -- encrypted
refresh_token TEXT, -- encrypted
linked_at TIMESTAMP DEFAULT NOW(),
last_used_at TIMESTAMP,
UNIQUE (provider, provider_user_id)
);
CREATE INDEX idx_third_party_user ON user_third_party_auths(user_id);
CREATE INDEX idx_third_party_provider ON user_third_party_auths(provider, provider_user_id);user_quota_usage
Tracks actual per-user quota consumption. Checked at runtime before any quota-consuming action (upload, AI call, etc.).
CREATE TABLE user_quota_usage (
id SERIAL PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
quota_type VARCHAR(30) NOT NULL, -- 'storage', 'ai_credits', 'hypermedia_credits', 'special_credits'
actual_value BIGINT NOT NULL DEFAULT 0, -- current usage (bytes for storage, credits for others)
overflow_value BIGINT NOT NULL DEFAULT 0, -- amount consumed via special credits overflow (for display)
period_start TIMESTAMP NOT NULL, -- start of current billing period
quota_reset_at TIMESTAMP NOT NULL, -- when this quota resets (end of billing period)
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE (user_id, quota_type, period_start)
);
CREATE INDEX idx_quota_user ON user_quota_usage(user_id);
CREATE INDEX idx_quota_type ON user_quota_usage(user_id, quota_type);
CREATE INDEX idx_quota_reset ON user_quota_usage(quota_reset_at) WHERE quota_reset_at <= NOW();Quota types:
storage—actual_valueis bytes storedai_credits—actual_valueis credits consumed this periodhypermedia_credits—actual_valueis credits consumed this periodspecial_credits—actual_valueis special credits consumed this period (across all overflow types)
quota_reset_at — the exact datetime when this quota row resets. Typically the 1st of the next month at 00:00 UTC, but can differ per user (e.g. signup anniversary billing cycle). The maintenance job scans for rows where quota_reset_at <= NOW() and resets them. This is more reliable than fixed calendar expressions and supports per-user billing cycles.Quota types:
storage—actual_valueis bytes storedai_credits—actual_valueis credits consumed this periodhypermedia_credits—actual_valueis credits consumed this periodspecial_credits—actual_valueis special credits consumed this period (across all overflow types)
The
overflow_value column tracks how much of the primary quota's overage was covered by special credits. Used for display purposes ("You used 150 MB of overflow storage this month").Quota Enforcement Flow
User tries to upload a file (50 MB)
│
▼
App calls core.console: POST /api/quota/check
{ user_id, resource_type: 'storage', amount: 52428800 }
│
▼
core.console:
1. Get account_type for user → storage_quota_mb
2. Get actual_value from user_quota_usage for this user + 'storage'
3. remaining = (storage_quota_mb * 1048576) - actual_value
│
├── remaining >= needed
│ → allow (return { allowed: true, remaining: ... })
│
├── remaining < needed
│ → calculate overflow_needed = needed - remaining
│ → check special_credits: get account_type.special_credits_monthly
│ → get special_credits actual_value from user_quota_usage
│ → special_remaining = special_credits_monthly - special_actual_value
│ → calculate conversion: overflow_needed in special credits
│ (via special_credit_conversion_rates table)
│ │
│ ├── special_remaining covers overflow
│ │ → allow, deduct from special_credits, update overflow_value on storage
│ │ → return { allowed: true, remaining: 0, overflow_used: ... }
│ │
│ └── special_remaining insufficient
│ → reject (return { allowed: false, reason: 'quota_exceeded' })
│
└── hard block (no special credits left)
→ rejectAI credit enforcement (different flow — credits, not bytes):
User sends a chat message (est. 1000 input + 500 output tokens)
│
▼
App calls core.console: POST /api/quota/check
{ user_id, resource_type: 'ai_credits',
tokens: { input: 1000, output: 500, cached: 0 } }
│
▼
core.console:
1. Look up ai_resource_costs for input_token, output_token, cached_token
2. Calculate total cost: (1000 × 100) + (500 × 250) + (0 × 5) = 225,000 credits
3. Get ai_credits_monthly from account_types
4. Get actual_value from user_quota_usage for 'ai_credits'
5. remaining = ai_credits_monthly - actual_value
│
├── remaining >= 225,000 → allow, deduct
├── remaining < 225,000 → check special_credits overflow (same as storage)
└── neither → rejectPost-operation update: After the operation completes, the app reports actual usage:
POST /api/quota/update
{ user_id, resource_type: 'ai_credits', actual_tokens: { input: 847, output: 423, cached: 156 } }
→ recalculates actual cost and updates user_quota_usageConnection Tokens
Temporary tokens that carry app context through registration/login flows.
CREATE TABLE connection_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
app_id INT NOT NULL REFERENCES registered_apps(id),
redirect_uri VARCHAR(500) NOT NULL,
scopes JSON, -- requested scopes (placeholder: null)
state VARCHAR(128), -- CSRF state from the app
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL, -- 30 min TTL
used_at TIMESTAMP
);
CREATE INDEX idx_conn_token_expires ON connection_tokens(expires_at);A
connection_ref (the connection token UUID) is passed as a URL parameter through the entire registration/login flow. When the flow completes, core.auth uses it to look up which app initiated the flow and where to redirect back.App-Side Schema (Minimal)
Each app maintains only what it needs. This schema lives in the app's own database:
CREATE TABLE linked_users (
id SERIAL PRIMARY KEY,
core_auth_user_id UUID NOT NULL UNIQUE,
local_role VARCHAR(20) DEFAULT 'user',
local_preferences JSON DEFAULT '{}',
linked_at TIMESTAMP DEFAULT NOW(),
last_seen_at TIMESTAMP
);
CREATE INDEX idx_linked_core_user ON linked_users(core_auth_user_id);Table Relationships
users
├── sessions (1:N) — user can have multiple active sessions
├── user_app_access (1:N) — user can access multiple apps
├── user_third_party_auths (1:N) — linked Google/Apple/Facebook accounts
├── user_quota_usage (1:N) — quota tracking per type
├── token_blacklist (1:N) — blacklisted tokens per user
├── webauthn_credentials (1:N) — registered passkeys/security keys
├── recovery_codes (1:N) — 2FA recovery codes
└── ident_events (1:N) — audit log
account_types
└── users (via users.account_type_id) — each user belongs to one type
registered_apps
├── sessions (1:N) — app can have sessions from multiple users
├── user_app_access (1:N) — app can be accessed by multiple users
└── connection_tokens (1:N) — pending auth flows from this app
sessions
├── ident_events (1:N) — events can reference a session
└── token_blacklist (via token_jti = session.id)users
├── sessions (1:N) — user can have multiple active sessions
├── user_app_access (1:N) — user can access multiple apps
├── token_blacklist (1:N) — blacklisted tokens per user
├── webauthn_credentials (1:N) — registered passkeys/security keys
├── recovery_codes (1:N) — 2FA recovery codes
└── ident_events (1:N) — audit log
registered_apps
├── sessions (1:N) — app can have sessions from multiple users
└── user_app_access (1:N) — app can be accessed by multiple users
sessions
├── ident_events (1:N) — events can reference a session
└── token_blacklist (via token_jti = session.id)No
refresh_tokens table. The session is the refresh credential.Maintenance Jobs
Quota Period Reset
All credit-based quotas reset monthly (AI credits, hypermedia credits, special general credits). Storage is persistent (not periodic).
The reset is driven by
quota_reset_at per row, not by a fixed calendar date. This supports per-user billing cycles (e.g. reset on the user's signup anniversary) as well as the default monthly cycle.-- Run every hour: reset any quotas whose reset time has passed
UPDATE user_quota_usage
SET actual_value = 0,
overflow_value = 0,
period_start = NOW(),
quota_reset_at = DATE_TRUNC('month', NOW()) + INTERVAL '1 month', -- next 1st of month
updated_at = NOW()
WHERE quota_reset_at <= NOW()
AND quota_type IN ('ai_credits', 'hypermedia_credits', 'special_credits');When a user first signs up (or changes tier),
quota_reset_at is set to the 1st of the next month at 00:00 UTC. For signup-anniversary billing, it would be set to the same day next month.Connection Token Cleanup
-- Run every 15 min: remove expired connection tokens
DELETE FROM connection_tokens WHERE expires_at < NOW();Expired Session Cleanup
-- Run every hour
DELETE FROM sessions WHERE expires_at < NOW() AND revoked_at IS NOT NULL;
-- Keep revoked sessions for audit trail for 7 days
DELETE FROM sessions WHERE revoked_at < NOW() - INTERVAL '7 days';Blacklist Cleanup
-- Run every hour
DELETE FROM token_blacklist WHERE expires_at < NOW();Event Pruning
-- Run daily
DELETE FROM ident_events
WHERE created_at < NOW() - INTERVAL '90 days'
AND event_type NOT IN ('login_failed');Recovery Code Cleanup
-- Run weekly: remove used recovery codes older than 90 days
DELETE FROM recovery_codes
WHERE used_at IS NOT NULL AND used_at < NOW() - INTERVAL '90 days';Vacuum & Analyze
-- Run weekly
VACUUM ANALYZE users;
VACUUM ANALYZE sessions;
VACUUM ANALYZE token_blacklist;
VACUUM ANALYZE ident_events;
VACUUM ANALYZE webauthn_credentials;
VACUUM ANALYZE recovery_codes;Schema Evolution (Phase 2+)
When the userbase grows and you move to Redis for session lookups:
-- Add a Redis-compatible session key (optional)
ALTER TABLE sessions ADD COLUMN session_key VARCHAR(64);
-- Populate: session_key = SHA-256(session.id)
-- Redis stores: session_key → { user_id, app_id, scopes, expires_at }
-- Session row in Postgres remains as audit/source of truth
-- Redis is the fast path for refresh checksWhen you introduce refresh tokens (Phase 3):
-- Add refresh_tokens table
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
token_hash VARCHAR(64) UNIQUE NOT NULL,
user_id UUID NOT NULL REFERENCES users(id),
app_id INT NOT NULL REFERENCES registered_apps(id),
session_id UUID NOT NULL REFERENCES sessions(id),
family_id UUID NOT NULL,
scopes JSON NOT NULL,
issued_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL,
revoked_at TIMESTAMP,
replaced_by UUID REFERENCES refresh_tokens(id),
revoke_reason VARCHAR(50)
);
-- The session table and JWT logic stay the same
-- Only the refresh endpoint's internal logic changesSee also: Account Model for field-level documentation, Token Mechanics for how tokens relate to these tables.