beeper-sdk
v0.2.0
Published
Server-side TypeScript SDK for Beeper notification APIs.
Readme
beeper-sdk
Server-side TypeScript SDK for Beeper notification APIs.
Install
pnpm add beeper-sdkUsage
import { BeeperClient } from "beeper-sdk";
const beeper = new BeeperClient({
projectId: process.env.BEEPER_PROJECT_ID!,
apiKey: process.env.BEEPER_SERVER_KEY!,
});baseUrl is optional and defaults to the hosted Beeper API — you do not need to know or
configure the API origin. Pass it only when self-hosting, or to target a staging or local
deployment:
const beeper = new BeeperClient({
baseUrl: process.env.BEEPER_BASE_URL, // optional override
projectId: process.env.BEEPER_PROJECT_ID!,
apiKey: process.env.BEEPER_SERVER_KEY!,
});
await beeper.upsertSubscriber({
subscriberId: "user_123",
displayName: "John Doe",
email: "[email protected]",
});
await beeper.triggerNotification({
channels: ["in_app", "email"],
subscriberIds: ["user_123"],
templateKey: "welcome",
category: "transactional",
actionLabel: "Open dashboard",
actionUrl: "https://app.example.com/dashboard",
});
await beeper.registerPushDevice({
subscriberId: "user_123",
pushToken: "<fcm_token>",
platform: "web",
});
await beeper.triggerNotification({
channels: ["push"],
pushTokens: ["<fcm_token_1>", "<fcm_token_2>"],
templateKey: "promo-push",
category: "transactional",
actionLabel: "View offer",
actionUrl: "https://example.com/offers/spring",
});actionLabel and actionUrl are stored in notification metadata and passed through to
in-app inbox items. The inbox widget renders them as an action button/link when present.
Push images and custom sounds
push controls how the operating system presents a push. It requires "push" in channels
and does not affect the in-app inbox:
await beeper.triggerNotification({
channels: ["push"],
subscriberIds: ["user_123"],
templateKey: "order-shipped",
push: {
imageUrl: "https://cdn.example.com/orders/123.jpg", // https only, max 1024 chars
sound: "chime", // bundled in the app: res/raw/chime.* and chime.caf
androidChannelId: "orders", // Android 8+ takes the sound from this channel
},
});Images on iOS need a Notification Service Extension in the app, custom sounds must be bundled in the app, and browsers support neither custom sounds nor (outside Chromium) images. Invalid values reject the trigger with a message naming the field. Full setup: Push images and custom sounds.
Widget session route handler
POST /widget/session authenticates your server key, not your end user — it mints a session
for whatever subscriberId it is handed. Your route is the only thing that decides who the
caller is, so the id must come from your own auth session and never from the request body.
widgetSessionHandler enforces that by construction:
// app/api/beeper/widget-session/route.ts — Next.js App Router
export const POST = beeper.widgetSessionHandler({
authenticate: async (request) => {
const session = await auth.api.getSession({ headers: request.headers });
return session ? { subscriberId: session.user.id } : null; // null → 401
},
});The handler never parses the request body, so there is no parameter through which a
caller-supplied subscriberId could enter. It takes and returns web-standard
Request/Response — the same export mounts in Next.js route handlers, Remix/React Router
actions, Hono, Cloudflare Workers, Bun and Deno.
// Remix / React Router
export const action = ({ request }: ActionFunctionArgs) => handler(request);
// Hono / Workers — build the client inside the handler, off the env binding
app.post("/api/beeper/widget-session", (c) =>
new BeeperClient({
projectId: c.env.BEEPER_PROJECT_ID,
apiKey: c.env.BEEPER_SERVER_KEY,
}).widgetSessionHandler({
authenticate: async (request) => {
const session = await auth.api.getSession({ headers: request.headers });
return session ? { subscriberId: session.user.id } : null;
},
})(c.req.raw),
);Options: ttlSeconds (clamped to 60-3600 by the API, default 900) and onError, which
receives the thrown error for your logger — the browser only ever sees a generic 500, never the
upstream message or your server key. Return profile from authenticate to sync the
subscriber's displayName/email/metadata at mint; that requires the subscribers:write
scope on the key.
Worth testing once per app: signed in as user A, curl your route with
{"subscriberId":"user_b"} — you must get back a token for A.
Widget session helpers
const session = await beeper.createWidgetSession({
subscriberId: "user_123",
ttlSeconds: 900,
});
await beeper.revokeWidgetSessions({ subscriberId: "user_123" });Push device helpers
await beeper.registerPushDevice({
subscriberId: "user_123",
pushToken: "<fcm_token>",
platform: "web",
});
await beeper.unregisterPushDevice({
subscriberId: "user_123",
pushToken: "<fcm_token>",
});
const pushStats = await beeper.getPushDeviceStats();Security
- Use this SDK only on trusted backend services.
- Never expose
BEEPER_SERVER_KEYin browser/mobile client bundles. - Rotate API keys regularly and revoke keys on suspicious activity.
Client-Safe Widget API
For browser and React Native clients, use the separate widget entry point with a publishable key and short-lived session token:
import { BeeperWidgetClient } from "beeper-sdk/widget";
const widget = new BeeperWidgetClient({
projectId,
publishableKey,
sessionToken,
});
const inbox = await widget.getInbox(userId);
await widget.markRead(userId, [inbox.items[0]._id]);The widget client does not accept a server API key. Mint and refresh the session token from your own authenticated backend.
Errors
Failed requests throw BeeperApiError.
import { BeeperApiError } from "beeper-sdk";
try {
await beeper.getUsage();
} catch (error) {
if (error instanceof BeeperApiError) {
console.error(error.status, error.message, error.payload);
}
}Release checklist
pnpm -F beeper-sdk run clean
pnpm -F beeper-sdk run build
pnpm -F beeper-sdk run check-types