@vibemonetize/server

Server-side SDK for VibeMonetize. Verifies entitlement JWTs locally via JWKS, meters usage fire-and-forget, and reads the current user off a verified token. Runs on Node and edge runtimes — web-standard fetch/crypto.subtle (via jose) only, no node: imports.

All checks in this package fail closed: any invalid, expired, wrong-audience, or under-entitled token throws a typed VibeMonetizeError rather than silently allowing access. (The companion @vibemonetize/react package intentionally fails open in the UI when the API is unreachable — the enforcement of record always happens here.)

Install

pnpm add @vibemonetize/server

Quick start

import { createClient } from "@vibemonetize/server";

const monetize = createClient({ appId: process.env.VIBEMONETIZE_APP_ID! });

// Next.js Route Handler / any Node or edge server framework:
export async function POST(req: Request) {
  const token = req.headers.get("x-vibemonetize-token") ?? "";

  // Throws payment_required/forbidden/token_expired/unauthorized — let it bubble
  // to your error handler, or catch VibeMonetizeError and map `.code`/`.status`.
  const claims = await monetize.requireEntitlement(token, "export");

  // ... handle the request using `claims` ...

  void monetize.meterUsage({ meterSlug: "exports", endUserId: claims.sub });
  return new Response("ok");
}

No backend? Use the edge-function recipe

If your app is a pure client-side Vite build (the vibe-coding default), there is no server for this package to run on — and without it, every client-side gate is advisory. The fix is one copy-paste edge function (Vercel/Netlify) in front of your one expensive call: it reads x-vibemonetize-token, calls requireEntitlement/verifyToken here, returns 402 { code, message } on fail, and fires meterUsage server-side. Full recipe: docs.vibemonetize.dev/guides/edge-enforcement (raw-markdown agent version at /guides/edge-enforcement.md). The tested reference implementation is the demo app's guarded action: apps/demo/api/answer.tsapps/demo/server/answerHandler.ts.

Public API

createClient(options?)

createClient({
  apiUrl?: string;        // default: VIBEMONETIZE_API_URL env var, else "https://api.vibemonetize.dev"
  appId?: string;         // default: VIBEMONETIZE_APP_ID env var (required one way or another)
  secretKey?: string;     // default: VIBEMONETIZE_SECRET_KEY, falling back to VIBEMONETIZE_TEST_SECRET_KEY; sent as `Authorization: Bearer`
  issuer?: string;        // JWT `iss` to require; defaults to `apiUrl`
  verificationKey?: EntitlementSigningKey | JWTVerifyGetKey; // override JWKS fetch — mainly for tests
  expectMode?: Mode | "any"; // RFC 022 — enforce the token's mode claim; default derived from secretKey's prefix
  meterUsageMaxRetries?: number;     // default 3
  meterUsageRetryDelayMs?: number;   // default 200 (multiplied by attempt number)
}): VibeMonetizeServerClient

Returns a client with:

Also re-exports VibeMonetizeError and the EntitlementTokenClaims/ErrorCode/Mode/ UsageEventResponse/GrantCreditsResponse types from @vibemonetize/core so you don't need a direct dependency on it just to catch errors or type a claims object.

Developer test/live split (RFC 012)

Every secret key is either sk_test_... or sk_live_... (Stripe-style), and every entitlement JWT carries a mode: "test" | "live" claim stamped from the key used to mint it. createClient never parses or branches on the key's prefix — just pass whichever key you have (secretKey option, VIBEMONETIZE_SECRET_KEY env var, or its VIBEMONETIZE_TEST_SECRET_KEY fallback — see below) and use getUser(token).mode/ getMode(token) to tell which mode a given request ran in:

const user = await monetize.getUser(token);
if (user.mode === "test") {
  // e.g. skip an external side effect you only want to fire for real (live) usage
}

Tokens issued before this claim existed (or minted without an explicit mode) verify the same as always and report mode: "live" — this is purely additive.

createClient's env-var resolution for secretKey mirrors @vibemonetize/react's publishable-key fallback: it prefers VIBEMONETIZE_SECRET_KEY (live) and falls back to VIBEMONETIZE_TEST_SECRET_KEY whenever the live var is unset/blank. vibemonetize signup only ever mints a TEST pair, so this fallback is what makes meterUsage/ mergeEndUser/requireEntitlement work with zero edits right after signup — the same "zero edits" guarantee the client SDK already gave you.

expectMode — mode enforcement is on by default (RFC 022)

Every entitlement token carries a mode claim, and createClient now enforces it by default instead of merely surfacing it — this closes a real gap: test-mode checkout is a 4242 card away from free, so without enforcement any end user could mint themselves a test-mode "pro" entitlement and replay it against a live backend. The default is derived from your configured secretKey's own prefix:

A mismatch throws VibeMonetizeError("mode_mismatch") (403). Pass expectMode: "any" as the explicit escape hatch (e.g. a preview deployment intentionally serving both modes), or an explicit expectMode: "test" | "live" to override the derived default. A verification-only client (no secretKey at all) enforces nothing by default and logs a one-time console warning recommending you pass expectMode explicitly.

// A live-only backend: sk_live_... already implies expectMode: "live" — no code change.
const monetize = createClient({ appId, secretKey: process.env.STRIPE_LIVE_SECRET_KEY });

// A preview/staging deployment that intentionally serves both modes:
const monetize = createClient({ appId, secretKey, expectMode: "any" });

This is a breaking behavior change for existing integrators: a live client that previously silently honored test-mode tokens will now throw mode_mismatch on one. That is the vulnerability closing, not a regression — see this package's changelog.

Error codes you may see

token_expired (401), unauthorized (401), payment_required (402), forbidden (403), mode_mismatch (403, RFC 022) — see @vibemonetize/core's ERROR_CATALOG for the full list and default HTTP status codes.

Tests

pnpm test — vitest. Generates a local ES256 keypair, issues tokens with core's issueEntitlementToken, and verifies them via an injected verificationKey (bypassing the network JWKS fetch), plus meterUsage/meterUsageStrict retry/give-up/throw behavior against a mocked fetch.

SDK compatibility telemetry

API requests made by createClient include x-vibemonetize-sdk: server/<version>. The header contains no user data and is used to identify deployed SDK versions before changing the /v1 wire protocol.