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({ streakId }) returns count, freezesAvailable, gracePeriodEndsAt, and nextMilestone.
- Tick the streak server-side. Sending the event from the client risks losing days when the user is offline.
- spendFreeze is an imperative call. Use it when the user explicitly opts to use one.
- Listen with onMilestoneHit for celebration moments at 7, 30, 100 days.
- Always render the freeze count if available. Hidden inventory undermines the safety net.
Render the streak
One hook, full state
"use client";
import { useStreak } from "@bricqs/headless-react";
export function StreakWidget() {
const {
count,
freezesAvailable,
gracePeriodEndsAt,
nextMilestone,
isLoading,
spendFreeze,
} = useStreak({ streakId: "daily_practice" });
if (isLoading) 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">{count} day streak</h3>
{freezesAvailable > 0 && (
<span className="text-sm text-slate-500">
{freezesAvailable} freeze{freezesAvailable === 1 ? "" : "s"} left
</span>
)}
</header>
{gracePeriodEndsAt && (
<p className="text-amber-600 text-sm">
You missed yesterday. Grace period ends {gracePeriodEndsAt}. Use a freeze?
<button onClick={() => spendFreeze()} className="ml-2 underline">
Use freeze
</button>
</p>
)}
{nextMilestone && (
<p className="text-sm text-slate-500 mt-3">
Next milestone: {nextMilestone.days} days · earns {nextMilestone.rewardLabel}
</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
const { onMilestoneHit } = useStreak({ streakId: "daily_practice" });
onMilestoneHit((event) => {
// event = { days: 7, rewardId: "..." }
showCelebration(`${event.days}-day streak earned!`);
fireConfetti();
});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.
Use gracePeriodEndsAt. Surface the freeze option; only show 'broken' after the grace expires.
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.
