Headless SDK: streaks
useStreak returns the live streak count, freeze inventory, grace status, and the next milestone. Wire one tick event a day; the SDK renders the rest.
Key takeaways
Quick read- useStreak(code) returns a StreakStatus: currentCount, longestCount, freezesAvailable, isAtRisk.
- Tick the streak server-side. Sending the event from the client risks losing days when the user is offline.
- Freezes are grace periods, spent automatically on a miss while any remain — there is no manual spend call.
- Read freezesAvailable to show the safety net; surface isAtRisk to nudge the user before the period ends.
- Always render the freeze count. Hidden inventory undermines the safety net.
Render the streak
One hook, full state
"use client";
import { useStreak } from "@bricqs/sdk-react";
export function StreakWidget() {
// participantId inherited from <BricqsProvider> / participantToken
const { streak, isLoading } = useStreak("daily_practice");
if (isLoading || !streak) return null;
return (
<section className="rounded-xl border p-5">
<header className="flex items-baseline justify-between mb-2">
<h3 className="font-bold text-2xl">{streak.currentCount} day streak</h3>
{streak.freezesAvailable > 0 && (
<span className="text-sm text-slate-500">
{streak.freezesAvailable} freeze{streak.freezesAvailable === 1 ? "" : "s"} left
</span>
)}
</header>
{streak.isAtRisk && (
<p className="text-amber-600 text-sm">
You haven't recorded today yet — record before the period ends to keep the
streak. If you miss, a freeze is spent automatically (while any remain).
</p>
)}
<p className="text-sm text-slate-500 mt-3">Longest: {streak.longestCount} days</p>
</section>
);
}Tick the streak
One event a day, server-side
Always tick from the server. Client-side ticks lose days when the user is offline.
import { emitToBricqs } from "@/lib/bricqs";
export async function logPractice(userId: string) {
await savePractice(userId);
// One tick per day. Idempotent: same key means same day.
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
await emitToBricqs(
userId,
"streak_tick",
{ streak_id: "daily_practice" },
`p_${userId}:streak_tick:daily_practice:${today}`
);
}Milestones
Celebrate the long runs
// Derive celebration moments from the streak count. Poll-based today
// (refreshInterval), so compare against the previous render.
const { streak } = useStreak("daily_practice");
const prev = usePrevious(streak?.currentCount ?? 0);
useEffect(() => {
const n = streak?.currentCount ?? 0;
if ([7, 30, 100].includes(n) && n !== prev) {
showCelebration(`${n}-day streak!`);
fireConfetti();
}
}, [streak?.currentCount, prev]);Common mistakes
What breaks streaks
Ticking from the client. Offline users lose days; the streak appears broken.
Always tick from the server when the qualifying action lands. Use idempotent keys so retries are safe.
Hiding the freeze count. The safety net feels punitive when invisible.
Always show freezesAvailable. The user only uses the freeze if they know it exists.
Allowing infinite freeze accumulation. The streak loses meaning.
Cap freezes server-side (configured at the streak definition). The SDK reflects the cap; do not implement client-side caps.
Showing 'You broke your streak!' immediately on miss. Users uninstall in anger.
Read isAtRisk and nudge the user to record. A freeze is spent automatically while any remain; only treat it as broken once currentCount resets.
Developer FAQ
Common questions when integrating gamification with Bricqs.
Ready to ship?
Wire it up with the Bricqs SDK or API
Headless SDK for React UIs, REST API for any backend. Same engine behind both.
