Skip to content

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.

Raw SDK:

Terminal window
npm install @guapocado/sdk hono

Hono helper:

Terminal window
npm install @guapocado/hono hono

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

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

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.

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