Skip to content

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.

Terminal window
npm install @guapocado/better-auth
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"

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.

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}`,
});

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.

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.

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.)

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.

The server plugin exposes a Guapocado receiver under the Better Auth route. With path: "/guap", the default receiver path is:

/api/auth/guap

Signal 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:

Terminal window
npx guap listen --test --dev --to http://localhost:3010/api/auth/guap

The relay is test-only. Live receivers are direct HTTPS endpoints and must be approved in the Guapocado dashboard.