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
-
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.completedforlevel.completed). -
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.tgzinfunctions/). Once published it isnpm install @orishare/firebase-functions. -
Store the secret key as a Firebase secret (the test key while you build):
firebase functions:secrets:set ORISHARE_SECRET_KEYFor the local emulator, put
ORISHARE_SECRET_KEY=osk_test_...infunctions/.secret.local(never committed). The key never goes in the app, inVITE_*orEXPO_PUBLIC_*variables, or in Firestore. -
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:
Function Does orishareSessionPOST /v1/customerswith{ "external_id": "<uid>" }, thenPOST /v1/customer_sessionswithgrant_type: "secret_key"; returnscustomer_token,customer_id(cust_...) andexpires_at. Call again to renew.orishareEventsPOST /v1/eventsfor the caller; each event'sidis derived from the uid and the app'soperation_id, so a retry is aduplicate.mode: "sync"evaluates before answering.orishareReferralQualifiedreferral.qualifiedfor the caller (see Referrals) once yourqualifiescheck passes.orishareFriendProgressAfter a finished level: POST /v1/friend_streak_definitions/{id}/activitiesand +1 onPOST /v1/friend_quest_definitions/{id}/contributions, once peroperation_id.orishareMatchResultWith matchResults: { profiles: [...] }:POST /v1/match_resultswithwonper player. A report with players other than the caller needs avalidatehook.orishareBattlePassPremiumWith battlePassPremium.verifyPurchase:POST /v1/customers/{id}/battle_pass_premiumafter your server verified the purchase.orishareInventoryGrantWith inventoryGrant.authorize:POST /v1/customers/{id}/inventory_grants, once peroperation_id.A function exists only when its option is set. The Orishare customer is the one whose
external_idis the Firebaseuid; anonymous Firebase users are refused (they cannot earn). The package README lists every option, theorishareHttpendpoint for clients without the callable SDK, andcreateOrishareClientfor your own triggers. -
Deploy:
firebase deploy --only functions. -
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 anIdempotency-Key, from Game setup, player screens. -
What the app sends, all through
track:session.startedonce a day on open (operation_idsession-<UTC day>),level.completed(level-<n>) andmatch.completed(your match id). After a level, also callorishareFriendProgresswith 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. Friend streak days and quest progress come fromorishareFriendProgress. -
Test in the test environment: the emulator (
firebase emulators:start --only functions,auth) withfunctions/.secret.local, or the deployed functions 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 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.
-
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. -
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
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; reward them with badges or cosmetic items instead. Friend streak days and friend quest progress still need a function with the secret key.
Roblox
One prompt: XP, levels, coins, streaks, badges, quests, leaderboards, leagues and referrals in a Roblox experience, from server scripts with HttpService.
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.