Sync Architecture
Sync Architecture Bounded startup sync with predictive cache warming — recent chats appear quickly after login without bulk-syncing the full last-100 chat co...
Sync Architecture
Bounded startup sync with predictive cache warming — recent chats appear quickly after login without bulk-syncing the full last-100 chat content set.
Summary
- Startup sync sends a small shell first, then recent parent-chat content, then metadata-only updates.
- Partially warmed cache entries must be completed from Directus before Phase 1a reaches the client.
- Full startup content is capped to recent parent chats; sub-chat content stays on demand.
Why This Exists
- Users expect instant chat access after login — even 2-second delay feels broken
- Zero-knowledge means server can’t pre-decrypt for fast access
- Multiple devices need sync while maintaining client-side encryption
- Different data has different priorities: startup shell > recent 10 full parent chats > recent 100 metadata > on-demand content
How It Works
- User enters email →
/lookuptriggers predictive cache warming (async, non-blocking) - User types password + 2FA (~10-30s) — warming runs concurrently
- Login succeeds → cache already warm → instant WebSocket startup sync
- All data stays encrypted during transit — decryption only in client memory
sequenceDiagram
participant C as Client
participant S as Server
participant R as Redis
participant D as Directus
C->>S: POST /lookup (hashed_email)
activate S
S-->>C: salt, wrapped_master_key
S--)R: Start cache warming (async)
deactivate S
Note over C: User types password + 2FA (~10-30s)
par Cache Warming (concurrent with auth)
R->>D: Query recent parent chats, metadata, messages, embeds
D-->>R: Encrypted chat data cached
end
C->>S: POST /login (lookup_hash + OTP)
S-->>C: Session cookie
C->>S: WebSocket connect
S->>R: Read warm cache
S-->>C: Startup shell — suggestions + recent metadata
S-->>C: Recent full — last 10 parent chats content
S-->>C: Recent metadata — last 100 chats metadata only
S-->>C: App memories
Data Flow (all phases)
Directus (encrypted) → Redis cache → WebSocket (encrypted) → IndexedDB (encrypted) → Memory (decrypted) → UI
Startup Contract
Startup/login sync has a hard payload boundary:
- The 10 most recent parent chats are fully synced.
- The 100 most recent chats are metadata-only beyond that full-content budget.
- Sub-chat messages and sub-chat embeds are always on-demand.
- Parent full-content sync may include embeds referenced by parent chat messages.
- Web startup never runs the 100-chat background message/embed sync path.
Full content means encrypted messages, referenced embeds, embed keys, Code Run output sidecars, and compression checkpoints.
Startup Shell: Metadata + Suggestions
- WebSocket event:
phase_1_last_chat_ready - Sends last-opened metadata, recent parent chat metadata, direct sub-chat preview metadata, new chat suggestions, and daily inspirations.
- Sends no messages, embeds, embed keys, Code Run outputs, or compression checkpoints.
- Only startup event that can trigger auto-navigation — later events update cached data/sidebar only.
Startup Metadata Invariant
Startup shell sync may read Redis while the cache is only partially warm. The backend must treat a Redis list item without matching versions, encrypted chat key, or titled-chat encrypted title as incomplete and fill missing fields from Directus before sending phase_1_last_chat_ready.
Required invariant for every Phase 1a chat payload:
encrypted_chat_keyis present for private encrypted chats.encrypted_iconandencrypted_categoryare present when available in Directus/cache.- If
title_v > 0,encrypted_titleis present. - Partial cache fields may be filled from Directus, but cached versions stay authoritative unless the versions key is missing.
Client-side Phase 1a handling must use the shared chat metadata merge policy (mergeServerChatWithLocal) rather than writing server payloads directly over IndexedDB. This prevents null/incomplete sync payloads from erasing locally valid encrypted metadata and keeps key-mismatch handling consistent with Phase 2/3.
Recent Full Content: Last 10 Parent Chats
- WebSocket event:
phase_1b_chat_content_ready - Sends full content for at most
STARTUP_FULL_PARENT_CHAT_LIMIT = 10parent chats. - Parent chat selection excludes
is_sub_chat=trueand rows withparent_id. - Sub-chat content is not included in this payload.
Recent Metadata: Last 100 Chats
- WebSocket event:
phase_2_last_20_chats_readyuntil the event name is renamed. - Sends metadata only for the recent 100-chat window.
- Does not send messages, embeds, embed keys, Code Run outputs, or compression checkpoints.
- Triggers metadata-only expansion for chats 101-1000 when needed.
On-Demand Hydration
- Request:
request_chat_content_batch - Response:
chat_content_batch_response - Used when opening a metadata-only parent chat or any sub-chat.
- Returns encrypted messages, embeds, embed keys, Code Run outputs, compression checkpoints, and version data for requested chats only.
App Settings And Memories
- Automatic after startup metadata sync
- Conflict resolution: higher
item_versionwins, thenupdated_at - Multi-device: all devices decrypt independently with master key
Native Offline Prefetch
Native/desktop offline sync for parent chats 11-100 is a separate capability-gated path, not part of web startup.
- REST endpoint:
POST /v1/sync/offline-prefetch - Request:
cursor,limit(max 5), andinclude_embeds. - Response: encrypted parent-chat metadata, encrypted message strings, referenced embeds, embed keys, Code Run output sidecars, compression checkpoints, and
next_cursor. - Cursor starts at offset 10 so startup-owned parent chats are not fetched again.
- The endpoint filters out sub-chat content; sub-chats remain on-demand only.
- Apple/native clients run it opportunistically after startup sync and on network restore only when network is non-expensive/non-constrained, Low Power Mode is off, thermal state is acceptable, and local storage remains under budget.
- Prefetched content is persisted to SwiftData through
OfflineStore; it is not pushed into the visible in-memory transcript unless the chat is already part of normal startup/opened-chat state.
User Choice Protection
Sync never overrides explicit user choices:
| Flag in phasedSyncStateStore.ts | Purpose |
|---|---|
initialChatLoaded |
Set when first chat loads → blocks future auto-navigation |
userMadeExplicitChoice |
Set on click chat / “new chat” → sync never overrides |
currentActiveChatId |
NEW_CHAT_SENTINEL for explicit new-chat mode (not null) |
canAutoNavigate()returns false if either flag set- Startup shell chat loading handled in
+page.svelte; later startup events update IndexedDB/sidebar only
Predictive Cache Warming
- Triggered at
/lookup(email entry), NOT at/login - Before: 2-5s wait after login. After: instant sync
- Deduplication: checks
cache_primed+warming_in_progressflags - Fallback:
/loginalso triggers warming if/lookupskipped - Security: all cached data encrypted, rate-limited (3/min), no premature transmission
- Implementation: auth_login.py (trigger), user_cache_tasks.py (warming task)
Edge Cases
- User switches chat during Phase 1:
userMadeExplicitChoiceprevents auto-navigation — handled byshouldAutoSelectPhase1Chat()in phasedSyncStateStore.ts - Duplicate cache warming: dedup via Redis flags
cache_warming_in_progress:{user_id}(5-min TTL) in auth_login.py - Multiple browser instances: unique sessionID per tab (UUID in sessionStorage) → device hash
SHA256(OS:Country:UserID:SessionID)→ separate WebSocket connections. See device-sessions.md - Storage overflow: startup full content is capped to 10 parent chats; older chat content hydrates on demand
- Embed eviction:
chat:{chat_id}:embed_idsRedis set tracks ownership → embeds evicted only when no active chat references them. In cache_chat_mixin.py
Improvement Opportunities
Improvement opportunity: Full-text search across synced metadata — client-side only for zero-knowledge Improvement opportunity: Pinned chat support — ensure pinned chats always sync in Phase 1 regardless of last-edited order Improvement opportunity: Smart eviction — evict by access frequency, not just age
Related Docs
- Security — encryption tiers and zero-knowledge
- Message Processing — dual-cache (AI vs. sync)
- Embeds — embed sync alongside messages
- Device Sessions — device fingerprinting, multi-device
- Memories — post-startup sync target