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.

messagepayloadreturnsnotes
signInnoneAuthUserinteractive Google sign-in (web-auth-flow, offscreen fallback)
signOutnonenonesigns out of Firebase, clears the stored user, revokes refresh tokens server-side
emailSignIn{ email, password }AuthUser
emailSignUp{ email, password }AuthUser
emailPasswordReset{ email }nonesends the reset email

Billing (UI → background) – billing module

Section titled “Billing (UI → background) – billing module”
messagepayloadreturnsnotes
billingCheckout{ lookupKey }{ url }the background does the API call and opens the Stripe tab, so the flow survives the popup closing
billingPortalnone{ 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”
messagepayloadreturnsnotes
gateFeature{ feature }GateDecision | nullnull = proceed; a decision means the wall is up (already published to every surface; just stop the action)
gateAction{ name? }GateDecision | nullcounts usage toward action thresholds
gateOpen{ gateId }GateDecision | nullmanually raise a wall ("signin" / "paywall")
gateDismiss{ gateId }nonedismiss the active wall (starts its cooldown)
gateResetnonenonedev tools: wipe local gate counters/dismissals/active wall
messagepayloadreturnsnotes
logLogEntrynoneany context → background: append to the support-log ring buffer
offscreenGetAuthnoneoffscreen auth payloadbackground → 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.