Guide
How to add virtual gifts to a Firebase chat
Your app already chats through Firestore: a messages collection under each
chat, read with a live listener. GiftingStack adds the gifts: coins, the gift tray, the
effects and the ledger. GiftingStack carries no chat of its own, so every gift lands in
your Firestore chat, next to the messages your users already read. This guide shows both
ways to get it there, and where the gift button, the tray and the effects go on the
screen.
Two ways a gift reaches the chat
-
From your server (direct messages, group chats, communities): the
gift.sentwebhook reaches your server, which writes a gift message into the chat'smessagescollection with the Firebase Admin SDK. The gift becomes part of the chat's history, and people who were offline see it later. - In the app, in a live room (live streams, audio rooms, watch parties): the gift room tells every viewer about every gift in under a second, and the app adds a gift row to the chat list it already draws. Nothing is written to Firestore, which keeps a busy stream from paying for thousands of gift documents and listener reads.
Pick one per screen, so a gift never shows twice. Both start from the same setup: a project, a gift catalog and a client token (quickstart).
1. Send the gift with the chat as its reference
For the server path, the webhook has to know which chat the gift belongs to. Give the tray
the chat's document id as reference (your own context id, up to 128
characters); it comes back in the gift.sent event.
const tray = createGiftTray({
container: screen,
client,
receiver: otherMemberId, // who gets the gift (your user id)
reference: `chat:${chatId}`, // "chat:ava-mateo"
effects,
onTopUp: openCoinStore,
});
2. Server: write a gift message on gift.sent
Verify the signature on the raw body first, with the verify function from
Webhooks. Then write with the Firebase Admin SDK (npm i firebase-admin). The document id is the event id and create refuses an existing document,
so a repeated delivery never writes a second message.
import express from "express";
import { initializeApp } from "firebase-admin/app";
import { FieldValue, getFirestore } from "firebase-admin/firestore";
initializeApp();
const db = getFirestore();
app.post("/webhooks/giftingstack", express.text({ type: "application/json" }), async (req, res) => {
if (!verify(req.body, req.get("GiftingStack-Signature") ?? "", process.env.GS_WEBHOOK_SECRET))
return res.sendStatus(400);
const event = JSON.parse(req.body);
const gift = event.data;
if (event.type !== "gift.sent" || !gift.reference?.startsWith("chat:")) return res.sendStatus(200);
const chatId = gift.reference.slice("chat:".length);
try {
await db.collection("chats").doc(chatId).collection("messages").doc(event.id).create({
type: "gift",
senderId: gift.sender,
giftName: gift.gift.name,
giftIcon: gift.gift.icon_url,
quantity: gift.quantity,
createdAt: FieldValue.serverTimestamp(),
});
} catch (err) {
if (err.code !== 6) return res.sendStatus(500); // 6: already written; else retried later
}
res.sendStatus(200);
});
-
The document holds the sender, the gift and the quantity only. The gift's
messageis never copied into a chat. -
Your message list already listens to the collection, so the gift appears for everyone in
the chat. Draw documents with
typegiftas a gift row: the picture, "Ava sent Rose x3". - Only your server writes gift rows. The Admin SDK is not bound by security rules; keep your users to plain messages, so nobody fakes a gift:
match /chats/{chatId}/messages/{messageId} {
allow create: if isMember(chatId) && request.resource.data.type == "text";
}
When Firestore fails, the handler answers 500 and GiftingStack delivers the event again later. Answer within 10 seconds: when writing can take longer, queue it and answer first.
3. App: a live gift row from the gift room
In a live stream, every viewer joins the gift room. The room's onEvent hands
over each gift sent into it by someone else; the tray's onSent hands over the
viewer's own. Both carry the gift transaction id, so a gift replayed after a reconnect is
not added again. The stream's chat messages from Firestore go into the same list.
import { GiftEffectLayer, createGiftTray, joinRoom } from "@giftingstack/sdk/web";
import { collection, onSnapshot, orderBy, query } from "firebase/firestore";
const shown = new Set();
function addGiftRow(tx, who, giftName, icon, quantity) {
if (shown.has(tx)) return;
shown.add(tx);
chatList.append(renderGiftRow({ who, giftName, icon, quantity })); // your row component
}
const effects = new GiftEffectLayer(videoBox); // effects play over the video
const room = joinRoom({
room: "live_123",
getToken,
client,
effects,
onEvent: (e) => addGiftRow(e.tx, e.senderName ?? "Someone", e.gift.name, e.gift.icon, e.quantity),
});
const tray = createGiftTray({
container: screen,
client,
receiver: hostId,
roomId: "live_123",
effects,
anchor: giftButton,
collapseOnSelect: true,
scrim: true,
onTopUp: openCoinStore,
onSent: (t) => addGiftRow(t.id, "You", t.gift.name, t.gift.icon_url, t.quantity),
});
giftButton.onclick = () => tray.toggle();
// the stream's chat, from Firestore, into the same list
const messages = query(collection(db, "chats", "live_123", "messages"), orderBy("createdAt"));
const stop = onSnapshot(messages, (snap) => {
for (const change of snap.docChanges()) {
if (change.type === "added") chatList.append(renderMessage(change.doc.data()));
}
});
// leaving the stream: room.leave(); tray.destroy(); stop();
React Native: the same two callbacks on the hooks.
const room = useGiftRoom({
room: "live_123", getToken, client, effects,
onEvent: (e) => addGiftRow(e.tx, e.senderName ?? "Someone", e.gift.name, e.gift.icon, e.quantity),
});
const tray = useGiftTray({
client, receiver: hostId, roomId: "live_123", effects,
onSent: (t) => addGiftRow(t.id, "You", t.gift.name, t.gift.icon_url, t.quantity),
});
Flutter GiftRoom(onEvent: ...) with
GiftTrayNotifier(onSent: ...), Android
rememberGiftRoom(onEvent = ...) with
rememberGiftTrayHolder(onSent = ...), iOS
GiftRoomModel(..., onEvent: ...) with
GiftTrayModel(..., onSent: ...). Every room event has tx,
senderName, gift.name and quantity. The room
carries gift events only: no message text, no chat.
4. The screen layout
-
Gift button next to the chat input. Put it beside your message input;
one tap calls
tray.toggle(). -
The tray as a bottom sheet. On phones it slides up over the lower part
of the screen.
collapseOnSelect: trueshrinks it to the picked gift and Send, so the stream and the chat stay in sight;scrim: truedims the page behind a tall sheet, and a tap beside it lowers or closes it. On wide screens,anchor: giftButtonmakes it a popover next to the button (web). - Effects over the video. Give the effect layer the video container, not the chat: big gifts play full size over the stream, and the chat list stays readable.
Colours, texts and motion: Customize. Rooms, tokens and limits: Gift rooms.
Next steps
- Quickstart: your first gift in minutes.
- Webhooks: events, signatures and retries.
- Stream Chat and Sendbird versions of this guide.
- llms-full.txt: the complete integration guide for AI coding assistants.