Skip to main content

Streaks

Code First

Drive daily engagement with streak mechanics that reward consistent user activity.

Configurable Reset

Define how many days of inactivity trigger a reset

Grace Periods

Give users extra hours for timezone flexibility

Milestones

Celebrate achievements at key day counts

Streak Recovery

Optional paid recovery for broken streaks

How Streaks Work

Streaks track consecutive days of user activity. Each day a user performs the tracked action, their streak counter increments. If they miss a day (plus any grace period), the streak resets to zero.

Streak Timeline Example

1
Mon
2
Tue
3
Wed
-
Thu
0
Fri
1
Sat

User missed Thursday. With resetAfterDays: 1, the streak reset on Friday.

Configuration

Define streaks in your engagement config file. Each streak has its own reset rules and milestones:

engagement.config.ts
import { defineEngagement, streak } from '@sylphx/sdk/engagement'

export const engagement = defineEngagement({
  streaks: {
    // Daily login streak - most common pattern
    daily_login: streak({
      name: 'Daily Login',
      description: 'Log in every day to maintain your streak',
      resetAfterDays: 1,        // Reset after 1 day of inactivity
      gracePeriodHours: 4,      // 4-hour buffer for timezone issues
      milestones: [7, 30, 100, 365],
      recoverable: true,        // Allow streak recovery (paid feature)
      maxRecoveryDays: 3,       // Can only recover if missed <= 3 days
    }),

    // Weekly check-in - more lenient
    weekly_checkin: streak({
      name: 'Weekly Check-in',
      description: 'Check in at least once per week',
      resetAfterDays: 7,        // Reset after 7 days of inactivity
      gracePeriodHours: 24,     // Full day grace period
      milestones: [4, 12, 52],  // 1 month, 3 months, 1 year
    }),

    // Workout streak - strict, no grace period
    workout: streak({
      name: 'Workout Streak',
      description: 'Complete a workout every day',
      resetAfterDays: 1,
      gracePeriodHours: 0,      // No grace period - strict tracking
      milestones: [7, 14, 30, 90],
    }),
  },
})
PropertyTypeDescription
namerequiredstringDisplay name for the streak
descriptionstringUser-facing description of what maintains the streak
resetAfterDaysrequirednumberNumber of days of inactivity before the streak resets (typically 1)
gracePeriodHoursnumberExtra hours added to resetAfterDays for timezone flexibility (default: 0)
milestonesnumber[]Day counts that trigger milestone events (e.g., [7, 30, 100])
recoverablebooleanWhether users can pay to recover a broken streak (default: false)
maxRecoveryDaysnumberMaximum days after reset that recovery is allowed (default: 7)

Recording Activity

Record streak activity from either the client or server. The platform handles all the streak logic automatically.

'use client'

import { useStreak } from '@sylphx/sdk/react'

export function DailyLoginButton() {
  const {
    streak,           // Current streak data
    recordActivity,   // Function to record activity
    isLoading,        // Loading state
    error,            // Any error
  } = useStreak('daily_login')

  const handleLogin = async () => {
    const result = await recordActivity()

    if (result.milestone) {
      // User hit a milestone! Show celebration
      toast.success(`🎉 ${result.milestone} day streak!`)
    }
  }

  return (
    <button
      onClick={handleLogin}
      disabled={isLoading || streak?.recordedToday}
      className="btn btn-primary"
    >
      {streak?.recordedToday ? (
        <>Logged Today ({streak.current} days)</>
      ) : (
        <>Check In (Current: {streak?.current ?? 0} days)</>
      )}
    </button>
  )
}

Idempotent Recording

Recording activity multiple times on the same day is safe. The streak will only increment once per day, and recordedToday will be true after the first recording.

Grace Periods

Grace periods add flexibility for users in different timezones or with irregular schedules. The grace period extends the deadline for recording activity.

// With gracePeriodHours: 4 and resetAfterDays: 1

// User's last activity: Monday 11:00 PM UTC
// Without grace period: Must record by Tuesday 11:59 PM UTC
// With 4-hour grace:    Must record by Wednesday 3:59 AM UTC

// This helps users who:
// - Are in different timezones
// - Have varying schedules
// - Might miss midnight by a few hours

