Supabase app
One prompt: XP, levels, coins, streaks, badges, quests, leaderboards, leagues and referrals in a Supabase app, through an Edge Function that holds the secret key.
For a web or mobile app (or game) that signs users in with Supabase Auth. The default path is server first: one Edge Function, from the Orishare template, holds the Orishare secret key, verifies the caller's Supabase JWT, mints each player's customer session and reports what happened with the secret key. The app calls the function and reads its screens with the publishable key. Facts the function reports are server trust: they are not held to the per-day caps, so level-ups can pay coins.
An app that cannot deploy functions can still report from the browser within caps: Without an Edge Function below.
The prompt
Paste this into your coding agent from the root of the app.
Add a reward system to this Supabase app with Orishare: XP and levels, coins, a daily streak with
repair, badges, a starter quest, a weekly XP leaderboard, a weekly league, friend referrals, and
friends with friend streaks and a weekly friend quest.
Follow https://docs.orishare.com/docs/recipes/supabase.md and the pages it links, step by step:
sign up for Orishare yourself (no human needed), apply the game starter configuration in the
test environment, add the Orishare Edge Function with the test secret key as the ORISHARE_SECRET_KEY
Supabase secret, add the client module and screens, test every call against the test environment, then
give me the claim link, the supabase commands I must run to set the secret and deploy, and a
short list of what you built. Only the publishable key goes in the app; the secret key lives only
in the Supabase secret.Steps
-
Sign up with Start here for AI agents, then apply the game starter in the test environment: Game setup.
-
Add the function. Copy the template's
supabase/functions/orishare/folder (index.ts,handler.ts,verify.ts,orishare-core.ts) into your project'ssupabase/functions/, and add tosupabase/config.toml:[functions.orishare] verify_jwt = false # the function verifies the JWT itselfThe template lives in the Orishare repository at
examples/supabase-edge-functionand is not published as a package yet; its core is the same code as the Firebase kit (@orishare/firebase-functions, also not on npm yet). -
Edit the options in
index.ts:const handler = createOrishareEdgeHandler({ secretKey: () => Deno.env.get('ORISHARE_SECRET_KEY') ?? '', verifyUser: createSupabaseVerifier({ supabaseUrl: SUPABASE_URL, getUser: async (token) => (await supabase.auth.getUser(token)).data.user, }), // Events the app may report. They are sent with the secret key and never capped: list only // facts the function can vouch for, and check them in `validate` (against your tables). events: { allow: ['session.started', 'level.completed', 'match.completed'] }, referral: { qualifies: async (user) => (await levelsFinished(user.uid)) >= 3 }, friends: { streak: 'friend_daily', quest: 'friend_weekly' }, }); Deno.serve(handler);The app sends
{ "action": "...", "data": { ... } }:Action Does sessionPOST /v1/customerswith{ "external_id": "<user id>" }, thenPOST /v1/customer_sessionswithgrant_type: "secret_key"; returnscustomer_token,customer_id(cust_...) andexpires_at. Call again to renew.eventsPOST /v1/eventsfor the caller; each event'sidis derived from the user id and the app'soperation_id, so a retry is aduplicate.mode: "sync"evaluates before answering.referral_qualifiedreferral.qualifiedfor the caller (see Referrals) oncequalifiespasses.friend_progressAfter a finished level: POST /v1/friend_streak_definitions/{id}/activitiesand +1 onPOST /v1/friend_quest_definitions/{id}/contributions, once peroperation_id.match_resultWith matchResults: { profiles: [...] }:POST /v1/match_resultswithwonper player. A report with players other than the caller needs avalidatehook.battle_pass_premiumWith battlePassPremium.verifyPurchase:POST /v1/customers/{id}/battle_pass_premiumafter your code verified the purchase.inventory_grantWith inventoryGrant.authorize:POST /v1/customers/{id}/inventory_grants, once peroperation_id.An action exists only when its option is set. The Orishare customer is the one whose
external_idis the Supabase user id; anonymous users are refused (they cannot earn). The JWT is checked against the project's JWKS when it signs with asymmetric keys (ES256 or RS256), and throughsupabase.auth.getUserfor a project still on the legacy HS256 secret. -
Store the key and deploy:
supabase secrets set ORISHARE_SECRET_KEY=osk_test_... supabase functions deploy orishareThe key never goes in the app or in
VITE_*variables. -
The client module calls the function (
supabase.functions.invokesends the user's access token) and keeps the token in memory:// src/orishare.js import { supabase } from './supabase'; const API = 'https://api.orishare.com'; const PUBLISHABLE_KEY = import.meta.env.VITE_ORISHARE_PUBLISHABLE_KEY; // opk_, public let token = null; // memory only let expiresAt = 0; supabase.auth.onAuthStateChange(() => { token = null; }); const call = async (action, data = {}) => { const { data: out, error } = await supabase.functions.invoke('orishare', { body: { action, data }, }); if (error) throw error; return out; }; async function session() { if (!token || expiresAt - Date.now() < 60_000) { const out = await call('session'); token = out.customer_token; expiresAt = Date.parse(out.expires_at); } return token; } // The same operation_id is the same fact: retry with it, never with a new one. export const track = (operationId, name, properties = {}) => call('events', { events: [{ operation_id: operationId, name, properties }] }); export const friendProgress = (operationId) => call('friend_progress', { operation_id: operationId }); export async function customer(path) { const res = await fetch(API + '/v1/customer/' + path, { credentials: 'omit', headers: { Authorization: `Bearer ${PUBLISHABLE_KEY}`, 'X-Orishare-Customer-Token': await session(), }, }); if (res.status === 401) token = null; if (!res.ok) throw new Error(`orishare ${res.status}`); return res.json(); }Reads go straight to Orishare with the publishable key, so a web app's origin must be allowed (
GET /v1/client_configuration; Game setup, identity). Player writes (repairs, referral claims, friend requests) use the same headers plus anIdempotency-Key, from Game setup, player screens. -
What the app sends, through
track:session.startedonce a day on open (operation_idsession-<UTC day>),level.completed(level-<n>) andmatch.completed(your match id); after a level alsofriendProgresswith the same operation id. Build the screens from Game setup, player screens. -
Friends. Apply
/recipes/social.jsonand add the friends screens from Game setup, friends and social: friend code, requests, the friend list, nudges, friend streaks, the weekly friend quest and league friend standings, all with the player session. -
Test in the test environment (
supabase functions serve orisharelocally, with the key insupabase/functions/.env, or the deployed function with the test key): sign in, play, and check the screens; Game setup, test lists what to expect. -
Go live: apply the configuration in a live environment (Game setup, go live), set
ORISHARE_SECRET_KEYto the live key, redeploy and swap the publishable key in the app.
In a live environment, a secret key an agent created may make the runtime calls above directly
(sessions, events, referrals, friend progress, match results), but value grants answer
403 confirmation_required while the project's autonomy mode is not auto: inventory grants,
energy refills and battle pass premium. For those, the person who claims the project creates the
live secret key, or sets the autonomy mode to auto.
Without an Edge Function
The app can exchange the Supabase access token for a session itself and report its own
events, counted up to the per-day caps (client XP never pays
spendable currency). Use this path or the function for a given app, never both: the identity
exchange creates its own customer for each user, not the one whose external_id is the user id.
This path needs a project that signs tokens with asymmetric keys (RS256 or ES256: Project
Settings, JWT signing keys); Orishare cannot verify the legacy HS256 secret.
-
Connect Supabase. With the test secret key,
POST /v1/identity_connections:{ "provider": "supabase", "issuer": "https://<project ref>.supabase.co/auth/v1", "audience": "authenticated", "verification_url": "https://<project ref>.supabase.co/auth/v1/.well-known/jwks.json", "algorithms": ["ES256"], "token_profile": "supabase_access" }Use
["RS256"]if your signing key is RSA. Keep the returnedid(ic_...) and allow the app's origin as in step 5. -
Exchange the access token whenever Supabase signs a user in or refreshes the session:
const CONNECTION_ID = import.meta.env.VITE_ORISHARE_CONNECTION_ID; // ic_ supabase.auth.onAuthStateChange(async (event, session) => { token = null; if (!session) return; const res = await fetch(`${API}/v1/customer_sessions`, { method: 'POST', credentials: 'omit', headers: { Authorization: `Bearer ${PUBLISHABLE_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ grant_type: 'identity_token', connection_id: CONNECTION_ID, provider_token: session.access_token, }), }); if (res.ok) token = (await res.json()).customer_token; });Anonymous Supabase users get an
anonymoussession whose events do not count. -
Report with
POST /v1/customer/eventsand the session (the helper is in Web game, step 6). Without a backend, levels rise from app XP but pay no level-up coins. Friend streak days and friend quest progress still need a function with the secret key.
Firebase app
One prompt: XP, levels, coins, streaks, badges, quests, leaderboards, leagues and referrals in a Firebase app, through Cloud Functions that hold the secret key.
Game economy
Add a skin shop, consumables, energy, timed boosts, chests, a battle pass and server-validated match results on top of the game starter.