Skip to content

Design: move SMS/RCS off the Google web session onto an Android companion (the durable pairing fix) #75

Description

@MaxGhenis

If we rebuilt OpenMessage from scratch: the pairing-fragility problem

A design note prompted by the Nth time Google Messages died silently. The
recurring "SMS stopped syncing N days ago" failure is not a bug — it is the
architecture working as designed. This note asks what architecture would not
have that failure mode, and what to actually do about it.

The root cause, stated plainly

OpenMessage aggregates four networks into one local inbox:

Network How we reach it Stability in practice
Signal signal-cli linked device rock-solid
WhatsApp whatsmeow companion device solid (occasional logout)
iMessage read chat.db on the Mac solid (local, read-only)
SMS/RCS libgm → messages.google.com web session fragile — dies every ~1–2 weeks

Three of four are fine. The one that keeps failing is SMS/RCS, and the reason is
specific: it is the only network where we impersonate a web session whose two
endpoints are both controlled by Google.
libgm reverse-engineers the "Messages
for web" client. Authentication is a bag of Google account cookies that rotate
about every 30 minutes
plus a linked-device registration the phone can revoke
at any time, silently, with no callback. When the laptop is asleep or travelling
during a rotation, the stored cookies expire, every token refresh returns
HTTP 401, and the session is dead until someone does cookie surgery or a full
re-pair. Google can also just change the web client and break libgm wholesale.

Contrast Signal and WhatsApp: those companion-device protocols are designed for
a persistent second endpoint, the libraries (signal-cli, whatsmeow) are
mature and self-heal their own sessions, and — critically — we control one end
(the linked device) and the protocol expects it to persist.
Google Messages
web expects a browser tab that reauthenticates constantly, and treats a
long-lived headless client as an anomaly to be evicted.

So "fix Android pairing" is really: how do you get SMS/RCS without impersonating
a browser session you don't own either end of?

The hard constraint: RCS is a Google monopoly on Android

Any redesign has to swallow this fact (confirmed, 2026):

  • RCS on Android is exclusively Google Messages. The RCS APIs are hidden
    behind an allowlist; only Google Messages (and a Samsung-only carve-out for
    wearables) can register with Google's Jibe backend. There is no third-party
    RCS API, rooted or not.
  • SMS/MMS is open. Any app that is the default SMS app on Android has full
    SMS/MMS access through the standard Telephony APIs.

Consequence: you cannot get RCS off an Android phone except through Google
Messages.
You can only choose which end of Google Messages you attach to:

  1. Its web session (today's libgm) — impersonate the browser. Fragile for the
    reasons above.
  2. The Google Messages app on the phone itself — read RCS from its
    notifications / RCS content provider, send via notification RemoteInput or an
    accessibility action. Ugly, but the phone's own RCS registration is
    legitimate and does not silently unlink, because you are not competing for
    a "linked device" slot — you are a local reader of an app that is already
    signed in.

That reframing is the whole design: move the SMS/RCS attachment point from
Google's web endpoint (which Google controls and evicts) to the phone endpoint
(which the user controls and Google blesses).

Four architectures

A. Status quo, done right — local reverse-engineered bridges

Keep libgm/whatsmeow/signal-cli running locally. Accept that sessions die; make
death cheap and self-healing.

  • Pros: zero extra devices, no phone app, everything on the Mac, works today.
  • Cons: SMS/RCS fragility is intrinsic and unremovable — you are always one
    Google web-client change away from a total break. You are renting a session
    Google can cancel.
  • What "done right" means (and what this session shipped): treat expired
    cookies as an expected event and auto-refresh them from the user's real Chrome
    profile in-process; retry token refresh on transient network errors (the
    upstream libgm fix we had been missing because our fork was frozen at March);
    and when self-heal genuinely can't recover, alert loudly instead of going
    silent for 15 days.

B. Android companion app — control the phone end (the KDE Connect / Phone Link model)

A small app on the user's own phone talks to the Mac over a LAN/mesh link
(Tailscale, or a direct socket). This is exactly how KDE Connect and
Microsoft Phone Link mirror SMS today — proven at scale.

  • SMS/MMS: the companion is (or cooperates with) the default SMS app and has
    full, official Telephony access. Rock-solid; no reverse engineering.
  • RCS: the companion reads inbound RCS from Google Messages' notifications /
    RCS provider and sends via RemoteInput. Lossier than a real API (notification
    text can be truncated; outbound is reply-shaped), but the registration is the
    phone's own and does not evict.
  • Transport: the phone is always on and with the user; a mesh link (Tailscale)
    makes it reachable off-LAN without exposing anything publicly.
  • Pros: kills the cookie/unlink fragility for SMS entirely; no Google web
    session anywhere; the failure mode becomes "phone offline," which is visible
    and self-correcting. This is the durable answer to the actual complaint.
  • Cons: you must build and keep alive an Android app; RCS via notifications is
    a compromise, not a clean API; iOS users are out of luck (but this is a
    Mac+Android tool).
  • Prior art that proves it: KDE Connect (open source, does SMS over LAN;
    RCS is a long-standing open request precisely because of the Jibe lock),
    Microsoft Phone Link (companion app mirrors SMS/notifications). The novel part
    here is only "unify with WhatsApp/Signal/iMessage locally and expose it over
    MCP" — which is OpenMessage's actual differentiator.

C. Hosted-Android device — a Pixel in a drawer

Run architecture B's companion on a dedicated always-on Android (a cheap Pixel on
a charger, or a cloud-hosted Android instance) that exposes an API the Mac
consumes.

  • Pros: most robust RCS endpoint possible — a real, always-online,
    Google-blessed Messages install; survives the user's own phone being off.
  • Cons: a device to own and keep charged/updated; cloud Android is costly and
    ToS-gray; more moving parts than most users want. This is the "power user /
    server" tier, not the default.

