Skip to content

Messaging protocol

Every runtime message in the extension is declared once, in apps/extension/utils/messaging.ts, as the ExtensionProtocol interface: key = message type, parameter = payload, return type = response. Both ends are typed end to end; never use raw runtime.sendMessage.

import { sendMessage, onMessage } from "@/utils/messaging";
// caller (any surface):
const user = await sendMessage("signIn", undefined);
// handler (background, top level):
onMessage("signIn", async () => { /* … */ });

The bus ignores foreign messages by envelope marker; handlers validate their own payloads.

message payload returns notes
signIn none AuthUser interactive Google sign-in (web-auth-flow, offscreen fallback)
signOut none none signs out of Firebase, clears the stored user, revokes refresh tokens server-side
emailSignIn { email, password } AuthUser
emailSignUp { email, password } AuthUser
emailPasswordReset { email } none sends the reset email

Billing (UI → background) – billing module

Section titled “Billing (UI → background) – billing module”
message payload returns notes
billingCheckout { lookupKey } { url } the background does the API call and opens the Stripe tab, so the flow survives the popup closing
billingPortal none { url } customer-portal session
creditsConsume { feature, amount? } { ok, balance, reason? } metered features: server-side transactional decrement, balance mirrored to storage. ok: false = stop the feature; see the credits model

Gates (UI/content → background) – gate module

Section titled “Gates (UI/content → background) – gate module”
message payload returns notes
gateFeature { feature } GateDecision | null null = proceed; a decision means the wall is up (already published to every surface; just stop the action)
gateAction { name? } GateDecision | null counts usage toward action thresholds
gateOpen { gateId } GateDecision | null manually raise a wall ("signin" / "paywall")
gateDismiss { gateId } none dismiss the active wall (starts its cooldown)
gateReset none none dev tools: wipe local gate counters/dismissals/active wall
message payload returns notes
log LogEntry none any context → background: append to the support-log ring buffer
offscreenGetAuth none offscreen auth payload background → offscreen document only
  1. Add the method signature to ExtensionProtocol in apps/extension/utils/messaging.ts.
  2. Register the handler in the owning background module, at the top level of the file (see background patterns).
  3. Call it with sendMessage from any surface. The compiler enforces payload and response types on both ends.

If the message belongs to a prunable module, wrap the protocol lines in that module’s wiring markers (// module:<id>:startend) so pruning keeps the file compiling; see the module system.

  • State reads. Surfaces don’t ask the background for state; they read the storage.local snapshots (user, entitlements, gateDecision, broadcasts) via hooks (useAuth, useEntitlement, useGateDecision). Messages are for actions.
  • Backend calls with tokens. UI and content scripts never hold ID tokens; they send a message, and the background attaches the token to the API call.