Skip to content
Get the kit

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");
// 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; on Firebase, an offscreen fallback)
signOutnonenonerevokes refresh tokens server-side, signs out, clears the stored user
emailSignIn{ email, password }AuthUser
emailSignUp{ email, password }AuthUser
emailPasswordReset{ email }nonesends the reset email: a link to Firebase’s reset page, or a code on Supabase
emailConfirmPasswordReset{ email, code, password }AuthUserSupabase only: checks the code, saves the new password, signs the user in

Billing (UI → background) – billing module

Section titled “Billing (UI → background) – billing module”
messagepayloadreturnsnotes
billingCheckout{ lookupKey }nonethe background does the API call and opens the provider’s checkout tab, so the flow survives the popup closing
billingPortalnonenoneopens the customer portal the same way
billingPricesnonenonerefreshes storage.local.prices from the public GET /billing/prices when the cache is older than an hour; the plan picker sends it on open and renders the cache meanwhile

Paywall (UI/content → background) – billing module

Section titled “Paywall (UI/content → background) – billing module”
messagepayloadreturnsnotes
access{ feature }Verdictthe one paywall call: checks the feature against paywall.config.ts, counts free uses, consumes credits for credit features (server-side, before the work), publishes the wall when denied or with the use that spends an allowance or the last credits. allowed: false = stop; reason is pro / limit / credits / signin / offline; remaining and resetsAt for limits. Feature ids are typed from the config
paywallOpennonenoneraise the upgrade prompt explicitly (manual placement)
paywallDismissnonenone“Maybe later” on the active wall (on the prompt or the trial-ended wall, starts the prompt cooldown)
paywallResetnonenonedev tools: wipe local counters, dismissals, and the active wall
tabIdnone{ tabId }content → background: the tab the script runs in, so a page renders only the walls raised there
messagepayloadreturnsnotes
offscreenGetAuthnoneOffscreenAuthResultbackground → offscreen document only; Firebase stack
  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>:start … end) 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, access, paywall, broadcasts) via hooks (useAuth, useEntitlement, useFeature, usePlan, usePaywall). 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.