Grace Period Best Practices

  • 4-6 hours - Good for daily streaks, covers timezone differences
  • 12-24 hours - Very lenient, good for weekly activities
  • 0 hours - Strict tracking, use for competitive/fitness apps

Streak Recovery

Allow users to recover broken streaks as a premium feature. This is a powerful monetization and retention tool.

'use client'

import { useStreak } from '@sylphx/sdk/react'

export function StreakRecoveryModal({ onClose }) {
  const { streak, recoverStreak, isLoading } = useStreak('daily_login')

  // Only show if streak was recently broken and is recoverable
  if (!streak?.canRecover) return null

  return (
    <div className="modal">
      <h2>😢 Your streak was broken!</h2>
      <p>You had a {streak.lastStreak} day streak.</p>
      <p>Days missed: {streak.daysSinceReset}</p>

      <button
        onClick={async () => {
          const result = await recoverStreak()
          if (result.recovered) {
            toast.success(`Streak restored to ${result.current} days!`)
            onClose()
          }
        }}
        disabled={isLoading}
        className="btn btn-primary"
      >
        Recover Streak (Use 1 Recovery Token)
      </button>

      <button onClick={onClose} className="btn btn-ghost">
        Start Fresh
      </button>
    </div>
  )
}

Monetization Opportunity

Streak recovery tokens can be sold as in-app purchases or awarded for completing challenges. This creates urgency and value for maintaining streaks.

Milestones & Events

Milestones trigger when users reach specific day counts. Use these to celebrate achievements and drive continued engagement.

Handling Milestones
// In your streak config
milestones: [7, 30, 100, 365]

// When recording activity, check for milestones
const result = await platform.engagement.recordStreak({
  userId,
  streakId: 'daily_login',
})

if (result.milestone) {
  // Milestone reached! Take action:

  // 1. Send notification
  await sendNotification(userId, {
    title: '🎉 Milestone Reached!',
    body: `You've logged in for ${result.milestone} days straight!`,
  })

  // 2. Award bonus points
  await platform.engagement.submitScore({
    userId,
    leaderboardId: 'monthly_points',
    score: result.milestone * 10,  // 70, 300, 1000, 3650 points
  })

  // 3. Unlock achievement
  const achievementMap = {
    7: 'week_warrior',
    30: 'monthly_master',
    100: 'centurion',
    365: 'year_champion',
  }
  await platform.engagement.unlockAchievement({
    userId,
    achievementId: achievementMap[result.milestone],
  })

  // 4. Track analytics
  analytics.track('streak_milestone', {
    streakId: 'daily_login',
    milestone: result.milestone,
  })
}

Automatic Triggers

Link streaks to analytics events for automatic recording without explicit API calls:

engagement.config.ts
export const engagement = defineEngagement({
  streaks: {
    daily_active: streak({
      name: 'Daily Active',
      description: 'Use the app every day',
      resetAfterDays: 1,
      gracePeriodHours: 4,
      milestones: [7, 30, 100],

      // Auto-record when ANY of these events fire
      triggers: [
        'page_view',
        'button_click',
        'form_submit',
        'feature_used',
      ],
    }),

    workout_streak: streak({
      name: 'Workout Streak',
      description: 'Complete a workout every day',
      resetAfterDays: 1,

      // Only this specific event triggers
      triggers: ['workout_completed'],
    }),
  },
})

// Now when you track these events via analytics...
analytics.track('workout_completed', { duration: 45 })

// ...the streak is automatically recorded!
// No need to call recordStreak() explicitly.

Trigger Considerations

Be careful with broad triggers like page_view. They make streaks very easy to maintain, which may reduce their motivational value.

Displaying Streaks

Pre-built components for common streak display patterns:

import {
  StreakBadge,      // Compact flame icon with count
  StreakCard,       // Full card with progress to milestone
  StreakCalendar,   // Monthly calendar view of activity
} from '@sylphx/sdk/react'

// Simple badge in header
<StreakBadge streakId="daily_login" />
// Renders: 🔥 7

// Full card with milestone progress
<StreakCard
  streakId="daily_login"
  showMilestoneProgress  // Progress bar to next milestone
  showBestStreak         // Display all-time best
  onMilestone={(m) => celebrate(m)}
/>

// Calendar view showing active days
<StreakCalendar
  streakId="daily_login"
  month={new Date()}
  highlightMilestones
/>