A shared inbox platform built inside Gmail for about 70 users across seven departments, owned from discovery interviews through production rollout.
- Python
- TypeScript
- Cloud Run
- Firestore
- Manifest V3
- OAuth
- KMS
- Pub/Sub
The brief was to make team email feel native to Gmail instead of moving people to another tool. That decision drove everything: a Chrome extension as a thin client over shared state, with a backend on Cloud Run and Firestore owning assignments, status, notes, SLA timers, and the audit log. Gmail push sync over Pub/Sub keeps it live.
Roughly 1,000 commits over six months. The backend went through four generations before it settled: Apps Script and Sheets, then n8n, then TypeScript on Cloud Run, then Python in the company monorepo. Each step was forced by a measured ceiling, not taste.
Decisions that mattered
- Per-inbox OAuth instead of a service account with domain-wide delegation. Least privilege: every shared mailbox grants explicit, revocable consent with narrow scopes. The cost is that operators connect each inbox by hand at kickoff, and that cost was accepted on purpose.
- Labels-only routing with a fan-out insert. A shared message is fetched raw and inserted byte-identical into each member’s own mailbox, then Gmail labels carry the queue state. No forwarded copies, no header rewriting, native Gmail search keeps working. The shared view is Firestore, not Gmail threading.
- Gmail thread ids turned out to be per-account, which broke the first right panel. The answer was a four-strategy thread resolver with the RFC 2822 Message-ID as the cross-account key, bounded by a seven-day subject-hash window.
- The TypeScript to Python rewrite was an organizational decision made technical: the company’s infra and on-call muscle is Python, and the TS code could not clear the reviewer’s conformance bar without a retrofit that cost as much as a port. The extension stayed byte-identical except for the API client; KMS envelopes stayed byte-compatible so tokens carried over without re-consent; every TS test got a pytest equivalent or a written exemption.
- KMS envelope encryption, not direct KMS calls, for the two token collections. Fresh DEK and IV per write, AES-256-GCM with the collection and document id as associated data, so a ciphertext moved to another document fails to decrypt. Rotation works at the key-version level without re-encrypting payloads.
- Authorization is per-inbox role checks in per-route decorators, never a global before-request hook. An AST test fails the build if any handler taking an inbox id forgets to call a role helper, and a 31-route by 5-role matrix pins the boundary.
- Three-tier rate limiting (user, inbox, global) because a per-user tier alone lets one member burn shared Firestore reads for everyone. The global tier is the circuit breaker for Cloud Run and the Gmail quota.
Hardest production bug: a single-instance saturation compound. A label sync for one nine-member inbox ran 85 seconds on the request path, because it rebuilt a Gmail client per thread per member, about 5,600 Firestore round trips and 1,800 KMS decrypts for 200 threads. Fixed by batching per member and moving the sweep behind an atomic claim, roughly 9 KMS calls and 50 Gmail calls instead of 1,800. The lesson that stuck: the saturation was self-inflicted load, not capacity.