Hono
Hono works well with Guapocado because each request already has a small context object. You can use the raw SDK directly, or install the thin Hono helper.
Install
Section titled “Install”Raw SDK:
npm install @guapocado/sdk honoHono helper:
npm install @guapocado/hono honoRaw SDK
Section titled “Raw SDK”Use this if you want to see exactly what is happening.
import { createGuapocadoClient } from "@guapocado/sdk";import { Hono } from "hono";
type Bindings = { GUAPOCADO_API_KEY: string;};
const app = new Hono<{ Bindings: Bindings }>();
function createBilling(c: { env: Bindings }, customerId: string) { return createGuapocadoClient({ apiKey: c.env.GUAPOCADO_API_KEY, customerId, });}
app.get("/features/:feature", async (c) => { const customerId = c.req.query("customerId"); if (!customerId) return c.json({ error: "customerId required" }, 400);
const guap = createBilling(c, customerId); const feature = c.req.param("feature"); const hasAccess = await guap.has(feature);
return c.json({ feature, hasAccess });});
export default app;Consume Usage
Section titled “Consume Usage”app.post("/api/exports", async (c) => { const body = await c.req.json<{ customerId?: string }>(); if (!body.customerId) return c.json({ error: "customerId required" }, 400);
const guap = createBilling(c, body.customerId);
const access = await guap.context({ features: ["exports"], usage: ["exports"], });
if (!access.features.exports) { return c.json({ error: "Upgrade required" }, 403); }
await guap.usage.consume("exports", 1); const exportJob = await createExportJob(body.customerId);
return c.json({ exportJob });});Consume usage after you know the request should count. If the expensive work can fail after consumption, refund on failure.
Check a Limit
Section titled “Check a Limit”Limits are compared against your app database:
app.post("/projects", async (c) => { const body = await c.req.json<{ customerId?: string; name?: string }>(); if (!body.customerId || !body.name) { return c.json({ error: "customerId and name required" }, 400); }
const guap = createBilling(c, body.customerId); const projects = await guap.limit("projects"); const currentProjects = await db.project.count({ where: { customerId: body.customerId, archived: false }, });
if (currentProjects >= projects.limit) { return c.json({ error: "Project limit reached" }, 403); }
const project = await db.project.create({ customerId: body.customerId, name: body.name, });
return c.json({ project });});Checkout Route
Section titled “Checkout Route”app.post("/checkout/:productKey", async (c) => { const customerId = c.req.query("customerId"); if (!customerId) return c.json({ error: "customerId required" }, 400);
const productKey = c.req.param("productKey"); const origin = new URL(c.req.url).origin; const guap = createBilling(c, customerId);
await guap.customers.create({ id: customerId });
const checkout = await guap.checkout.create({ productKey, successUrl: `${origin}/billing/success`, cancelUrl: `${origin}/billing`, });
return c.redirect(checkout.url);});With @guapocado/hono
Section titled “With @guapocado/hono”The helper package removes the repeated client setup.
import { type GuapocadoHonoEnv, getGuap, getGuapCustomerId, guapocado,} from "@guapocado/hono";import { Hono } from "hono";
type Bindings = { GUAPOCADO_API_KEY: string;};
type AppEnv = GuapocadoHonoEnv<{ Bindings: Bindings }>;
const app = new Hono<AppEnv>();
app.use( "*", guapocado<{ Bindings: Bindings }>({ apiKey: (c) => c.env.GUAPOCADO_API_KEY, customerId: (c) => c.req.query("customerId"), }),);
app.get("/features/:feature", async (c) => { const customerId = getGuapCustomerId(c); if (!customerId) return c.json({ error: "customerId required" }, 400);
const hasAccess = await getGuap(c).has(c.req.param("feature")); return c.json({ hasAccess });});The helper does not own auth. In a real app, resolve customerId from the
authenticated user or active organization, not from a query string.
Local Read Model Webhook Route
Section titled “Local Read Model Webhook Route”@guapocado/hono also adapts @guapocado/sdk’s store-backed local read model
into a one-line route mount, via guapLocalHandler:
import { createGuapLocal, createGuapocadoClient } from "@guapocado/sdk";import { guapLocalHandler } from "@guapocado/hono";import { Hono } from "hono";
const local = createGuapLocal({ apiKey: process.env.GUAPOCADO_API_KEY!, webhook: { publicUrl: "https://app.example.com/webhooks/guap" },});
const guap = createGuapocadoClient({ apiKey: process.env.GUAPOCADO_API_KEY!, adapter: local.adapter,});
const app = new Hono();
app.all( "/webhooks/guap", guapLocalHandler(local, { onPurchase: async (ctx) => { await sendReceipt(ctx.customerId, ctx.purchase.productKey); }, onCancel: async (ctx) => { await notifyChurn(ctx.customerId, ctx.previous); }, }),);
app.get("/features/:key", async (c) => { const hasAccess = await guap.has(c.req.param("key"), { customerId: c.req.query("customerId"), }); return c.json({ hasAccess });});
export default app;guapLocalHandler takes the GuapLocal from createGuapLocal — not the
.handler-augmented client from createGuapocadoClientWithLocal — and adapts
its fetch-shaped webhook handler into a Hono route. See
Local Read Model for the store contract,
webhook hooks, and staleness options.