Inside Banger’s Proprietary Sync Engine
Sync is where email products become real.
The UI can be beautiful. The backend can be well designed. But if the inbox feels stale, slow, or inconsistent, users do not trust it. Email is operational. People expect the thread they archived to disappear now, the unread count to update now, the new customer reply to appear now, and the app to keep working while background work catches up.
Banger’s sync engine exists to make that possible.
It is not just a downloader. It is a local projection system that turns encrypted mailbox streams, provider ingest, backfill data, pending actions, labels, read state, and workflow metadata into the thread list the user actually sees.
The local database is the working set
Banger keeps mailbox state locally. That is essential for speed.
The local database stores the entities the UI needs to render and operate:
- threads
- emails
- labels
- thread labels
- email read state
- pending actions
- sync cursors
- body availability
- workflow metadata
- local app state
That local database is not a cache in the casual sense. It is the working set for the product. The UI reads from it. Sync updates it. Pending actions project over it. Background jobs use it to decide what to do next.
The backend remains the source of shared mailbox history, but the local projection is what makes the app feel immediate.
Streams, not snapshots
Banger mailbox state moves through ordered streams.
There are separate concepts for:
- normal actions
- provider ingest
- history backfill
Those streams have different semantics. New Gmail mail arriving through watch and Pub/Sub is not the same as a client-driven history import. A local user archive action is not the same as an encrypted inbound message item waiting to be processed.
Keeping these paths separate gives the sync engine clearer recovery behavior. It can track different cursors, resume different kinds of work, and avoid treating all mailbox change as one undifferentiated blob.
That matters when a mailbox is large, a network is unreliable, or a backfill has to pause and resume.
Pending actions make the UI immediate
Users should not wait for a round trip before the inbox responds.
When the user archives a thread, changes labels, marks mail read, or triggers a workflow transition, Banger records a pending local action. The UI projection then applies that pending action on top of persisted state.
The effective row the user sees is:
persisted mailbox state
+ ordered pending action overlay
= effective UI state
This is the core idea behind the sync engine. The local database may not yet contain the acknowledged remote result, but the UI can still behave deterministically.
Pending actions are not random optimistic hacks. They are ordered, stored, retried, pushed, acknowledged, and removed through the sync pipeline.
Thread labels are canonical for mailbox views
Email labels and thread labels are easy to mix up.
Banger separates them because team inbox views operate at the thread level. A thread belongs in Inbox, Sent, Spam, Trash, or a custom label view based on effective thread labels. Read and unread state remains email-level, because one thread can contain multiple emails with different read states.
That separation gives us cleaner rules:
- Inbox is a thread label view, not a duplicated boolean.
- Sent is a thread label view.
- Spam and Trash are thread label views.
- Custom label filters read from effective thread labels.
- Unread indicators come from email-level state.
The projection starts with persisted thread_labels and then applies pending label actions. That keeps the UI consistent even before the server has acknowledged a change.
Counts and visibility are projected too
Thread lists are more than rows. They have counts, unread indicators, visibility rules, and sorting.
The sync engine computes effective rows by combining:
- persisted thread aggregates
- persisted thread labels
- pending thread actions
- pending email actions
- unread maps
- draft state
- workflow filters
A row can disappear from the current view because its effective labels no longer match the active filter. A count can change because a pending delete or add affects the thread. An unread indicator can update before a remote acknowledgment arrives.
The important part is that all of this happens through deterministic projection rules. The app should not have five different answers for whether a thread is visible.
Backfill is not normal ingest
Gmail history import is a different kind of sync work.
Backfill can involve large volumes of historical messages. It may need to pause, resume, respect plan limits, and coordinate so devices do not duplicate work. Banger treats backfill as its own stream with its own cursor state.
That separation prevents old history import from blocking or confusing the normal new-mail path.
New mail should keep moving. Backfill can progress behind it.
Large mailboxes need windows
One of the easiest ways to make an email app slow is to load too much.
Banger’s thread list uses a windowed query model. Instead of holding an unbounded list of rows, it queries a raw window, loads supporting data, applies pending overlays and filters, then renders the visible subset.
The runtime tracks:
- rendered row limit
- raw query window size
- raw query offset
- whether a window query is in flight
- scroll compensation when shifting older or newer
This lets the app browse large mailboxes without growing memory forever.
Sparse filters are a real problem
Filtering after raw pagination creates a practical issue: the current raw window might contain only a few rows matching the selected filter.
For example, a custom label view might be sparse inside the broader mailbox. If the app only expands rendered rows, the user can get stuck with an underfilled list even though more matching rows exist deeper in history.
Banger handles this by expanding the query window and eventually shifting the raw offset. The windowing logic adapts when rows are sparse so the app can keep moving through the mailbox.
This is one of those engineering details users should never notice. If they do notice it, the app feels broken.
Roll-to-new keeps browsing stable
When a user is reading older mail, new mail can arrive at the head of the list.
Automatically jumping the user back to the top is disruptive. Ignoring the new mail is also wrong. Banger tracks head changes and can show a roll-to-new affordance when the user is browsing an older window.
That behavior comes from treating list position as part of the runtime state, not just a visual side effect.
AI categorization plugs into sync as actions
AI categorization is most useful when it writes into the same action model as everything else.
The categorization flow can inspect thread context, decide on a label or triage state, and enqueue actions such as label updates or archive operations. Those actions then move through the same pending-action projection and push pipeline.
That means AI behavior does not become a parallel universe. It uses the mailbox system the rest of the app uses.
This is also the path future local models like BM1 should take: classify locally, then write structured results back through the sync engine.
Sync is product infrastructure
The sync engine is not glamorous, but it is one of the most important parts of Banger.
It is what makes the app feel fast. It is what lets Gmail, custom-domain mail, local actions, workflow state, and AI categorization converge into one inbox. It is what lets a team trust the state they see.
Email is old, but the expectations around it are modern: instant UI, local responsiveness, large data sets, AI assistance, shared ownership, and reliable recovery.
That is why Banger’s sync engine is proprietary infrastructure. It is not plumbing under the product. It is part of the product.