Data Synchronization
The Synchronization Engine is responsible for continuously fetching data and updating the local database to stay synchronized with the backend, without missing a single event or encountering race conditions.
Server-Side Architecture (CQRS)
The Novyse backend employs a CQRS (Command Query Responsibility Segregation) pattern to manage data efficiently across distributed clients. This division creates two distinct layers:
- Materialized State (Snapshot): Primary tables (messages, chats, users) representing the current "truth".
- Event Pipeline (Mutations): Chronological logs used to synchronize offline clients without state conflicts.
Why we don't use Timestamps
Timestamps are vulnerable to clock skew and race conditions during high-volume periods. Novyse utilizes a deterministic Sequential Cursor Pattern based on auto-incrementing IDs.
The client simply requests "everything that happened after ID X", guaranteeing mathematical consistency: no skipped rows and no duplicated events, even through abrupt disconnects.
Synchronization Lifecycles
1. Initialization
When the app starts with an empty database, it performs a full bootstrap.
| Step | Action | Description |
|---|---|---|
| 1 | Request | Client calls GET /user/initialize. |
| 2 | Snapshot | Server provides the full Materialized State (latest messages, active chats). |
| 3 | Baseline | Server returns the Maximum Current IDs for all entities to set local cursors. |
2. Update
When the app wakes up or regains connectivity, it fetches only the delta. The server performs a membership diff to determine what the client has versus what it should have.
| Step | Action | Description |
|---|---|---|
| 1 | Delta Request | Client sends its local chat and user lists (with cursors) via POST /user/update. |
| 2 | Membership Diff | Server queries the real memberships and compares them to what the client sent. Missing chats/users are flagged as new. |
| 3 | New Entities | For each new chat/user, the server returns the full object (same structure as initialization) including all messages for new chats. |
| 4 | Existing Events | For chats/users the client already has, the server returns only the events that occurred after the cursor sent by the client. |
| 5 | Pipeline | Data is applied in order: New Chats → New Users → Messages → Chat Events → User Profile Events → Local User Events. |
Response Structure
The update response splits chats and users into two sub-objects, while messages remains a flat array:
Key Content chats.new Full chat objects (members, pinned messages, eventID) for chats the client lacks chats.events Incremental events for chats the client already has users.new Full user profiles for users the client lacks users.events Incremental profile events for users the client already has messages Flat array — all new messages from both new and existing chats local Private user events (theme, muted chats, folders)
Network Transport: Ping & Fetch
| Layer | State | Behavior |
|---|---|---|
| Socket.IO | Foreground | Real-time updates. Payloads are processed instantly and trigger UI re-renders via Zustand. |
| Firebase FCM | Background | "Silent Alarm". FCM wakes the app to execute a background Update Cycle before the user opens the notification. |
The Event Pipeline
While the local database stores the Materialized State (the current snapshot), the server handles changes via an Event Pipeline. Instead of directly mutating the state on the server, we log alterations in event tables.
Crucially, these event logs never contain the creation of an entity (e.g., sending a new message), but only track its subsequent modifications over time to ensure eventual consistency across all devices.
Event Payload Structure
Every event delivered through the pipeline includes a payload object. This object contains a current field, which holds the exact value of the modification performed (e.g., the new message content, the new avatar UUID, or the updated theme preference). This ensures the client can apply the update directly without additional lookups.
1. Chat Events
These events manage the evolution of a conversation. They are synchronized only with users who are currently active members of the chat.
| Event | Payload | Visibility |
|---|---|---|
| MESSAGE_EDITED | | Chat Members Only |
| REACTION_ADDED | | Chat Members Only |
| REACTION_REMOVED | | Chat Members Only |
| MESSAGE_DELETED | | Chat Members Only |
| MESSAGE_PINNED | | Chat Members Only |
| MESSAGE_UNPINNED | | Chat Members Only |
| MEMBER_JOINED | | Chat Members Only |
| MEMBER_LEFT | | Chat Members Only |
Optimized Message Mutations
To minimize network overhead and protect privacy, Novyse applies specific visibility rules for message history:
- External Users: Only receive the most recent edit event for any given message, seeing only the "final" state since their last check.
- Authors & Admins: Retain access to the Full Sequence of Changes, allowing them to audit the complete history of edits for that specific message.
2. User Profile Events
These events track changes to public-facing user data. They power the "Address Book" synchronization, ensuring contacts always see the latest profile data.
| Event | Payload | Visibility |
|---|---|---|
| BIO_CHANGED | | Public |
| PICTURE_CHANGED | | Public |
| BANNER_CHANGED | | Public |
| NAME_CHANGED | | Public |
| SURNAME_CHANGED | | Public |
| BIRTHDAY_CHANGED | | Public |
| COLOR_CHANGED | | Public |
| HANDLE_CHANGED | | Public |
History Privacy & Visibility
To protect user privacy, Novyse applies a differential history policy for profile events:
- External Users: When a contact fetches updates, they only receive the most recent event of each type. They cannot see the history of previous avatars or bios.
- Account Owner: The owner of the account retains access to the Full Event Log across all their devices, ensuring their personal history and audit trail are preserved.
3. User Events
These events synchronize a user's private configuration and interaction metadata across all their personal devices.
| Event | Payload | Visibility |
|---|---|---|
| CHAT_PINNED | | Owner Only |
| CHAT_UNPINNED | | Owner Only |
By combining ordered execution with cursor-based fetching, Novyse ensures that the local SQLite database is a perfect, eventually-consistent mirror of the server.