BricqsBricqs

Pattern: streak with freeze

A daily streak with a one-freeze-per-week safety net and milestones at 7, 30, 100, and 365 days. Server-side config, server-side ticks, client-side rendering. Roughly 80 lines of code total.

Reading time9 minutes
Last updatedMay 2026

Key takeaways

Quick read
  • Streak config lives server-side. Period (day), grace (24h), freeze allowance (1/week), milestones (7/30/100/365).
  • Tick from the server when the qualifying action lands. Idempotency key on the date.
  • Client renders count, freeze inventory, grace state, and the next milestone with one hook.
  • Milestones fire webhooks; route them to your ESP for celebration emails.
  • Recovery: when the user misses a day, surface the freeze option for the duration of the grace window.

Anatomy

What you are building

API

POST /admin/streaks defines the streak. POST /events ticks it. Webhook fires on milestones.

SDK

useStreak({ streakId }) renders count, freezesAvailable, gracePeriodEndsAt, nextMilestone, and exposes spendFreeze().

User sees

A flame counter that grows each day, a 'use a freeze' button when grace kicks in, and confetti at 7, 30, 100, 365 days.

Step 1: config

Define the streak once

POST /api/v1/admin/streaks·bash
curl -X POST https://api.bricqs.co/api/v1/admin/streaks \
  -H "Authorization: Bearer bq_live_admin_..." \
  -d '{
    "id": "daily_practice",
    "label": "Daily practice",
    "period": "day",
    "qualifying_event": "practice_completed",
    "grace_hours": 24,
    "freeze_allowance": { "per_period": "week", "max": 1, "stockpile_max": 4 },
    "milestones": [
      { "days": 7,   "reward": { "type": "badge",  "id": "streak_7"   } },
      { "days": 30,  "reward": { "type": "voucher","value": 200      } },
      { "days": 100, "reward": { "type": "badge",  "id": "streak_100" } },
      { "days": 365, "reward": { "type": "experience", "id": "streak_year" } }
    ]
  }'

Step 2: tick

Server-side, on every qualifying action

server-side·ts
import { emitToBricqs } from "@/lib/bricqs";

export async function logPractice(userId: string) {
  await savePractice(userId);

  const today = new Date().toISOString().slice(0, 10);
  await emitToBricqs(
    userId,
    "practice_completed",
    { practice_id: "daily" },
    `p_${userId}:practice_completed:${today}`
  );
}

Idempotent on the date. Multiple practice events on the same day count once.

Step 3: render

One hook, full state

components/StreakWidget.tsx·tsx
"use client";
import { useStreak } from "@bricqs/headless-react";

export function StreakWidget() {
  const {
    count,
    freezesAvailable,
    gracePeriodEndsAt,
    nextMilestone,
    spendFreeze,
    onMilestoneHit,
  } = useStreak({ streakId: "daily_practice" });

  onMilestoneHit((event) => {
    fireConfetti();
    showToast(`${event.days}-day streak earned!`);
  });

  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. Use a freeze before {gracePeriodEndsAt}?
          <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>
  );
}

Step 4: celebrations

Wire milestone webhooks to your ESP

webhook handler·ts
if (event.type === "streak.milestone_hit") {
  const { participant_id, days, reward_id } = event.data;
  await sendCelebrationEmail(participant_id, {
    template: `streak_${days}_day`,
    rewardId: reward_id,
  });
}

Pre-launch checklist

Before you ship

text
Configuration
[ ] Streak created (POST /admin/streaks)
[ ] Period, grace, and freeze allowance set per category
[ ] Milestone rewards funded (badge ids exist; voucher inventory ready)

Server
[ ] practice_completed event fires on qualifying action
[ ] Idempotency key uses local date in user timezone (not UTC)
[ ] Webhook handler verifies HMAC

Client
[ ] StreakWidget on home screen
[ ] Freeze button visible during grace
[ ] Confetti / toast on milestone

Recovery
[ ] Email at 24h after miss with freeze CTA
[ ] Email at 48h after miss confirming streak ended (if not frozen)

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.

1 brief to align the room2 mechanics max in version one
What happens next
01
Pick the mechanic
Choose the smallest working system for the brief.
02
Launch without rebuilds
Configure rules and rewards in one place.