Better Auth
@guapocado/better-auth installs Guapocado into a Better Auth server.
Use it when Better Auth already owns your users, sessions, organizations, or
teams. The plugin resolves the current session into a Guapocado customerId
and exposes authenticated billing endpoints.
Install
Section titled “Install”npm install @guapocado/better-authServer Plugin
Section titled “Server Plugin”import { guapocado } from "@guapocado/better-auth";import { betterAuth } from "better-auth";import { organization } from "better-auth/plugins";
export const auth = betterAuth({ plugins: [ organization({ teams: { enabled: true, }, }), guapocado({ apiKey: process.env.GUAPOCADO_API_KEY!, customerId: "organization", webhook: { path: "/guap", events: "*", autoRegister: true, }, }), ],});customerId: "organization" means the active Better Auth organization becomes
the Guapocado customer. The plugin uses the organization’s native ID unchanged,
so an organization ID of abc becomes customer ID abc.
Other built-in sources:
"user""organization""team"
Choosing the Customer Source
Section titled “Choosing the Customer Source”Use "user" for individual products.
Use "organization" for most B2B SaaS products.
Use "team" only when teams inside an organization buy separate plans.
You can also resolve the customer yourself:
guapocado({ apiKey: process.env.GUAPOCADO_API_KEY!, resolveCustomerId: (session) => { return String(session.session?.workspaceId); },});resolveCustomerId returns an already-final ID and bypasses mapCustomerId.
A function passed directly as customerId also uses its returned ID unchanged
unless mapCustomerId is configured.
Raw SDK and background jobs
Section titled “Raw SDK and background jobs”Cron jobs, queues, webhook handlers, signup hooks, and admin tools often use
@guapocado/sdk without a Better Auth HTTP session. Pass the same native ID so
those calls address the same customer as the plugin:
import { createGuapocadoClient } from "@guapocado/sdk";
const guap = createGuapocadoClient({ apiKey: process.env.GUAPOCADO_API_KEY!, customerId: organization.id,});Use mapCustomerId if you intentionally need a namespace or a dedicated
billing ID:
guapocado({ apiKey: process.env.GUAPOCADO_API_KEY!, customerId: "organization", mapCustomerId: ({ source, id }) => `${source}_${id}`,});Server API Calls
Section titled “Server API Calls”The plugin adds authenticated methods to auth.api.
const result = await auth.api.guapocadoHas({ headers: request.headers, body: { key: "advanced-analytics" },});Usage:
await auth.api.guapocadoUsageBalance({ headers: request.headers, body: { key: "api-calls" },});
await auth.api.guapocadoUsageConsume({ headers: request.headers, body: { key: "api-calls", amount: 1 },});Checkout:
await auth.api.guapocadoCheckout({ headers: request.headers, body: { productKey: "pro", successUrl: "https://app.example.com/billing/success", cancelUrl: "https://app.example.com/billing", },});On Cloudflare Workers (per-request construction)
Section titled “On Cloudflare Workers (per-request construction)”On Workers the environment is only available per request, so you build the Better
Auth instance inside the request handler. This is fully supported — the plugin’s
init() only validates plugin order and has no per-request side effects. In
particular, it does not register the webhook.
Webhook registration is lazy and idempotent, not tied to construction:
- It runs only when an inbound webhook POST arrives with no stored endpoint yet (or when you hit the status endpoint / register via config).
- It is guarded by a stored-endpoint lookup, so once an endpoint exists it is reused — it will not re-register on subsequent requests.
- The registration call itself dedupes by URL and registration key, so even a first-time race cannot create duplicate endpoints.
The stored-endpoint dedupe relies on a persistent Better Auth database adapter (the same DB that stores users/sessions). With an in-memory or no adapter, the “already registered” lookup can’t persist across requests, so the guarantee weakens — use a real database adapter in production.
So autoRegister: true is safe under per-request construction. If you would
rather register once, up front, set autoRegister: false and register via the
webhook entry in billing.config.ts + guap push (see Webhook Receiver),
or by hitting the status endpoint (GET /api/auth/<path>) once during setup.
Browser Client Plugin
Section titled “Browser Client Plugin”Add the client plugin to your Better Auth client:
import { guapocadoClient } from "@guapocado/better-auth/client";import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({ plugins: [guapocadoClient()],});Then call authenticated billing methods from browser code:
await authClient.guapocado.customer.sync();await authClient.guapocado.has("advanced-analytics");await authClient.guapocado.usage.balance("api-calls");await authClient.guapocado.usage.consume("api-calls", 1);await authClient.guapocado.checkout.create({ productKey: "pro", successUrl: `${location.origin}/billing/success`, cancelUrl: `${location.origin}/billing`,});These browser calls go through Better Auth endpoints on your server. The Guapocado server key stays on the server.
Return shape
Section titled “Return shape”authClient.guapocado.* actions return Better Auth’s { data, error } envelope,
just like the rest of authClient.* — so you handle errors without try/catch:
const { data, error } = await authClient.guapocado.context();if (error) { // network, auth, or API error} else { // `data` is a GuapocadoContext}(The standalone @guapocado/sdk server client is different: it returns values
directly and throws GuapocadoError.)
Syncing Customers
Section titled “Syncing Customers”Call customer.sync() after sign-in or before checkout:
const customer = await authClient.guapocado.customer.sync();The plugin creates or updates the Guapocado customer for the active session.
Webhook Receiver
Section titled “Webhook Receiver”The server plugin exposes a Guapocado receiver under the Better Auth route. With
path: "/guap", the default receiver path is:
/api/auth/guapSignal that receiver in billing.config.ts:
export default defineBilling({ entitlements: { "advanced-analytics": { type: "feature" }, }, products: [], webhooks: { devTunnel: true, forwarding: [ { key: "better-auth", path: "/api/auth/guap", events: "*", integration: "better-auth", autoRegister: true, }, ], },});Run the dev relay while your app is running:
npx guap listen --test --dev --to http://localhost:3010/api/auth/guapThe relay is test-only. Live receivers are direct HTTPS endpoints and must be approved in the Guapocado dashboard.