Skip to content

Supabase

Supabase is a good fit when your app already uses Supabase Auth or Postgres and you want billing checks to live near the rest of your backend code.

The @guapocado/supabase package is intentionally small. It exports a standard Deno HTTP handler for Supabase Edge Functions. It does not pull in Supabase client libraries, own your auth model, or register webhooks unless you enable that route.

Supabase documents Edge Functions as Deno-compatible TypeScript functions, and their routing guide shows handlers served with Deno.serve(handler) and paths prefixed with the function name. Guapocado follows that model.

In a Supabase Edge Function, import the package through Deno’s npm support:

import { handler } from "npm:@guapocado/supabase";
Deno.serve(handler);

Set your Guapocado server key as a Supabase secret:

Terminal window
supabase secrets set GUAPOCADO_API_KEY=guap_sk_...

Create a function named guap:

Terminal window
supabase functions new guap

Then put this in supabase/functions/guap/index.ts:

import { handler } from "npm:@guapocado/supabase";
Deno.serve(handler);

Deploy it:

Terminal window
supabase functions deploy guap --no-verify-jwt

Use --no-verify-jwt when your function code is responsible for auth. Supabase can otherwise reject requests before your handler runs; their 401 troubleshooting guide now recommends handling auth in function code when you need that control.

With a function named guap, call routes under:

https://<project-ref>.supabase.co/functions/v1/guap

Available routes:

RoutePurpose
GET /healthFunction health check
GET /features/:key?customerId=...Check a boolean feature
GET /limits/:key?customerId=...Read an effective numeric limit
GET /usage/:key?customerId=...Read meter balance
POST /usage/:key/consumeConsume meter usage
POST /usage/:key/refundRefund meter usage
POST /contextFetch features, usage, limits, plans, and subscription in one request
POST /checkoutCreate a checkout session
GET /plansList configured products
GET /subscription?customerId=...Read the current subscription
POST /subscription/changeChange the subscription product
POST /customersCreate or sync a customer

The default handler accepts customerId from query strings or JSON bodies. That is useful for server-to-server calls and early internal testing. Do not use that default for browser-callable functions unless your app has another access control layer.

Terminal window
curl \
"https://<project-ref>.supabase.co/functions/v1/guap/features/exports?customerId=user_123"

Response:

{
"key": "exports",
"hasAccess": true
}

Use context when your app needs several checks for the same customer.

Terminal window
curl \
"https://<project-ref>.supabase.co/functions/v1/guap/context" \
-H "content-type: application/json" \
-d '{
"customerId": "user_123",
"features": ["exports"],
"usage": ["ai_credits"],
"limits": ["projects"]
}'
Terminal window
curl \
"https://<project-ref>.supabase.co/functions/v1/guap/usage/ai_credits/consume" \
-H "content-type: application/json" \
-d '{
"customerId": "user_123",
"amount": 1
}'

Consume usage after your app has decided that the action should count. If the expensive work can fail after consumption, refund the amount on failure.

For browser-callable functions, resolve the Guapocado customerId from Supabase Auth or from the user’s active organization. The handler exposes a customerId hook for that.

This example scopes billing to the authenticated Supabase user ID:

import { createGuapocadoSupabaseHandler } from "npm:@guapocado/supabase";
import { createClient } from "npm:@supabase/supabase-js@2";
const handler = createGuapocadoSupabaseHandler({
allowRequestCustomerId: false,
customerId: async (request) => {
const authorization = request.headers.get("Authorization");
if (!authorization) return undefined;
const supabase = createClient(
Deno.env.get("SUPABASE_URL") ?? "",
Deno.env.get("SUPABASE_ANON_KEY") ?? "",
{
global: {
headers: { Authorization: authorization },
},
},
);
const { data, error } = await supabase.auth.getUser();
if (error || !data.user) return undefined;
return data.user.id;
},
});
Deno.serve(handler);

If your app bills organizations or workspaces, return the organization ID instead. That resolver is also the right place to query Supabase Postgres and confirm that the authenticated user belongs to the organization they are acting inside.

Call the Supabase function with the user’s Supabase access token. The exact client wrapper is up to your app; the important part is that the Authorization header reaches the Edge Function.

const response = await fetch(
`${SUPABASE_URL}/functions/v1/guap/context`,
{
method: "POST",
headers: {
"content-type": "application/json",
Authorization: `Bearer ${session.access_token}`,
},
body: JSON.stringify({
features: ["exports"],
usage: ["ai_credits"],
limits: ["projects"],
}),
},
);
const context = await response.json();

Do not send customerId from the browser in this pattern. The function resolves it from the Supabase token.

const response = await fetch(
`${SUPABASE_URL}/functions/v1/guap/checkout`,
{
method: "POST",
headers: {
"content-type": "application/json",
Authorization: `Bearer ${session.access_token}`,
},
body: JSON.stringify({
productKey: "pro",
successUrl: `${APP_URL}/billing/success`,
cancelUrl: `${APP_URL}/billing`,
}),
},
);
const { url } = await response.json();
window.location.href = url;

The handler uses the resolved customerId when creating checkout sessions. If your customer record needs an email or Stripe customer ID, create or sync the customer first with the server SDK or POST /customers.

Webhook registration is off by default. Enable it only when you want the Supabase function to register Guapocado webhook endpoints.

import { createGuapocadoSupabaseHandler } from "npm:@guapocado/supabase";
const handler = createGuapocadoSupabaseHandler({
webhooks: {
enabled: true,
registrationKey: Deno.env.get("GUAPOCADO_WEBHOOK_REGISTRATION_KEY"),
},
});
Deno.serve(handler);

This does not install a local webhook projection table or sync process. If you want a Supabase Postgres read model, generate Postgres tables with the CLI and wire the server SDK adapter in your own backend code.

Some Deno adapters and file-based routers ask for an ALL export. Supabase Edge Functions should still use Deno.serve(handler), but the package includes an alias for adapters with that convention:

import { handler } from "@guapocado/supabase";
export const ALL = handler;

You can also import the alias directly:

export { ALL } from "@guapocado/supabase";

Most apps only need this:

createGuapocadoSupabaseHandler({
customerId: async (request) => {
// Resolve from Supabase Auth, a workspace membership row, or another
// trusted server-side source.
},
});

Supported options:

OptionPurpose
apiKeyOverride the default GUAPOCADO_API_KEY secret lookup
customerIdResolve the customer for this request
allowRequestCustomerIdAllow customerId from query/body when no resolver returned one
corsDisable or customize CORS headers
routePrefixStrip a custom path prefix before route matching
webhooksEnable the webhook registration route
onErrorLog unexpected handler errors

Keep the Guapocado server key in Supabase secrets. Never send it to the browser.