BricqsBricqs

Pattern: onboarding flow

A complete onboarding challenge from configuration to completion. Five steps, three reward beats, server-side scoring, client-side rendering. Copy this file, change the names, ship.

Last updatedMay 2026

Key takeaways

Quick read
  • Configure the challenge once via the admin API. Render it with a single React hook.
  • Five-step shape: profile, first action, connect, invite, outcome.
  • Reward at step 1 (badge), at step 3 (free shipping), at completion (loyalty enrolment plus 500 INR voucher).
  • Drop-off recovery via webhook to your ESP. No client-side scheduling needed.
  • Total integration time: a working day if you already have auth and a CRM.

Anatomy

What you are building

API

POST /admin/challenges to create the challenge. POST /events when the user completes a step. Webhooks fire on completion.

SDK

useChallenge({ challengeId: 'onboarding_v3' }) renders the live state. submitEvent advances objectives optimistically.

User sees

A 5-step checklist on the home screen, ticking off as they act. Reward moments at step 1, step 3, and completion. Skip option in the corner.

Step 1: configuration

Create the challenge once

Run this once per environment. The challenge id is what your client code references.

POST /api/v1/admin/challenges·bash
curl -X POST https://api.bricqs.co/api/v1/admin/challenges \
  -H "Authorization: Bearer bq_live_admin_..." \
  -H "X-Bricqs-Tenant: tenant_brand_xyz" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "onboarding_v3",
    "title": "Get started in 5 steps",
    "window_days": 14,
    "auto_enrol_on_signup": true,
    "objectives": [
      {
        "id": "complete_profile",
        "label": "Complete your profile",
        "evaluator": "completion",
        "event_type": "profile_completed",
        "reward": { "type": "badge", "id": "welcome_explorer" }
      },
      {
        "id": "first_purchase",
        "label": "Place your first order",
        "evaluator": "completion",
        "event_type": "purchase_completed"
      },
      {
        "id": "free_shipping_unlocked",
        "label": "Unlock free shipping for life",
        "evaluator": "completion",
        "event_type": "internal:milestone_3",
        "reward": { "type": "perk", "id": "free_shipping_lifetime" }
      },
      {
        "id": "invite_a_friend",
        "label": "Invite a friend",
        "evaluator": "completion",
        "event_type": "referral_sent"
      },
      {
        "id": "second_purchase",
        "label": "Make your second order",
        "evaluator": "completion",
        "event_type": "purchase_completed",
        "min_count": 2
      }
    ],
    "completion_reward": {
      "type": "voucher",
      "value": 500,
      "currency": "INR"
    },
    "drop_off_recovery": {
      "webhook_at": ["72h_no_progress", "168h_incomplete"]
    }
  }'

Auto-enrol on signup means every new user enters the challenge automatically. The window_days is the deadline.

Step 2: server

Send events as the user acts

Most events are already firing in your stack. You just need to forward them with an idempotency key.

