Orishare
Recipes

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

  1. Sign up with Start here for AI agents, then apply the game starter in the test environment: Game setup.

  2. Add the function. Copy the template's supabase/functions/orishare/ folder (index.ts, handler.ts, verify.ts, orishare-core.ts) into your project's supabase/functions/, and add to supabase/config.toml:

    [functions.orishare]
    verify_jwt = false # the function verifies the JWT itself

    The template lives in the Orishare repository at examples/supabase-edge-function and 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).

  3. 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": { ... } }:

    ActionDoes
    sessionPOST /v1/customers with { "external_id": "<user id>" }, then POST /v1/customer_sessions with grant_type: "secret_key"; returns customer_token, customer_id (cust_...) and expires_at. Call again to renew.
    eventsPOST /v1/events for the caller; each event's id is derived from the user id and the app's operation_id, so a retry is a duplicate. mode: "sync" evaluates before answering.
    referral_qualifiedreferral.qualified for the caller (see Referrals) once qualifies passes.
    friend_progressAfter a finished level: POST /v1/friend_streak_definitions/{id}/activities and +1 on POST /v1/friend_quest_definitions/{id}/contributions, once per operation_id.
    match_resultWith matchResults: { profiles: [...] }: POST /v1/match_results with won per player. A report with players other than the caller needs a validate hook.
    battle_pass_premiumWith battlePassPremium.verifyPurchase: POST /v1/customers/{id}/battle_pass_premium after your code verified the purchase.
    inventory_grantWith inventoryGrant.authorize: POST /v1/customers/{id}/inventory_grants, once per operation_id.

    An action exists only when its option is set. The Orishare customer is the one whose external_id is 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 through supabase.auth.getUser for a project still on the legacy HS256 secret.

  4. Store the key and deploy:

    supabase secrets set ORISHARE_SECRET_KEY=osk_test_...
    supabase functions deploy orishare

    The key never goes in the app or in VITE_* variables.

  5. The client module calls the function (supabase.functions.invoke sends 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 an Idempotency-Key, from Game setup, player screens.

  6. What the app sends, through track: session.started once a day on open (operation_id session-<UTC day>), level.completed (level-<n>) and match.completed (your match id); after a level also friendProgress with the same operation id. Build the screens from Game setup, player screens.

  7. Friends. Apply /recipes/social.json and 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.

  8. Test in the test environment (supabase functions serve orishare locally, with the key in supabase/functions/.env, or the deployed function with the test key): sign in, play, and check the screens; Game setup, test lists what to expect.

  9. Go live: apply the configuration in a live environment (Game setup, go live), set ORISHARE_SECRET_KEY to 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.

  1. 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 returned id (ic_...) and allow the app's origin as in step 5.

  2. 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 anonymous session whose events do not count.

  3. Report with POST /v1/customer/events and 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.

On this page