The AI gateway for
Supabase Edge Functions.
Verify the Supabase user, forward their id in one header, and every AI request is metered, limited and billable to that person. Any model, one OpenAI-compatible API, no package to install.
Your function decides who is asking. odnoga does the rest.
- Your app
Calls the Edge Function with the signed-in user’s Supabase session.
Your Edge Function
Verifies the JWT, refuses anyone unverified, adds x-odnoga-end-user.
- odnoga
Checks the user’s credits, routes to the model, records cost and latency for that user.
From an Edge Function to your first metered request
- 1
Create a free odnoga account and an API key.
In your workspace, open “API keys” and choose “New key”. - 2
Store it as a Supabase secret.
supabase secrets set ODNOGA_API_KEY=sk_live_… - 3
Add the helper and call it from any function.
One shared file. Every function that imports it is verified, metered and traced per user.
// supabase/functions/_shared/odnoga.ts
import { createClient } from "jsr:@supabase/supabase-js@2";
const BASE = "https://api.odnoga.com/v1";
// Resolve the caller, or refuse. Your odnoga key is a paid credential: if
// this throws before it is used, an unauthenticated request cannot spend it.
export async function requireUser(req: Request): Promise<string> {
const jwt = (req.headers.get("authorization") ?? "").split(" ")[1];
if (!jwt) throw new Response("Unauthorized", { status: 401 });
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
);
// VERIFY the token. A decoded-but-unverified subject can be forged, and
// the spend would land on someone else's account.
const { data, error } = await supabase.auth.getUser(jwt);
if (error || !data.user) throw new Response("Unauthorized", { status: 401 });
return data.user.id;
}
// One caller per request, bound to the verified Supabase user.
export function odnoga(endUser: string, fn = "edge") {
return (path: string, body: unknown) =>
fetch(BASE + path, {
method: "POST",
headers: {
"content-type": "application/json",
Authorization: `Bearer ${Deno.env.get("ODNOGA_API_KEY")}`,
"x-odnoga-end-user": endUser, // whose usage this is
"x-odnoga-function": fn, // which of your functions spent it
},
body: JSON.stringify(body),
});
}What you get once the helper is in
Your Supabase user is the end user
Verify the JWT with supabase.auth.getUser() and send the user id in one header, x-odnoga-end-user. Every request, cost and limit is then that person’s. An id that looks like an email or phone number is hashed before it is stored.
One secret, server-side only
supabase secrets set ODNOGA_API_KEY=… The key lives in your Edge Function’s environment, never in the browser or in your repository.
Credits per user
Give each Supabase user a wallet. When it runs out, odnoga answers 402 end_user_exhausted before any model is called, so one user can never spend another’s money or yours.
How it worksBill your users through your own Stripe
Plans, checkout and a billing portal for your users, keyed by the same user id. The revenue goes to your Stripe account, not ours.
How it worksSee which function spends
Name the caller with x-odnoga-function (and x-odnoga-app) and every Edge Function gets its own cost, volume and error line, plus a warning when its traffic stops or surges.
How it worksCall from the browser with eut_ tokens
Your function mints a short-lived token for one user with POST /v1/sessions (a browser-scoped key and an allowed origin; one hour by default, at most 24). The browser calls odnoga directly and never sees cost or vendor details.
How it worksRAG over pgvector, your data stays put
Embeddings and chat through odnoga, the vector search in your own database under your RLS. odnoga stores no embeddings and never connects to your project.
Streaming, embeddings, images
OpenAI-compatible /chat/completions (streamed or not), /embeddings and /images/generations, across the model catalogue, behind the same helper.
Every call is a row
User, function, model, prompt version, tokens, cost and latency for each request, searchable in the dashboard and exportable per user for GDPR requests.
How it worksThree functions you will probably write
All three import the helper above. Model names come from the live catalogue.
import { odnoga, requireUser } from "../_shared/odnoga.ts";
Deno.serve(async (req) => {
let userId: string;
try {
userId = await requireUser(req);
} catch (refusal) {
return refusal as Response; // 401, before the paid key is touched
}
const { message } = await req.json();
const ai = odnoga(userId, "chat");
const r = await ai("/chat/completions", {
model: "gpt-5.1",
messages: [{ role: "user", content: message }],
});
// Pass the status through: a 402 (credits used) or 429 must not
// reach your client as a 200.
return new Response(r.body, {
status: r.status,
headers: { "content-type": "application/json" },
});
});Call the vendor directly, or through odnoga?
Call the vendor directly if…
- — one model from one vendor is all you will ever need
- — you do not need to know what each user costs
- — nobody pays you for the AI inside your app
Put odnoga in between when…
- — you want every Supabase user’s AI cost, not one monthly total
- — a user’s spend must stop at a limit before the bill arrives
- — you want to switch models without redeploying functions
- — you bill your users for usage through Stripe
Is this an official Supabase product?
No. odnoga is an independent AI gateway and is not affiliated with Supabase. The integration is a plain HTTPS call from your Edge Function, so nothing in your project depends on us.
Do I need to install a package?
No. One file in supabase/functions/_shared and one secret. The endpoint is OpenAI-compatible, so any OpenAI SDK also works if you point its base URL at odnoga and add the header.
Does odnoga verify my users’ JWTs?
No — your Edge Function does, with supabase.auth.getUser(). odnoga attributes usage to the id you send, which is exactly why the helper refuses an unverified caller before your key is used.
What about row-level security?
Untouched. odnoga never connects to your database. Your Supabase client and RLS keep deciding what each user can read; odnoga only records the id you send to attribute cost.
Can I cap what one user spends?
Yes, with per-user wallets: when a user’s credits run out, their next request is refused with 402. Dollar budgets and alerts apply to the whole workspace on top of that.
Can the browser call odnoga directly?
Yes. Create a browser-scoped key with your site’s origin allowed, mint an eut_ token for the signed-in user from your Edge Function with POST /v1/sessions, and use it as the bearer. Your server key never leaves the server.
I built my app in Lovable. Does this apply?
Yes — Lovable apps run on Supabase, including Lovable Cloud. The Lovable page has one prompt that makes Lovable write this helper for you.
Building with Lovable? One prompt sets this up for you