D. Matrix bridges — the industrial version of A

Run the full mautrix-gmessages / mautrix-whatsapp / mautrix-signal bridges
against a headless Matrix homeserver and consume via the Matrix client-server API.

  • Pros: mature bridge lifecycle management; one clean unified protocol
    (Matrix) instead of per-platform Go glue; you inherit upstream fixes directly.
  • Cons: heavy (homeserver + N bridges + a Matrix client); and it does not
    solve the fragility — the Google bridge is the same libgm underneath, so the
    same cookie/unlink death happens, just inside a bigger stack. Matrix is a better
    integration substrate, not a fix for the pairing problem.

Recommendation

Two tiers, matching effort to how much the fragility actually costs.

  1. Keep OpenMessage's shape — local-first, one inbox, MCP-native. That part
    is right and is the product's whole reason to exist. None of the failures are
    caused by the local-first or MCP choices. Do not rebuild toward a Matrix
    stack (Option D) chasing robustness it won't deliver.

  2. For SMS/RCS specifically, migrate the attachment point from Google's web
    session to the phone (Option B).
    The insight is that
    Google-Messages-for-web is a bridge where Google owns both ends and can evict
    you on their schedule
    — which is exactly what keeps happening. An Android
    companion app moves one end onto hardware the user controls and a registration
    Google blesses. SMS becomes as solid as Signal is today; RCS becomes
    "as good as the phone's notifications," which is strictly more durable than a
    rotating-cookie web session even if occasionally lossier.

    Concretely: a thin Android app (default SMS app for SMS/MMS; NotificationListener

    • RemoteInput bridging Google Messages for RCS) that speaks a small local
      protocol to the existing Go backend over Tailscale. The backend keeps
      whatsmeow/signal-cli/iMessage exactly as they are and swaps the libgm route for
      the companion route. libgm can stay as an optional no-extra-device fallback.
  3. Until B exists, Option A "done right" is the stopgap — and most of it now
    exists
    (in-process cookie self-heal, upstream libgm retry fix, loud
    health alerts). That converts the failure from "silently dead for two weeks,
    needs an expert to revive" into "auto-recovers, and if it can't, tells you in
    one notification." That is enough to live on while B is built.

One-line version

The SMS pain is intrinsic to renting a Google web session. Three of the four
networks already work because we hold a legitimate endpoint; the fix for the
fourth is to hold a legitimate endpoint there too — an app on the phone — not to
keep impersonating a browser Google keeps logging out.


Sources for the RCS/companion claims:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions