Orishare
Recipes

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.

For a web or mobile app (or game) that signs users in with Firebase Authentication. The default path is server first: a few Cloud Functions from the @orishare/firebase-functions kit hold the Orishare secret key, trust only users Firebase Auth verified, mint each player's customer session and report what happened with the secret key. The app calls those functions and reads its screens with the publishable key. Facts your functions report 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 Cloud Functions below.

The prompt

Paste this into your coding agent from the root of the app.

Add a reward system to this Firebase 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/firebase.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/firebase-functions Cloud Functions with the test secret key as the
ORISHARE_SECRET_KEY Firebase secret, add the client module and screens, test every call against
the test environment, then give me the claim link, the firebase 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 Firebase secret.

Steps

  1. Sign up with Start here for AI agents, then apply the game starter in the test environment: Game setup. For an app that is not a game, rename the events in the change set before you apply it (for example lesson.completed for level.completed).

  2. Add the kit to the functions project (functions/, firebase-functions 6 or later and firebase-admin 12 or later, Node 20 or 22). The package is not on npm yet: until it is published, build the tarball from the Orishare repository (pnpm --filter @orishare/firebase-functions pack) and install that file (npm install ./orishare-firebase-functions-0.1.0.tgz in functions/). Once published it is npm install @orishare/firebase-functions.

  3. Store the secret key as a Firebase secret (the test key while you build):

    firebase functions:secrets:set ORISHARE_SECRET_KEY

    For the local emulator, put ORISHARE_SECRET_KEY=osk_test_... in functions/.secret.local (never committed). The key never goes in the app, in VITE_* or EXPO_PUBLIC_* variables, or in Firestore.

  4. Write the functions (functions/src/index.ts):

    import { createOrishareFunctions } from '@orishare/firebase-functions';
    
    const orishare = createOrishareFunctions({
      runtime: { region: 'us-central1' },
      // Events the app may report. They are sent with the secret key and never capped: list only
      // facts your function can vouch for, and check them in `validate` (against Firestore, say).
      events: {
        allow: ['session.started', 'level.completed', 'match.completed'],
        validate: async (user, event) => true,
      },
      referral: { qualifies: async (user) => (await levelsFinished(user.uid)) >= 3 },
      friends: { streak: 'friend_daily', quest: 'friend_weekly' },
    });
    
    export const {
      orishareSession,
      orishareEvents,
      orishareReferralQualified,
      orishareFriendProgress,
    } = orishare;

    What each function does:

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

    A function exists only when its option is set. The Orishare customer is the one whose external_id is the Firebase uid; anonymous Firebase users are refused (they cannot earn). The package README lists every option, the orishareHttp endpoint for clients without the callable SDK, and createOrishareClient for your own triggers.

  5. Deploy: firebase deploy --only functions.

  6. The client module calls the functions and keeps the token in memory:

    // src/orishare.js
    import { getFunctions, httpsCallable } from 'firebase/functions';
    
    const API = 'https://api.orishare.com';
    const PUBLISHABLE_KEY = import.meta.env.VITE_ORISHARE_PUBLISHABLE_KEY; // opk_, public
    const functions = getFunctions();
    const sessionFn = httpsCallable(functions, 'orishareSession');
    const eventsFn = httpsCallable(functions, 'orishareEvents');
    let token = null; // memory only
    let expiresAt = 0;
    
    async function session() {
      if (!token || expiresAt - Date.now() < 60_000) {
        const { data } = await sessionFn();
        token = data.customer_token;
        expiresAt = Date.parse(data.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 = {}) =>
      eventsFn({ events: [{ operation_id: operationId, name, properties }] });
    
    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). Writes the player makes (repairs, referral claims, friend requests) use the same headers plus an Idempotency-Key, from Game setup, player screens.

  7. What the app sends, all 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 call orishareFriendProgress with the same operation id. Build the screens from Game setup, player screens.

  8. 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. Friend streak days and quest progress come from orishareFriendProgress.

  9. Test in the test environment: the emulator (firebase emulators:start --only functions,auth) with functions/.secret.local, or the deployed functions with the test key. Sign in, play, and check the screens; Game setup, test lists what to expect.

  10. Go live: apply the configuration in a live environment (Game setup, go live), set ORISHARE_SECRET_KEY to the live key, redeploy the functions 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 Cloud Functions

The app can exchange the Firebase ID 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 kit for a given app, never both: the identity exchange creates its own customer for each user, not the one whose external_id is the uid.

  1. Connect Firebase. With the test secret key, POST /v1/identity_connections:

    {
      "provider": "firebase",
      "issuer": "https://securetoken.google.com/<firebase project id>",
      "audience": "<firebase project id>",
      "verification_url": "https://www.googleapis.com/robot/v1/metadata/x509/[email protected]",
      "algorithms": ["RS256"],
      "token_profile": "firebase_id"
    }

    Keep the returned id (ic_...); with the publishable key it is the app's whole Orishare config. Allow the app's origin as in step 6.

  2. Exchange the ID token after sign-in (not a custom token):

    import { getAuth, onIdTokenChanged } from 'firebase/auth';
    
    const CONNECTION_ID = import.meta.env.VITE_ORISHARE_CONNECTION_ID; // ic_
    onIdTokenChanged(getAuth(), async (user) => {
      token = null;
      if (!user) 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: await user.getIdToken(),
        }),
      });
      if (res.ok) token = (await res.json()).customer_token;
    });

    A Firebase anonymous user gets 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; reward them with badges or cosmetic items instead. Friend streak days and friend quest progress still need a function with the secret key.

On this page