lib/bricqs.ts (server-only)·ts
export async function emitToBricqs(
  participantId: string,
  eventType: string,
  attributes: Record<string, unknown>,
  idempotencyKey: string
) {
  await fetch("https://api.bricqs.co/api/v1/ingest/events", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${process.env.BRICQS_ADMIN_KEY!}`,
      "X-Bricqs-Tenant": process.env.BRICQS_TENANT!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      participant_id: participantId,
      event_type: eventType,
      attributes,
      idempotency_key: idempotencyKey,
    }),
  });
}
app/api/profile/route.ts (when the user saves their profile)·ts
import { emitToBricqs } from "@/lib/bricqs";

export async function POST(request: Request) {
  const userId = await getCurrentUserId();
  const profile = await request.json();

  await saveProfile(userId, profile);

  // Forward to Bricqs. Idempotent: same user, same event, once.
  await emitToBricqs(
    userId,
    "profile_completed",
    { has_avatar: !!profile.avatar },
    `p_${userId}:profile_completed`
  );

  return Response.json({ ok: true });
}

Step 3: client

Render the challenge with one hook

The hook returns objective state, progress, and a completion callback. Polling and reconciliation are built in.

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

const STEP_CTAS: Record<string, { label: string; href: string }> = {
  complete_profile: { label: "Edit profile", href: "/account/profile" },
  first_purchase:   { label: "Browse the catalog", href: "/shop" },
  invite_a_friend:  { label: "Invite a friend", href: "/refer" },
};

export function OnboardingChallenge() {
  const {
    challenge,
    objectives,
    progressPercent,
    isCompleted,
    isLoading,
    onCompleted,
  } = useChallenge({ challengeId: "onboarding_v3" });

  onCompleted((event) => {
    window.location.href = `/welcome/done?reward=${event.reward.id}`;
  });

  if (isLoading) return null;
  if (isCompleted) return null;

  return (
    <aside className="rounded-2xl border bg-white p-6">
      <header className="flex items-baseline justify-between mb-3">
        <h2 className="font-bold text-lg">{challenge.title}</h2>
        <span className="text-sm text-slate-500">
          {progressPercent}% done · {challenge.daysRemaining} days left
        </span>
      </header>
      <ProgressBar percent={progressPercent} />
      <ol className="mt-4 space-y-2">
        {objectives.map((o) => {
          const cta = STEP_CTAS[o.id];
          return (
            <li
              key={o.id}
              className={`flex items-center gap-3 ${o.isComplete ? "opacity-60" : ""}`}
            >
              <span>{o.isComplete ? "✓" : "○"}</span>
              <span className="flex-1">{o.label}</span>
              {!o.isComplete && cta && (
                <a className="text-blue-600 text-sm" href={cta.href}>{cta.label}</a>
              )}
            </li>
          );
        })}
      </ol>
    </aside>
  );
}

Step 4: recovery

Drop-off recovery via webhook

Bricqs fires a webhook when a participant has been inactive for 72 hours or has not completed by 168 hours. Wire it to your ESP.

app/api/webhooks/bricqs/route.ts·ts
import crypto from "crypto";

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get("x-bricqs-signature");
  if (!verifySignature(body, signature)) {
    return new Response("Invalid signature", { status: 401 });
  }

  const event = JSON.parse(body);
  if (event.type === "challenge.drop_off_at_72h") {
    await sendRecoveryEmail(event.data.participant_id, {
      challenge: event.data.challenge_id,
      next_step: event.data.next_objective_label,
    });
  }
  return Response.json({ ok: true });
}

function verifySignature(body: string, signature: string | null) {
  if (!signature) return false;
  const expected = crypto
    .createHmac("sha256", process.env.BRICQS_WEBHOOK_SECRET!)
    .update(body)
    .digest("hex");
  return signature === expected;
}

HMAC verify before acting on the webhook. Anything else is exposing your ESP to spoofing.

Step 5: completion

The handoff moment

Auto-claim the reward

If the completion reward is a voucher, claim the code in the onCompleted handler so the user lands on a success screen with the code already in hand.

Route to the next thing

Onboarding done is a launchpad. Hand off to the first habit (a streak), the loyalty program, or the home dashboard with a 'mission complete' moment.

onCompleted handler·tsx
onCompleted(async (event) => {
  // 1. Claim the voucher server-side
  const res = await fetch("/api/bricqs/claim-reward", {
    method: "POST",
    body: JSON.stringify({ rewardId: event.reward.id }),
  });
  const { code } = await res.json();

  // 2. Show the celebration screen
  router.push(`/welcome/done?code=${code}`);
});

Pre-launch checklist

Before you ship

text
Configuration
[ ] Challenge created in admin (POST /admin/challenges)
[ ] Window length set to 14 days
[ ] Auto-enrol on signup enabled
[ ] Drop-off webhook URLs configured

Server
[ ] BRICQS_ADMIN_KEY set in production env (server-only)
[ ] BRICQS_WEBHOOK_SECRET set in production env
[ ] All five qualifying actions emit events with idempotency keys
[ ] Webhook handler verifies HMAC before acting

Client
[ ] BricqsProvider wraps the authenticated tree
[ ] Participant token minted server-side, refreshed at 80% TTL
[ ] OnboardingChallenge component rendered on home screen
[ ] Skeleton fallback during isLoading
[ ] Hidden when isCompleted is true

Rollout
[ ] Verified on a real test account end-to-end
[ ] First step completes in under 60 seconds for a fresh user
[ ] Recovery email lands in inbox at 72h test
[ ] Completion reward claims successfully on a fresh account

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.