<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Charlie Brinicombe</title>
    <description>The latest articles on Forem by Charlie Brinicombe (@charlie_brinicombe).</description>
    <link>https://forem.com/charlie_brinicombe</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3054910%2F517c2750-346b-4a1e-974a-ddef5ae24952.jpg</url>
      <title>Forem: Charlie Brinicombe</title>
      <link>https://forem.com/charlie_brinicombe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/charlie_brinicombe"/>
    <language>en</language>
    <item>
      <title>How We Built SaaS Calculators in Next.js (And Kept Them Shareable)</title>
      <dc:creator>Charlie Brinicombe</dc:creator>
      <pubDate>Wed, 25 Mar 2026 15:51:13 +0000</pubDate>
      <link>https://forem.com/charlie_brinicombe/how-we-built-saas-calculators-in-nextjs-and-kept-them-shareable-66o</link>
      <guid>https://forem.com/charlie_brinicombe/how-we-built-saas-calculators-in-nextjs-and-kept-them-shareable-66o</guid>
      <description>&lt;p&gt;When we built our &lt;a href="https://trophy.so/calculators" rel="noopener noreferrer"&gt;calculator suite&lt;/a&gt;, we wanted more than "a form that outputs a number."&lt;/p&gt;

&lt;p&gt;We wanted calculators that were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fast and SEO-friendly,&lt;/li&gt;
&lt;li&gt;easy to extend,&lt;/li&gt;
&lt;li&gt;mathematically auditable,&lt;/li&gt;
&lt;li&gt;and shareable via URL.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post breaks down the architecture, design patterns, and trade-offs behind that implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Motivation (and a bit about Trophy)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://trophy.so" rel="noopener noreferrer"&gt;Trophy&lt;/a&gt; is a product aimed at helping teams make better decisions about growth and retention. In our space, the same few questions come up constantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What does our churn imply about retention over time?&lt;/li&gt;
&lt;li&gt;If we reduce churn, what’s the &lt;em&gt;revenue impact&lt;/em&gt; over the next 6–12 months?&lt;/li&gt;
&lt;li&gt;Given ARPU and churn, what’s a reasonable LTV and customer lifespan?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We could have answered those with spreadsheets, PDFs, or one-off blog posts but those formats don’t travel well inside a team. We wanted something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;turns “back-of-napkin” math into an interactive tool&lt;/strong&gt;,
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;makes assumptions explicit&lt;/strong&gt;,
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;produces a link you can drop into Slack/Notion&lt;/strong&gt;, and
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;holds up technically&lt;/strong&gt; (fast load, predictable state).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s why we built calculators as a first-class part of the web app: not just for lead-gen, but as a reusable, composable surface we can iterate on as the product evolves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Anatomy of a Calculator Page (and why each section exists)
&lt;/h2&gt;

&lt;p&gt;One thing we learned quickly: the calculation itself is only a small part of the user journey.&lt;/p&gt;

&lt;p&gt;Each page section has a distinct job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Header + description: establishes context fast ("what this calculator answers") so users know they’re in the right place before entering data.&lt;/li&gt;
&lt;li&gt;Formula block: adds transparency and trust, especially for technical readers who want to validate assumptions before using outputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftv5oyxfkuaox1f6pvd1k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftv5oyxfkuaox1f6pvd1k.png" alt="Header and formula section" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input controls: collect the minimum viable assumptions needed to compute a meaningful result without overwhelming the user.&lt;/li&gt;
&lt;li&gt;Primary result card(s): surface the key answer immediately (e.g. churn, retention, LTV), with lightweight interpretive context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9pqqmoyyw7mnc1ft99ey.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9pqqmoyyw7mnc1ft99ey.png" alt="Primary result section" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Impact chart section: translates one-off outputs into a forward-looking narrative ("what changes over 3, 6, 12 months"), which is where decision-making usually happens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqps4xwveigeo9rkxhyy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqps4xwveigeo9rkxhyy.png" alt="Revenue impact section" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Share section: turns a result into a portable artifact via URL, so teams can discuss the same scenario in Slack/Notion/email.&lt;/li&gt;
&lt;li&gt;Related calculators: supports natural next questions (e.g. from churn to LTV), increasing usefulness and reducing dead-ends.&lt;/li&gt;
&lt;li&gt;FAQ + CTA: FAQ handles objections and clarification; CTA gives users a clear next step after insight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, this structure helped us balance three goals simultaneously: educational content, interactive analysis, and team communication.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack at a Glance
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js App Router&lt;/strong&gt; for server/client component boundaries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React + TypeScript&lt;/strong&gt; for predictable UI and typed state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS&lt;/strong&gt; for consistent composable styling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recharts&lt;/strong&gt; for chart rendering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL query params&lt;/strong&gt; as the source of truth for shareable results&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  1) Server-First Page Composition
&lt;/h2&gt;

&lt;p&gt;A key architectural choice: keep route pages as &lt;strong&gt;server components&lt;/strong&gt;, and pass &lt;code&gt;searchParams&lt;/code&gt; down.&lt;/p&gt;

&lt;p&gt;That gives us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;better SSR/SEO behavior,&lt;/li&gt;
&lt;li&gt;cleaner hydration boundaries,&lt;/li&gt;
&lt;li&gt;no &lt;code&gt;useSearchParams()&lt;/code&gt; at the page level (which can force Suspense/client boundaries).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example page composition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Calculator&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./calculator&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ChurnImpactChart&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./churn-impact-chart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CalculatorPage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createCalculatorMetadata&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/components/calculators&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createCalculatorMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ChurnRateCalculatorPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CalculatorPage&lt;/span&gt;
      &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;initialSearchParams&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;calculator&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Calculator&lt;/span&gt; &lt;span class="na"&gt;initialSearchParams&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;impactChart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ChurnImpactChart&lt;/span&gt; &lt;span class="na"&gt;initialSearchParams&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a clean "orchestration layer" pattern: the page wires dependencies together, while feature components do domain work.&lt;/p&gt;




&lt;h2&gt;
  
  
  2) URL-Driven State via a Dedicated Hook
&lt;/h2&gt;

&lt;p&gt;Instead of each calculator calling &lt;code&gt;useSearchParams()&lt;/code&gt; directly, we introduced one shared adapter hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useCalculatorStateFromParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;initialSearchParams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SearchParamsInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;UseCalculatorStateReturn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePathname&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;initialSearchParams&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parseParamsToState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;setState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CalculatorState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;updates&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;paramsToSearchString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;scroll&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setState&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this pattern works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single source of truth&lt;/strong&gt; for query parsing/serialization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No duplicated URL sync logic&lt;/strong&gt; across calculators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share-by-default UX&lt;/strong&gt;: every calculated result can be copied as a link.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is effectively an adapter around Next routing primitives with a calculator-focused API.&lt;/p&gt;




&lt;h2&gt;
  
  
  3) Config-Driven Page Metadata and Content
&lt;/h2&gt;

&lt;p&gt;We model each calculator with a typed &lt;code&gt;CalculatorConfig&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CalculatorConfig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nl"&gt;formula&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;intro&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nl"&gt;calculatorSectionTitle&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;ctaTitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
    &lt;span class="nx"&gt;ctaDescription&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then metadata generation becomes deterministic and reusable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createCalculatorMetadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CalculatorConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Metadata&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alternates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://trophy.so/calculators/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduces drift between SEO tags and on-page content, while making it easier to add new calculators safely.&lt;/p&gt;




&lt;h2&gt;
  
  
  4) Composition Over Monoliths
&lt;/h2&gt;

&lt;p&gt;The shared shell (&lt;code&gt;CalculatorPage&lt;/code&gt;) receives three key inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;config&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;calculator&lt;/code&gt; node&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;impactChart&lt;/code&gt; node
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CalculatorPage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;initialSearchParams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;calculator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;impactChart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;Props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CalculatorLayout&lt;/span&gt; &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CalculatorShell&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;calculator&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;CalculatorShell&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;impactChart&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;CalculatorShareSection&lt;/span&gt;
        &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;initialSearchParams&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;initialSearchParams&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;hideUntilCalculated&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;CalculatorLayout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a pragmatic slot-based composition pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The layout remains consistent across calculators.&lt;/li&gt;
&lt;li&gt;Each domain calculator can evolve independently.&lt;/li&gt;
&lt;li&gt;Shared behavior (share section, formula, related calculators, CTA) stays centralized.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5) Domain Math Lives in Pure Functions
&lt;/h2&gt;

&lt;p&gt;UI is stateful and interactive; math should be deterministic and testable.&lt;/p&gt;

&lt;p&gt;So the formulas sit in standalone utility modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateChurnDecayData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;startingUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;churnPercent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maxDays&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clamped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;churnPercent&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;survivalPerPeriod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;clamped&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;clamped&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;period&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;maxDays&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;periodsElapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;period&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;survival&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;survivalPerPeriod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;periodsElapsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;points&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startingUsers&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;survival&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;points&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Benefits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;easier unit testing,&lt;/li&gt;
&lt;li&gt;less coupling to React render cycles,&lt;/li&gt;
&lt;li&gt;clearer auditing for stakeholders who care about formula correctness.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6) Two-Layer Validation Strategy
&lt;/h2&gt;

&lt;p&gt;Each calculator validates in two places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Realtime input validity&lt;/strong&gt; (&lt;code&gt;isValid&lt;/code&gt;) to disable calculate actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action-time validation&lt;/strong&gt; inside &lt;code&gt;handleCalculate&lt;/code&gt; for safety.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isValid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startNum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;remainingNum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;startNum&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;remainingNum&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;remainingNum&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;startNum&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleCalculate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isValid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;shareReady&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;churn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateChurn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;setResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;churn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;startingUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;remainingUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;churnRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;churn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;shareReady&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids accidental invalid URL state and keeps result cards/charts consistent.&lt;/p&gt;




&lt;h2&gt;
  
  
  7) Performance and UX Choices
&lt;/h2&gt;

&lt;p&gt;Some implementation details that made a big difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;useMemo&lt;/code&gt; for chart data and derived values (half-life, domains).&lt;/li&gt;
&lt;li&gt;route updates via &lt;code&gt;router.replace&lt;/code&gt; with &lt;code&gt;{ scroll: false }&lt;/code&gt; to avoid janky navigation.&lt;/li&gt;
&lt;li&gt;fixed, typed period options (&lt;code&gt;1 | 7 | 30&lt;/code&gt;) to simplify math and UI consistency.&lt;/li&gt;
&lt;li&gt;responsive chart containers (&lt;code&gt;overflow-x-auto&lt;/code&gt; + minimum widths) for small screens.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  8) Design Patterns We Reused Across Calculators
&lt;/h2&gt;

&lt;p&gt;The biggest win was pattern consistency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Template + Slots&lt;/strong&gt;: one page shell, pluggable calculator + chart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adapter Hook&lt;/strong&gt;: one URL-state bridge for all calculators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure Domain Modules&lt;/strong&gt;: math separated from rendering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config-Driven Metadata&lt;/strong&gt;: SEO/content generated from typed config.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feature Flags in Query&lt;/strong&gt;: share readiness and parameterized results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These patterns made adding new calculators substantially faster after the first one.&lt;/p&gt;




&lt;h2&gt;
  
  
  9) What We’d Improve Next
&lt;/h2&gt;

&lt;p&gt;If we were extending this further, we’d likely add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;schema-based query parsing/validation (e.g. zod/valibot) for stricter runtime guarantees,&lt;/li&gt;
&lt;li&gt;analytics events at "calculate" and "share" boundaries,&lt;/li&gt;
&lt;li&gt;optional server-side persistence for comparison history,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;benchmarking&lt;/strong&gt; (e.g. “you’re in the 60th percentile for churn in fitness apps”) with clear cohort definitions and data provenance,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;downloadable reports&lt;/strong&gt; (PDF/CSV) that bundle inputs, assumptions, charts, and a narrative summary,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shareable team reports&lt;/strong&gt; (invite colleagues, comments, pinned scenarios) so calculators become collaborative artifacts—not just individual tools,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;scenario management&lt;/strong&gt; (save multiple parameter sets, compare side-by-side, and track deltas over time),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;permissions + workspace context&lt;/strong&gt; so sharing can be public links, private links, or org-only depending on the audience.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The core idea is simple: &lt;strong&gt;treat calculators as a product surface, not a throwaway widget&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By combining server-first composition, URL-based state, and pure domain math, we ended up with calculators that are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;maintainable,&lt;/li&gt;
&lt;li&gt;predictable,&lt;/li&gt;
&lt;li&gt;and genuinely useful to share in real workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re building anything similar, start with these two constraints:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every result should be reproducible from the URL.&lt;/li&gt;
&lt;li&gt;Every formula should live outside the component tree.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else gets easier from there.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>startup</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How to Build an Achievements Feature</title>
      <dc:creator>Charlie Brinicombe</dc:creator>
      <pubDate>Fri, 17 Oct 2025 15:21:22 +0000</pubDate>
      <link>https://forem.com/charlie_brinicombe/how-to-build-an-achievements-feature-5dd2</link>
      <guid>https://forem.com/charlie_brinicombe/how-to-build-an-achievements-feature-5dd2</guid>
      <description>&lt;p&gt;You want achievements. Track user progress. Award badges for milestones. Display completion status. Seems straightforward. Three weeks later, you're debugging why some users didn't get achievements they qualified for, handling backdating for new achievements, and optimizing queries that check completion criteria for every user action.&lt;/p&gt;

&lt;p&gt;Achievement systems appear simple until you implement them at scale. The core concept (recognize milestone completion) hides complexity. When do you check if achievements are complete? How do you handle users who qualified before the achievement existed? What about achievements that require checking historical data across multiple metrics?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://trophy.so/?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;Trophy&lt;/a&gt; handles &lt;a href="https://trophy.so/features/achievements?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;achievement systems&lt;/a&gt; including completion logic, progress tracking, and backdating. The &lt;a href="https://docs.trophy.so/guides/how-to-build-an-achievements-feature?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;complete implementation guide&lt;/a&gt; walks through the full process. Integration takes 1 day to 1 week. But understanding what building from scratch involves helps you make informed build versus buy decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Technical challenges in achievement system implementation&lt;/li&gt;
&lt;li&gt;Completion checking patterns that scale&lt;/li&gt;
&lt;li&gt;Backdating strategies for fairness&lt;/li&gt;
&lt;li&gt;Integration examples with Trophy's API&lt;/li&gt;
&lt;li&gt;Build versus buy considerations for achievement features&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Technical Reality
&lt;/h2&gt;

&lt;p&gt;Before building achievement systems, understand the problems beyond simple milestone tracking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Completion checking logic&lt;/strong&gt; needs efficiency at scale. Checking every achievement on every user action kills performance. You need smart triggering that only checks relevant achievements. Building this trigger system takes time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progress tracking&lt;/strong&gt; for multi-step achievements adds complexity. "Complete 100 tasks" needs counting. Showing users "45/100" requires storing partial progress. Updating this efficiently as users act requires careful database design.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.trophy.so/platform/achievements?ref=trophy.ghost.io#backdating-achievements" rel="noopener noreferrer"&gt;&lt;strong&gt;Backdating&lt;/strong&gt;&lt;/a&gt; ensures fairness when you add new achievements. Users who qualified before the achievement existed should complete it automatically. Scanning historical data for every user takes time. Getting this right without overloading your database is tricky.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Badge hosting and delivery&lt;/strong&gt; seems minor but adds infrastructure. Where do badge images live? How do you serve them efficiently? What formats work across platforms? These details accumulate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rarity calculations&lt;/strong&gt; show how many users completed each achievement. This requires counting completions across your entire user base and keeping these counts updated as more users complete achievements.&lt;/p&gt;

&lt;p&gt;Building production-ready achievement systems typically takes 3-5 weeks including completion logic, progress tracking, and backdating. Trophy's infrastructure handles these problems, reducing implementation to integration work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Patterns
&lt;/h2&gt;

&lt;p&gt;If building in-house, these patterns avoid common mistakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event-based checking&lt;/strong&gt; triggers achievement evaluation only when relevant events occur. Store achievement triggers. When events arrive, check only achievements that could complete based on that event type. This scales better than checking all achievements on every action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incremental progress updates&lt;/strong&gt; maintain partial completion state. Store "tasks completed: 45" instead of recalculating from history on every check. Update incrementally as users act. Trophy uses this pattern for efficient progress tracking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async backdating&lt;/strong&gt; processes achievement qualification in background jobs. When you create an achievement, queue a job that scans historical data and awards to qualified users. Don't block achievement creation on backdating completion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Denormalized completion state&lt;/strong&gt; stores which users completed which achievements in fast-access storage. Recompute from history only when needed. Trophy maintains completion state with millisecond query latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger mapping&lt;/strong&gt; links achievement requirements to specific events. Achievement requires metric X reaching value Y, so only check it when metric X events arrive. Trophy's configuration system includes this trigger mapping automatically.&lt;/p&gt;

&lt;p&gt;Trophy implements these patterns. You configure achievement criteria. Trophy handles the infrastructure complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Estimate
&lt;/h2&gt;

&lt;p&gt;Here's realistic timeline for building achievements in-house:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1: Basic implementation.&lt;/strong&gt; Create achievements. Track completions. Display badges. Works in development with simple achievements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 2: Progress tracking.&lt;/strong&gt; Store partial progress for multi-step achievements. Update progress efficiently. Show users their advancement toward incomplete achievements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 3: Completion logic.&lt;/strong&gt; Build trigger system that checks relevant achievements on user actions. Optimize to avoid checking irrelevant achievements. Handle achievement criteria complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 4: Backdating and edge cases.&lt;/strong&gt; Implement fair backdating for new achievements. Handle concurrent completion attempts. Test with realistic data volumes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ongoing: Maintenance and expansion.&lt;/strong&gt; New achievements need adding regularly. Badge management continues. Performance tuning as usage scales.&lt;/p&gt;

&lt;p&gt;That's 4+ weeks of engineering time plus ongoing maintenance. Trophy's infrastructure eliminates this timeline, reducing implementation to 1 day to 1 week of integration work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building with Trophy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://trophy.so/?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;Trophy's&lt;/a&gt; integration is faster because achievement infrastructure already exists. Here's what implementation looks like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create Achievements
&lt;/h3&gt;

&lt;p&gt;In Trophy's dashboard, create achievements with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name and description&lt;/li&gt;
&lt;li&gt;Badge image (Trophy hosts this for you)&lt;/li&gt;
&lt;li&gt;Trigger type (metric-based, API-based, or streak-based)&lt;/li&gt;
&lt;li&gt;Completion criteria&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For metric achievements, specify which metric and what threshold completes the achievement. Trophy automatically tracks progress and awards completion when users reach the threshold.&lt;/p&gt;

&lt;p&gt;Managing achievements online means future additions or changes happen in the dashboard and not in your codebase, preventing back and forth changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Track User Actions
&lt;/h3&gt;

&lt;p&gt;Send events to Trophy when users perform achievement-relevant actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { TrophyApiClient } from '@trophyso/node';

const trophy = new TrophyApiClient(process.env.TROPHY_API_KEY);

// When user completes a task
const response = await trophy.metrics.event('tasks_completed', {
  user: {
    id: 'user-123'
  },
  value: 1
});

// Check if user completed any achievements
if (response.achievements &amp;amp;&amp;amp; response.achievements.length &amp;gt; 0) {
  response.achievements.forEach(achievement =&amp;gt; {
    console.log(`Unlocked: ${achievement.name}`);
    console.log(`Badge: ${achievement.badgeUrl}`);
    showAchievementNotification(achievement);
  });
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy processes events and automatically checks if any achievements should complete. The response includes newly completed achievements for immediate user feedback.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Display User Achievements
&lt;/h3&gt;

&lt;p&gt;Fetch achievements a user has completed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Get user's completed achievements
const achievements = await trophy.users.achievements('user-123', {
  includeIncomplete: 'false' // Only show completed
});

achievements.forEach(achievement =&amp;gt; {
  console.log({
    name: achievement.name,
    description: achievement.description,
    badgeUrl: achievement.badgeUrl,
    completedAt: achievement.achievedAt,
    rarity: achievement.rarity // Percentage of users who completed it
  });
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy returns achievement data including badges, completion timestamps, and rarity statistics. The &lt;a href="https://docs.trophy.so/api-reference/endpoints/users/get-a-users-completed-achievements?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;achievements API documentation&lt;/a&gt; covers all available fields.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Show Achievement Progress
&lt;/h3&gt;

&lt;p&gt;For incomplete achievements, show progress toward completion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Get all achievements including incomplete ones with progress
const allAchievements = await trophy.users.achievements('user-123', {
  includeIncomplete: 'true'
});

allAchievements.forEach(achievement =&amp;gt; {
  if (achievement.achievedAt) {
    // Completed
    console.log(`✓ ${achievement.name}`);
  } else {
    // Incomplete - show progress if available
    if (achievement.progress) {
      const percent = (achievement.progress.current / achievement.progress.target) * 100;
      console.log(`${achievement.name}: ${percent}% complete`);
    }
  }
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy tracks progress for metric-based achievements automatically. Users see how close they are to completion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Complete API Achievements
&lt;/h3&gt;

&lt;p&gt;For achievements that can't be automatically tracked via metrics (completing onboarding, linking accounts, etc.), use the complete achievement API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// When user completes onboarding
await trophy.achievements.complete('user-123', 'onboarding_complete');

// Trophy marks it as complete and returns achievement data
const result = await trophy.achievements.complete('user-123', 'onboarding_complete');

if (result.achievement) {
  console.log(`Completed: ${result.achievement.name}`);
  showAchievementNotification(result.achievement);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you control over when achievements complete for events Trophy can't track automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Achievement Types and Strategies
&lt;/h2&gt;

&lt;p&gt;Different achievement structures serve different product goals. Understanding &lt;a href="https://trophy.so/blog/when-your-app-needs-an-achievements-feature?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;when your app needs achievements&lt;/a&gt; helps you design effective systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metric achievements&lt;/strong&gt; track cumulative actions. "Complete 100 tasks" or "View 50 lessons." These recognize consistent usage and progress toward mastery. Trophy's metric system tracks these automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streak achievements&lt;/strong&gt; recognize consistency. "Maintain a 30-day streak" celebrates sustained engagement. Trophy's streak tracking makes these simple to implement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API achievements&lt;/strong&gt; recognize specific one-time events. "Complete onboarding" or "Link social account." You trigger these manually when appropriate events occur.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tiered achievements&lt;/strong&gt; create progression. Bronze (10 tasks), silver (50 tasks), gold (100 tasks). Users see clear advancement path. Trophy's metric thresholds support tiered structures naturally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hidden achievements&lt;/strong&gt; surprise users through discovery. Don't show them until completed.&lt;/p&gt;

&lt;p&gt;Mix achievement types to serve different user motivations and engagement patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Achievement Structures
&lt;/h2&gt;

&lt;p&gt;Effective achievement design requires understanding user psychology and product goals. &lt;a href="https://trophy.so/blog/designing-achievements-for-optimal-user-engagement?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;Designing achievements for optimal engagement&lt;/a&gt; explores strategic frameworks for achievement creation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accessibility balance&lt;/strong&gt; matters. Some achievements should be easy (first task completed) for quick wins. Others should be challenging (1,000 tasks completed) for long-term goals. Trophy's analytics show completion rates helping you tune difficulty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progressive revelation&lt;/strong&gt; prevents overwhelming new users. Show basic achievements upfront. Reveal advanced achievements as users progress. Trophy's achievement system supports controlling visibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meaningful milestones&lt;/strong&gt; align with user goals. Achievements should recognize progress users care about, not arbitrary metrics. Trophy's flexible metric system lets you track what matters for your product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rarity indicators&lt;/strong&gt; show prestige. "Only 5% of users have completed this" motivates completion. Trophy automatically calculates and displays rarity statistics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backdating Logic
&lt;/h2&gt;

&lt;p&gt;When you create new achievements, users who already qualified should complete them automatically. Trophy handles this through backdating.&lt;/p&gt;

&lt;p&gt;When you activate an achievement in Trophy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Trophy scans historical data for users who meet completion criteria&lt;/li&gt;
&lt;li&gt;Awards the achievement to qualified users with original completion timestamp&lt;/li&gt;
&lt;li&gt;Updates completion counts and rarity statistics&lt;/li&gt;
&lt;li&gt;Processes in background without blocking achievement activation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This ensures fairness without requiring manual intervention. Users who completed 100 tasks before that achievement existed get credit when you launch it.&lt;/p&gt;

&lt;p&gt;Your code doesn't change. Trophy handles backdating automatically based on achievement configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notification Strategy
&lt;/h2&gt;

&lt;p&gt;Achievement completions need communication without overwhelming users. Trophy's &lt;a href="https://docs.trophy.so/platform/emails?ref=trophy.ghost.io#achievement-emails" rel="noopener noreferrer"&gt;email system&lt;/a&gt; handles notification timing.&lt;/p&gt;

&lt;p&gt;Configure achievement emails in Trophy's dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When to send (immediately, batched, specific times)&lt;/li&gt;
&lt;li&gt;Email content and design&lt;/li&gt;
&lt;li&gt;Which achievements trigger emails&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trophy sends notifications automatically when users complete achievements. No manual email logic needed in your code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Trophy handles notifications automatically
// Your code just tracks events
await trophy.metrics.event('tasks_completed', {
  user: { id: 'user-123' },
  value: 1
});

// If user completes achievement, Trophy sends configured emails

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;Trophy's infrastructure is built for scale, but your integration patterns affect performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check achievements server-side only&lt;/strong&gt;. Don't query achievement status on every page load. Cache achievement data and refresh periodically or after user actions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache completion state&lt;/strong&gt; for display-heavy scenarios. Achievement lists don't need real-time accuracy. Cache for 5-10 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const cache = new Map();

async function getCachedAchievements(userId: string) {
  const cached = cache.get(userId);
  if (cached &amp;amp;&amp;amp; Date.now() - cached.timestamp &amp;lt; 300000) {
    return cached.data;
  }

  const fresh = await trophy.users.achievements(userId);
  cache.set(userId, { data: fresh, timestamp: Date.now() });
  return fresh;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy handles backend scaling. These client-side patterns optimize your application's performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analytics and Tuning
&lt;/h2&gt;

&lt;p&gt;Trophy provides analytics for achievement system health.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Completion rates&lt;/strong&gt; show whether achievements are appropriately difficult. Trophy's dashboard shows what percentage of users complete each achievement. Aim for variety: some easy (60%+), some moderate (30-60%), some difficult (5-30%).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Completion velocity&lt;/strong&gt; reveals how long achievements take. If most users complete an achievement within days of starting, it might be too easy. If it takes months on average, consider whether it's appropriately challenging or frustratingly difficult.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rarity distribution&lt;/strong&gt; shows whether you have enough aspirational achievements. If all achievements have 40-60% completion, you might need harder goals for engaged users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Abandonment patterns&lt;/strong&gt; indicate where users give up. If users progress halfway toward an achievement then stop, the difficulty curve might be wrong or the achievement might not be compelling.&lt;/p&gt;

&lt;p&gt;Use these insights to refine achievement thresholds and create new achievements that fill gaps. Trophy's dashboard configuration makes adjustments quick without code changes.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>mobile</category>
      <category>gamification</category>
    </item>
    <item>
      <title>How to Build an Energy Feature</title>
      <dc:creator>Charlie Brinicombe</dc:creator>
      <pubDate>Fri, 17 Oct 2025 11:47:21 +0000</pubDate>
      <link>https://forem.com/charlie_brinicombe/how-to-build-an-energy-feature-73i</link>
      <guid>https://forem.com/charlie_brinicombe/how-to-build-an-energy-feature-73i</guid>
      <description>&lt;p&gt;Your want to add an energy feature to your platform. Users consume energy for actions. Energy regenerates over time. Cap it at a maximum. Seems like simple arithmetic. Three weeks later, you're debugging regeneration timing, handling edge cases around maximum caps, and dealing with race conditions when users perform rapid actions.&lt;/p&gt;

&lt;p&gt;Energy systems appear straightforward until you implement them at scale. The core concept (limited resource that regenerates) hides complexity. When does regeneration happen? What if users act while at zero energy? How do you handle time zones for regeneration timing? What prevents users from gaming the system?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://trophy.so/?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;Trophy&lt;/a&gt; handles &lt;a href="https://trophy.so/features/points?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;energy systems&lt;/a&gt; including regeneration, consumption, and metering. The &lt;a href="https://docs.trophy.so/guides/how-to-build-an-energy-feature?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;complete implementation guide&lt;/a&gt; walks through the full process. Integration takes 1 day to 1 week. But understanding what building from scratch involves helps you make informed build versus buy decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Technical challenges in energy system implementation&lt;/li&gt;
&lt;li&gt;Regeneration patterns and timing considerations&lt;/li&gt;
&lt;li&gt;Consumption triggers and usage metering&lt;/li&gt;
&lt;li&gt;Integration examples with Trophy's API&lt;/li&gt;
&lt;li&gt;Build versus buy considerations for energy features&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Technical Reality
&lt;/h2&gt;

&lt;p&gt;Before building energy systems, understand the problems beyond simple counters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time-based regeneration&lt;/strong&gt; needs careful implementation. Energy regenerates hourly or daily, but when exactly? Server-scheduled jobs create spiky regeneration patterns. Per-user timers scale poorly. Event-driven regeneration requires complex triggering. Getting this right takes time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maximum cap handling&lt;/strong&gt; prevents infinite accumulation. Users hit the cap and stop regenerating. But what if regeneration happens while they're at cap? Do they lose potential energy? How do you communicate this clearly? Edge cases around caps create complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumption timing&lt;/strong&gt; affects user experience. Immediate energy deduction prevents spam but blocks users at zero. Deferred consumption allows actions but creates debt states. Partial consumption for partial actions adds more complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Race conditions&lt;/strong&gt; happen when users perform rapid actions. Two actions at 5 energy each when user has 8 energy total. Which succeeds? Do they both check balance simultaneously and both succeed, overdrawing the account? Proper locking prevents this but adds latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time zone handling&lt;/strong&gt; for regeneration schedules requires per-user logic. Daily regeneration at midnight means different times for different users. Trophy handles &lt;a href="https://trophy.so/blog/handling-time-zones-gamification?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;time zones automatically&lt;/a&gt;, but building this yourself means complex timezone math.&lt;/p&gt;

&lt;p&gt;Building production-ready energy systems typically takes 3-6 months including regeneration logic, consumption triggers, and edge cases. Trophy's infrastructure handles these problems, reducing implementation to integration work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Patterns
&lt;/h2&gt;

&lt;p&gt;If building in-house, these patterns avoid common mistakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event-based consumption&lt;/strong&gt; separates action tracking from energy deduction. Store user actions as events. Process energy changes asynchronously. This prevents blocking user actions on energy calculations but requires careful consistency management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scheduled regeneration jobs&lt;/strong&gt; grant energy at fixed intervals. Cron jobs that run hourly or daily and grant energy to eligible users. This pattern is simple but creates load spikes. Trophy uses distributed scheduling that spreads regeneration load evenly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lazy regeneration&lt;/strong&gt; computes energy only when queried. Store last check time. When user requests their balance, calculate regeneration since last check and update. This avoids scheduled jobs but complicates balance queries. Trophy uses hybrid approach with cached totals and lazy updates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimistic locking&lt;/strong&gt; prevents race conditions. Check energy balance, attempt consumption, verify balance didn't change during operation. If it changed, retry. This ensures consistency without blocking concurrent operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Denormalized balances&lt;/strong&gt; for query performance. Maintain current energy in fast storage (Redis). Recompute from event history only when needed. Trophy caches current balances with millisecond query latency.&lt;/p&gt;

&lt;p&gt;Trophy implements these patterns. You configure regeneration rules and consumption triggers. Trophy handles the infrastructure complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Estimate
&lt;/h2&gt;

&lt;p&gt;Here's realistic timeline for building energy systems in-house:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1-2: Basic implementation.&lt;/strong&gt; Track energy balance. Deduct for actions. Display totals. Works in development with simple cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 3-4: Regeneration logic.&lt;/strong&gt; Implement time-based regeneration with scheduled jobs. Handle maximum caps. Make regeneration work across user sessions and time zones. Test edge cases around timing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 5-6: Consumption triggers.&lt;/strong&gt; Build system for deducting energy based on different actions. Implement variable consumption amounts. Handle insufficient energy cases gracefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 7: Concurrency and edge cases.&lt;/strong&gt; Prevent race conditions. Handle rapid actions correctly. Test regeneration at scale. Fix performance issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ongoing: Maintenance and tuning.&lt;/strong&gt; As usage patterns change, regeneration rates need adjustment. New actions need consumption rules. This work continues indefinitely.&lt;/p&gt;

&lt;p&gt;That's 7+ weeks of engineering time plus ongoing maintenance. Trophy's infrastructure eliminates this timeline, reducing implementation to 1 day to 1 week of integration work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building with Trophy
&lt;/h2&gt;

&lt;p&gt;Trophy's integration is faster because energy infrastructure already exists. Here's what implementation looks like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create Energy System
&lt;/h3&gt;

&lt;p&gt;In Trophy's dashboard, create a points system called "Energy" (or your preferred name). Configure the maximum energy cap users can have. Trophy supports any maximum up to your requirements.&lt;/p&gt;

&lt;p&gt;Energy systems are just points systems with specific regeneration and consumption rules. Trophy's flexible points infrastructure supports both XP-style accumulation and energy-style &lt;a href="https://docs.trophy.so/platform/points?ref=trophy.ghost.io#metering" rel="noopener noreferrer"&gt;metering&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Configure Regeneration Triggers
&lt;/h3&gt;

&lt;p&gt;Set up how users gain energy over time. In Trophy's dashboard, create time-based triggers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hourly regeneration&lt;/strong&gt; : Grant X energy every N hours&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Award 1 energy every hour&lt;/li&gt;
&lt;li&gt;Award 10 energy every 6 hours&lt;/li&gt;
&lt;li&gt;Maximum caps prevent energy exceeding limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Daily regeneration&lt;/strong&gt; : Grant energy once per day&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Award 20 energy at midnight user time&lt;/li&gt;
&lt;li&gt;Award 50 energy every 24 hours&lt;/li&gt;
&lt;li&gt;Trophy handles timezone timing automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trophy's trigger system grants energy automatically based on your configuration. Users receive energy without manual processing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure Consumption Triggers
&lt;/h3&gt;

&lt;p&gt;Set up how users spend energy. Create negative-value triggers for actions that consume energy:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Action-based consumption&lt;/strong&gt; : Deduct energy when users perform specific actions&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deduct 1 energy per lesson viewed&lt;/li&gt;
&lt;li&gt;Deduct 5 energy per workout started&lt;/li&gt;
&lt;li&gt;Deduct 10 energy per premium feature access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Configure these through Trophy's dashboard as negative point awards. When users perform tracked actions, Trophy automatically deducts configured energy amounts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Track Energy-Consuming Actions
&lt;/h3&gt;

&lt;p&gt;Send events for actions that consume energy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { TrophyApiClient } from '@trophyso/node';

const trophy = new TrophyApiClient(process.env.TROPHY_API_KEY);

// When user views a lesson (consumes 1 energy)
const response = await trophy.metrics.event('lesson_viewed', {
  user: {
    id: 'user-123'
  },
  value: 1 // 1 lesson viewed
});

// Check remaining energy
if (response.points?.energy) {
  const remaining = response.points.energy.total;
  const consumed = Math.abs(response.points.energy.added); // Will be negative
  console.log(`Consumed ${consumed} energy. ${remaining} remaining.`);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy processes the event and automatically deducts energy based on trigger configuration. The response includes updated energy balance for immediate display.&lt;/p&gt;

&lt;p&gt;Changes to energy consumption logic can be made in the Trophy dashboard, preventing back and forth code changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Check Energy Before Actions
&lt;/h3&gt;

&lt;p&gt;Before allowing energy-consuming actions, check if user has sufficient energy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Check user's current energy
const energy = await trophy.users.points('user-123', 'energy');

if (energy.total &amp;gt; 0) {
  // User has energy, allow action
  await performAction();

  // Track the action (consumes energy)
  await trophy.metrics.event('lesson_viewed', {
    user: { id: 'user-123' },
    value: 1
  });
} else {
  // User has no energy, prevent action or show paywall
  showInsufficientEnergyMessage();
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern prevents users from attempting actions they can't afford. The energy check happens before action processing, providing clear feedback.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Display Energy Status
&lt;/h3&gt;

&lt;p&gt;Show users their current energy and when it regenerates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Get detailed energy information
const energy = await trophy.users.points('user-123', 'energy', {
  awards: 5 // Last 5 energy changes
});

console.log({
  current: energy.total,
  maximum: energy.maximum,
  recentChanges: energy.awards.map(award =&amp;gt; ({
    amount: award.points,
    trigger: award.trigger.name,
    time: award.timestamp
  }))
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy returns current energy, maximum cap, and recent energy changes. Use this data for UI showing energy status and regeneration timing. The &lt;a href="https://docs.trophy.so/api-reference/endpoints/users/get-a-users-points?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;points API documentation&lt;/a&gt; covers all available fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  Regeneration Strategies
&lt;/h2&gt;

&lt;p&gt;Different regeneration patterns serve different product goals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constant regeneration&lt;/strong&gt; grants energy at fixed intervals regardless of usage. Users get 1 energy per hour even if they're at maximum. Simple but can waste potential energy when users hit caps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regeneration until cap&lt;/strong&gt; stops when users reach maximum. Trophy's default behavior. Users don't waste regeneration but might feel pressure to spend energy before hitting cap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overflow to storage&lt;/strong&gt; lets excess regeneration accumulate in separate pool. Complex but prevents waste. Trophy supports this through secondary points systems that have different caps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Activity-based regeneration&lt;/strong&gt; grants energy for specific actions beyond time. Complete a challenge, gain energy. This creates positive feedback loops where engagement grants resources for more engagement.&lt;/p&gt;

&lt;p&gt;Trophy's time-based triggers handle first two patterns natively. Configure in dashboard without code. More complex patterns use multiple points systems or custom trigger logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consumption Patterns
&lt;/h2&gt;

&lt;p&gt;How you consume energy affects gameplay and user psychology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixed consumption&lt;/strong&gt; deducts the same amount for all instances of an action. 1 energy per lesson. Simple and predictable. Users understand cost clearly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variable consumption&lt;/strong&gt; charges different amounts for different actions or contexts. 1 energy for basic lesson, 5 energy for advanced lesson. Creates strategic choice about energy spending.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scaling consumption&lt;/strong&gt; increases cost based on usage. First 5 lessons cost 1 energy each, next 5 cost 2 each. Encourages moderation and prevents grinding. Trophy implements through threshold-based triggers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial refunds&lt;/strong&gt; return energy if actions don't complete. User starts lesson (spends energy) but quits (refunds energy). Requires custom logic to track partial completions and issue refunds as positive point awards.&lt;/p&gt;

&lt;p&gt;Trophy's trigger flexibility supports all these patterns. Configure consumption amounts through dashboard. Adjust based on player behavior without code changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing Energy Gaming
&lt;/h2&gt;

&lt;p&gt;Energy systems create incentives to game. Design prevents exploitation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maximum caps&lt;/strong&gt; prevent infinite accumulation. Trophy's configurable maximum prevents users from stockpiling unlimited energy for later use. Choose caps based on intended session length.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting&lt;/strong&gt; beyond energy. If users can refresh energy artificially (time zone changes, system clock manipulation), add detection and rate limits. Trophy's server-side processing prevents client-side time manipulation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumption verification&lt;/strong&gt; ensures actions actually completed before deducting energy. Deduct energy only after verifying action succeeded. Trophy's event-based model supports this through proper event sequencing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Account-level tracking&lt;/strong&gt; prevents multi-account farming. If users create multiple accounts to bypass energy limits, implement account-level detection. Trophy tracks per-user; your authentication layer handles account limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Display and Communication
&lt;/h2&gt;

&lt;p&gt;How you present energy affects user experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clear cost indicators&lt;/strong&gt; before actions. "This will cost 5 energy" prevents surprise when users can't afford actions. Trophy's balance checking enables this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regeneration timing&lt;/strong&gt; transparency. "Energy regenerates in 2 hours" or "Full energy at 8 PM" gives users planning information. Trophy's time-based triggers have predictable schedules you can communicate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Friendly empty states&lt;/strong&gt;. "You're out of energy! It regenerates 1 per hour." explains situation without being punitive. Frame energy as pacing mechanic, not punishment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progress toward regeneration&lt;/strong&gt;. "Energy: 3/10 (regenerating...)" shows both current state and that progress continues. Trophy's balance provides current amount; you track maximum for display.&lt;/p&gt;

&lt;h2&gt;
  
  
  Economic Tuning
&lt;/h2&gt;

&lt;p&gt;Energy systems need careful tuning to feel fair without killing engagement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regeneration rate&lt;/strong&gt; determines session frequency. Fast regeneration (hourly) encourages frequent short sessions. Slow regeneration (daily) encourages longer, less frequent sessions. Trophy makes rate adjustments through dashboard configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maximum cap&lt;/strong&gt; determines session length. Cap of 10 with consumption of 1 per action allows 10 actions per session. Cap of 100 allows longer sessions but slower regeneration to full. Trophy's configurable cap lets you test different values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumption amounts&lt;/strong&gt; relative to regeneration define gameplay pace. If regeneration grants 20 energy daily and average session consumes 15, users can play daily with buffer. Trophy's analytics show consumption patterns informing tuning.&lt;/p&gt;

&lt;p&gt;Monitor average energy levels across users. If most users sit at maximum constantly, regeneration is too generous or consumption too low. If most users sit at zero, consumption is too high or regeneration too slow. Trophy's analytics dashboard shows energy distribution.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>gamification</category>
      <category>mobile</category>
    </item>
    <item>
      <title>How to Build an XP Feature</title>
      <dc:creator>Charlie Brinicombe</dc:creator>
      <pubDate>Fri, 17 Oct 2025 11:32:54 +0000</pubDate>
      <link>https://forem.com/charlie_brinicombe/how-to-build-an-xp-feature-1178</link>
      <guid>https://forem.com/charlie_brinicombe/how-to-build-an-xp-feature-1178</guid>
      <description>&lt;p&gt;You want to add experience points. Track user actions. Award XP for each one. Display totals. Seems straightforward. Three weeks later, you're dealing with point inflation, rebalancing award amounts, and handling edge cases around which actions should grant XP and when.&lt;/p&gt;

&lt;p&gt;XP systems appear simple until you implement them at scale. The core concept (accumulate points for actions) hides complexity in the details. Which actions grant how many points? How do you prevent inflation? What happens when you need to rebalance the economy? How do you handle retrospective changes fairly?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://trophy.so/?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;Trophy&lt;/a&gt; handles &lt;a href="https://trophy.so/features/points?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;XP systems&lt;/a&gt; including triggers, economic balance, and award tracking. The &lt;a href="https://docs.trophy.so/guides/how-to-build-an-xp-feature?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;complete implementation guide&lt;/a&gt; walks through the full process. Integration takes 1 day to 1 week. But understanding what building from scratch involves helps you make informed build versus buy decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Technical challenges in XP system implementation&lt;/li&gt;
&lt;li&gt;Economic design patterns that prevent inflation&lt;/li&gt;
&lt;li&gt;Trigger systems for awarding points automatically&lt;/li&gt;
&lt;li&gt;Integration examples with Trophy's API&lt;/li&gt;
&lt;li&gt;Build versus buy considerations for XP features&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Technical Reality
&lt;/h2&gt;

&lt;p&gt;Before building XP systems, understand the problems you're solving beyond simple counters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Point triggers&lt;/strong&gt; need sophisticated logic. Users earn XP for actions, but not just any action. Completing 10 tasks might grant 50 XP. But should it grant 5 XP per task or 50 XP at the 10-task milestone? Different trigger patterns serve different goals. Building flexible trigger systems takes time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Economic balance&lt;/strong&gt; prevents inflation. If point values never change but users keep accumulating, eventually everyone has millions of points that mean nothing. You need either point sinks (ways to spend points) or the ability to rebalance without disrupting existing users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retrospective changes&lt;/strong&gt; require careful handling. When you adjust point values or add new ways to earn XP, existing users might feel cheated if new users can earn more easily. Handling this fairly while improving the system is complex.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Award attribution&lt;/strong&gt; matters for analytics. When users earn XP, you need to know why. Which specific trigger fired? This enables analysis of which behaviors drive engagement and which point awards are effective.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Historical tracking&lt;/strong&gt; for progress visualization requires efficient storage. Showing users their XP over time means storing daily or weekly snapshots. This data grows linearly with users and time.&lt;/p&gt;

&lt;p&gt;Building production-ready XP systems typically takes 3-6 months including trigger logic, economic tuning, and analytics. Trophy's infrastructure handles these problems, reducing implementation to integration work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Patterns
&lt;/h2&gt;

&lt;p&gt;If building in-house, these patterns avoid common mistakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event-sourced points&lt;/strong&gt; separate actions from point awards. Store user actions as events. Compute point totals from event history. This enables retroactive rebalancing and complete audit trails. Trophy uses this pattern, making it easy to understand exactly why users have their current XP totals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configurable triggers&lt;/strong&gt; rather than hardcoded point awards. Store trigger rules in configuration or database, not code. This lets you adjust point values without deployment. Trophy's dashboard-based &lt;a href="https://docs.trophy.so/platform/points?ref=trophy.ghost.io#points-triggers" rel="noopener noreferrer"&gt;trigger configuration&lt;/a&gt; exemplifies this pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Denormalized totals&lt;/strong&gt; for query performance. Recomputing point totals from event history on every query doesn't scale. Maintain precomputed totals updated via triggers or async processes. Trophy caches current totals with millisecond query latency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotent award processing&lt;/strong&gt; prevents double-awarding. If the same action triggers multiple times due to retries or bugs, ensure points award only once. Trophy's event processing includes &lt;a href="https://docs.trophy.so/api-reference/idempotency?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;idempotency&lt;/a&gt; to handle this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tiered trigger logic&lt;/strong&gt; enables progressive complexity. Basic triggers: award X points for action Y. Advanced triggers: award X points for every N of action Y. Expert triggers: award points based on combinations of actions or user attributes.&lt;/p&gt;

&lt;p&gt;Trophy implements all these patterns. You configure triggers through the dashboard. Trophy handles the infrastructure complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Estimate
&lt;/h2&gt;

&lt;p&gt;Here's realistic timeline for building XP systems in-house:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1: Basic implementation.&lt;/strong&gt; Track user actions. Award fixed points per action. Display totals. Works in development with sample data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 2-3: Trigger system.&lt;/strong&gt; Build configurable triggers for different actions. Implement threshold-based awards (points for every N actions). Make trigger configuration updateable without code changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 4-5: Economic balancing.&lt;/strong&gt; Add tools to analyze point distribution. Implement rebalancing without disrupting existing users. Test point economy with realistic usage patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 6-7: Analytics and history.&lt;/strong&gt; Track which triggers award most points. Store historical point data for progress charts. Build reporting for understanding XP effectiveness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ongoing: Maintenance and tuning.&lt;/strong&gt; As usage patterns change, point values need adjustment. New features need new triggers. This work continues indefinitely.&lt;/p&gt;

&lt;p&gt;That's 7+ weeks of engineering time plus ongoing maintenance. Trophy's infrastructure eliminates this timeline, reducing implementation to 1 day to 1 week of integration work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building with Trophy
&lt;/h2&gt;

&lt;p&gt;Trophy's integration is faster because XP infrastructure already exists. Here's what implementation looks like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Create Points System
&lt;/h3&gt;

&lt;p&gt;In Trophy's dashboard, &lt;a href="https://docs.trophy.so/platform/points?ref=trophy.ghost.io#creating-points-systems" rel="noopener noreferrer"&gt;create a points system&lt;/a&gt; called "XP" (or whatever name fits your product). Configure optional settings like maximum points per user if you want caps.&lt;/p&gt;

&lt;p&gt;Trophy supports multiple points systems simultaneously. You might have XP for overall progress and separate currency for other purposes. Each system tracks independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Configure Point Triggers
&lt;/h3&gt;

&lt;p&gt;Point triggers define how users earn XP. In Trophy's dashboard, create triggers for each way users should earn points:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metric-based triggers&lt;/strong&gt; award points when users reach metric thresholds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Award 10 XP for every task completed&lt;/li&gt;
&lt;li&gt;Award 50 XP for every 5 lessons finished&lt;/li&gt;
&lt;li&gt;Award 100 XP for every workout logged&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Achievement-based triggers&lt;/strong&gt; award points when users complete achievements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Award 500 XP for completing the "Power User" achievement&lt;/li&gt;
&lt;li&gt;Award 1000 XP for reaching 30-day streak milestone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Streak-based triggers&lt;/strong&gt; award points for streak milestones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Award 50 XP for every 7-day streak&lt;/li&gt;
&lt;li&gt;Award 200 XP for 30-day streak milestone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;User identification triggers&lt;/strong&gt; aware points to users the moment that they are first identified with Trophy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Award 100 XP at sign up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trophy processes these triggers automatically when relevant events occur. No manual point awarding needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Track User Actions
&lt;/h3&gt;

&lt;p&gt;Send events to Trophy when users perform actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { TrophyApiClient } from '@trophyso/node';

const trophy = new TrophyApiClient(process.env.TROPHY_API_KEY);

// When user completes a task
await trophy.metrics.event('task_completed', {
  user: {
    id: 'user-123'
  },
  value: 1 // 1 task completed
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy processes the event and automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checks which point triggers apply&lt;/li&gt;
&lt;li&gt;Awards appropriate XP based on trigger configuration&lt;/li&gt;
&lt;li&gt;Updates user's total XP&lt;/li&gt;
&lt;li&gt;Returns award details in the response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The response includes point awards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const response = await trophy.metrics.event('task_completed', {
  user: {
    id: 'user-123'
  },
  value: 1
});

// Check if user earned XP
if (response.points?.xp) {
  const xpAwarded = response.points.xp.added;
  const newTotal = response.points.xp.total;
  console.log(`Earned ${xpAwarded} XP! Total: ${newTotal}`);

  // Show which triggers fired
  response.points.xp.awards.forEach(award =&amp;gt; {
    console.log(`${award.trigger.points} XP from ${award.trigger.name}`);
  });
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Display User XP
&lt;/h3&gt;

&lt;p&gt;Fetch user's XP total and recent awards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Get user's current XP with recent awards
const xp = await trophy.users.points('user-123', 'xp', {
  awards: 10 // Include last 10 XP awards
});

console.log({
  total: xp.total, // Total XP
  recentAwards: xp.awards.map(award =&amp;gt; ({
    amount: award.points,
    trigger: award.trigger.name,
    timestamp: award.timestamp
  }))
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides data for displaying XP totals and recent earning activity. The &lt;a href="https://docs.trophy.so/api-reference/endpoints/users/get-a-users-points?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;points API documentation&lt;/a&gt; covers all available query options.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Display XP Progress Over Time
&lt;/h3&gt;

&lt;p&gt;For charts showing XP earned over time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Get XP summary aggregated by day for last 30 days
const response = await trophy.users.pointsEventSummary('user-123', 'xp', {
  aggregation: 'daily',
  startDate: '2025-09-17',
  endDate: '2025-10-17'
});

// response.data contains daily XP totals for charting
response.data.forEach(day =&amp;gt; {
  console.log(`${day.date}: ${day.total} XP`);
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy returns chart-ready data aggregated by day, week, or month. Use this for progress visualizations showing users their XP growth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Point Trigger Strategies
&lt;/h2&gt;

&lt;p&gt;Effective XP systems use multiple trigger types that serve different goals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistent action rewards&lt;/strong&gt; drive habit formation. Award the same XP amount every time users complete core actions. 10 XP per task completed. 5 XP per lesson reviewed. Users learn what actions are "worth" and build routines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Milestone rewards&lt;/strong&gt; celebrate progress. Award bonus XP at thresholds. 50 XP for 10th task. 200 XP for 50th task. These create goal posts beyond the core action loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variety rewards&lt;/strong&gt; encourage exploration. Award XP for trying different features. 20 XP for first time using advanced editor. 30 XP for completing different task types. This drives feature discovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Achievement rewards&lt;/strong&gt; recognize special accomplishments. Award large XP amounts when users complete difficult achievements. 500 XP for mastery achievement. This creates high-value moments worth pursuing.&lt;/p&gt;

&lt;p&gt;Trophy's &lt;a href="https://docs.trophy.so/platform/points?ref=trophy.ghost.io#points-triggers" rel="noopener noreferrer"&gt;trigger configuration&lt;/a&gt; supports all these patterns through dashboard settings. Test different combinations to find what drives engagement in your product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Economic Balance
&lt;/h2&gt;

&lt;p&gt;XP systems need economic design to prevent inflation and maintain meaning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relative values matter&lt;/strong&gt; more than absolute amounts. Users don't care if they have 1,000 XP or 1,000,000 XP. They care about progression rate and comparisons to others. Design trigger values relative to each other, not based on absolute numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progression curves&lt;/strong&gt; should feel good. Early users should earn XP quickly (fast positive feedback). Late users should still progress but at reasonable pace. Trophy's analytics show XP distribution across users, helping you tune progression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rebalancing capability&lt;/strong&gt; is critical. As your product evolves, point values need adjustment. Trophy's trigger configuration makes &lt;a href="https://docs.trophy.so/platform/points?ref=trophy.ghost.io#balancing-points" rel="noopener noreferrer"&gt;rebalancing&lt;/a&gt; simple. Change values in dashboard. New events use new values. Existing XP remains unchanged, preventing user disruption.&lt;/p&gt;

&lt;p&gt;Monitor XP distribution regularly. If everyone has millions of XP, values might need adjustment. Trophy's analytics dashboard shows distribution curves, helping identify inflation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Retroactive Changes
&lt;/h2&gt;

&lt;p&gt;When you add new point triggers or change existing values, existing users have legitimate concerns about fairness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grandfathering&lt;/strong&gt; means existing XP remains unchanged when you adjust values. Users keep what they earned under old rules. New actions use new values. This prevents disruption but creates inconsistency over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One-time bonuses&lt;/strong&gt; can smooth transitions. When rebalancing downward, give existing high-XP users a one-time bonus so they don't feel penalized. Trophy's dashboard lets you create special one-time awards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clear communication&lt;/strong&gt; about changes prevents confusion. "We're rebalancing XP to better reflect action value" explains the change. Users accept adjustments when rationale is clear.&lt;/p&gt;

&lt;p&gt;Trophy's event sourcing means you can see exactly how users earned their XP. This transparency helps make fair decisions about retroactive changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Display and Communication
&lt;/h2&gt;

&lt;p&gt;How you present XP affects user motivation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prominent placement&lt;/strong&gt; for XP totals. Users should easily see their current XP and recent earnings. Trophy provides totals via API for display anywhere in your UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Immediate feedback&lt;/strong&gt; when earning XP. Show "+10 XP" animations when users complete actions. Trophy's event response includes XP awards for immediate display.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Progress context&lt;/strong&gt; helps users understand their XP. "You earned 250 XP this week, up 50 from last week" provides meaningful comparison. Trophy's summary API enables these comparisons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger transparency&lt;/strong&gt; shows users how to earn more. "Complete 5 more tasks to earn 50 XP bonus" creates clear goals. Trophy's trigger configuration can be exposed to users through your UI design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analytics and Optimization
&lt;/h2&gt;

&lt;p&gt;Trophy provides analytics showing XP system health.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distribution curves&lt;/strong&gt; show how XP spreads across your user base. Healthy systems show smooth progression from new users to power users. Clustering indicates problems in trigger design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trigger effectiveness&lt;/strong&gt; shows which awards drive behavior. If users rarely hit certain triggers, they might be too difficult or poorly communicated. Trophy's analytics show trigger fire rates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Earning velocity&lt;/strong&gt; shows how quickly users accumulate XP. Compare early user velocity (first 30 days) to later velocity. Dramatic drops suggest progression issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retention correlation&lt;/strong&gt; reveals whether XP drives engagement. Do users who earn more XP retain better? Trophy's data combined with your retention analytics answers this question.&lt;/p&gt;

&lt;p&gt;Use these insights to refine trigger values and introduce new ways to earn XP that drive behavior you want to encourage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Implementation Mistakes
&lt;/h2&gt;

&lt;p&gt;Teams integrating Trophy make predictable errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Too many triggers initially&lt;/strong&gt;. Start with 5-10 core triggers covering primary user actions. Add more based on usage patterns. Trophy makes adding triggers easy, so start focused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring trigger analytics&lt;/strong&gt;. Trophy shows which triggers fire frequently and which don't. If triggers never fire, they're misconfigured or targeting rare behavior. Adjust based on data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No progression structure&lt;/strong&gt;. Award the same points for beginner and expert actions creates flat experience. Design trigger values that recognize increasing mastery. Trophy's flexible triggers support progression design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blocking user actions on Trophy responses&lt;/strong&gt;. Trophy is fast, but don't block critical flows waiting for API responses. Track events asynchronously when possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forgetting about inflation&lt;/strong&gt;. Point values that seem balanced at 1,000 users might create inflation at 100,000 users. Monitor distribution curves and rebalance proactively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build vs. Buy Decision
&lt;/h2&gt;

&lt;p&gt;Here's how to think about build versus buy for XP:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build in-house when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your XP mechanics are so unusual platforms don't support them&lt;/li&gt;
&lt;li&gt;You have backend engineers with capacity for 2-3 months plus maintenance&lt;/li&gt;
&lt;li&gt;Engineering control over infrastructure is critical to your business&lt;/li&gt;
&lt;li&gt;Scale is small enough that simple implementations work (under 1,000 users)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Trophy when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want XP features in days, not months&lt;/li&gt;
&lt;li&gt;Trigger flexibility and rebalancing are important&lt;/li&gt;
&lt;li&gt;Engineering time is better spent on core features&lt;/li&gt;
&lt;li&gt;Analytics about XP effectiveness matter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trophy's implementation guide covers the complete integration process. Most teams implement basic XP functionality in 1-2 days, advanced triggers and analytics in 3-5 days. Compare this to 2-3 months building from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How long does Trophy integration take for XP?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Basic XP tracking: 1-2 days including trigger configuration and event tracking. Advanced features like custom trigger logic and detailed analytics: 3-5 days. Compare this to 2-3 months building in-house.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can we adjust point values after launch?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Trophy's dashboard configuration means changing point values requires no code deployment. Adjust trigger values and new events use new amounts. Existing XP remains unchanged unless you explicitly choose to adjust it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we prevent XP inflation?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Monitor distribution through Trophy's analytics. If concentration gets too high, adjust trigger values downward. Trophy's granular trigger control makes rebalancing straightforward without affecting existing user XP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if we want different XP rates for different users?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trophy's &lt;a href="https://docs.trophy.so/platform/users?ref=trophy.ghost.io#custom-user-attributes" rel="noopener noreferrer"&gt;user attributes&lt;/a&gt; enable segment-specific triggers. Award different XP amounts based on user level, subscription tier, or other attributes. Configure this through Trophy's dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can users spend XP?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trophy tracks XP totals. Implementing spending mechanics (point sinks) happens in your application. You can track spending as negative-value events that reduce XP totals, or implement separate spending tracking in your system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we test XP changes without affecting real users?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trophy supports multiple environments (staging, production). Test trigger configurations in staging before deploying to production. Trophy's event processing is deterministic, so staging tests accurately predict production behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if Trophy's API goes down?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Design your integration to degrade gracefully. Queue events for retry. Show cached XP totals. Most teams find Trophy's &lt;a href="https://status.trophy.so/?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;uptime&lt;/a&gt; meets or exceeds what they'd achieve with in-house infrastructure.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Build a Streaks Feature</title>
      <dc:creator>Charlie Brinicombe</dc:creator>
      <pubDate>Fri, 17 Oct 2025 11:14:47 +0000</pubDate>
      <link>https://forem.com/charlie_brinicombe/how-to-build-a-streaks-feature-4fck</link>
      <guid>https://forem.com/charlie_brinicombe/how-to-build-a-streaks-feature-4fck</guid>
      <description>&lt;p&gt;You decide to add a daily streak feature. Track consecutive days of user activity. Should be straightforward. Three weeks later, you're debugging why users in different time zones lose streaks unfairly, handling edge cases around daylight saving time, and dealing with race conditions when users act near midnight.&lt;/p&gt;

&lt;p&gt;Streak tracking seems simple until you implement it. The core logic (did user act today?) hides complexity in "today." Whose today? Server time creates unfair advantages. User time zones require careful handling. Midnight boundaries need grace periods. Consistency matters because broken streaks feel like lost investment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://trophy.so/?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;Trophy&lt;/a&gt; handles &lt;a href="https://trophy.so/features/streaks?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;streak tracking&lt;/a&gt; including time zones, streak freezes, and edge cases. The &lt;a href="https://docs.trophy.so/guides/how-to-build-a-streaks-feature?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;complete implementation guide&lt;/a&gt; covers the full process. Integration takes 1 day to 1 week. But understanding what building from scratch involves helps you make informed build versus buy decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Technical challenges that make streak tracking harder than it appears&lt;/li&gt;
&lt;li&gt;Time zone handling and consistency requirements&lt;/li&gt;
&lt;li&gt;Implementation patterns for scalable streak systems&lt;/li&gt;
&lt;li&gt;Integration examples with Trophy's API&lt;/li&gt;
&lt;li&gt;Build versus buy considerations for streak features&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Technical Reality
&lt;/h2&gt;

&lt;p&gt;Before building streak tracking, understand the problems you're solving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time zone fairness&lt;/strong&gt; requires per-user calculations. A user in Tokyo and a user in New York shouldn't compete on different playing fields. Server-time streaks give systematic advantages to users in certain time zones. Implementing &lt;a href="https://trophy.so/blog/handling-time-zones-gamification?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;time zone handling correctly&lt;/a&gt; takes weeks and ongoing maintenance as time zone rules change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistency guarantees&lt;/strong&gt; prevent race conditions. If a user acts at 11:59 PM and 12:01 AM, that's one action or two depending on timing. Concurrent requests near midnight create edge cases. Without proper handling, users might extend streaks twice for one action or lose streaks despite acting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.trophy.so/platform/streaks?ref=trophy.ghost.io#streak-freezes" rel="noopener noreferrer"&gt;&lt;strong&gt;Streak freeze&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;logic&lt;/strong&gt; adds complexity. Users need forgiveness for missed days without making streaks meaningless. Tracking freeze counts, granting new freezes over time, and applying them correctly when streaks would break requires careful state management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Historical data&lt;/strong&gt; for streak calendars and analytics needs efficient storage and querying. Showing users their past year of activity means storing and retrieving daily status. This grows linearly with users and time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DST transitions&lt;/strong&gt; create days that are 23 or 25 hours long. Naive date math breaks. Users might lose streaks on spring-forward days or extend twice on fall-back days without proper handling.&lt;/p&gt;

&lt;p&gt;Building production-ready streaks typically takes 1-2 months including edge cases and testing. Trophy's infrastructure handles these problems, reducing implementation to integration work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Patterns
&lt;/h2&gt;

&lt;p&gt;If building in-house, these patterns avoid common mistakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event-based tracking&lt;/strong&gt; separates user actions from streak computation. Store time stamped events. Compute streak status from event history rather than maintaining streak counters directly. This enables accurate historical queries and simplifies consistency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-user timezone storage&lt;/strong&gt; lets you compute streaks in user-local time. Store user timezone preferences. Convert all streak calculations to user-local time. This requires timezone libraries that handle DST correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grace periods&lt;/strong&gt; around midnight prevent losing streaks from being minutes late. Allow actions within 3-6 hours after midnight to count for the previous day. This reduces anxiety without meaningfully weakening consistency incentives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eventual consistency&lt;/strong&gt; in streak display is acceptable. Showing slightly stale streak counts for a few seconds doesn't affect user experience. Prioritize correctness over real-time updates. Trophy's architecture uses eventual consistency with fast convergence (milliseconds to seconds).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Precomputed streak state&lt;/strong&gt; for active users improves query performance. Maintain current streak length in fast storage (Redis or similar). Recompute from event history only when needed. This pattern scales better than computing from scratch on every query.&lt;/p&gt;

&lt;p&gt;Trophy uses &lt;a href="https://docs.trophy.so/platform/events?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;event-based architecture&lt;/a&gt; with timezone-aware computation and cached state. This combination provides both correctness and performance without requiring you to implement it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Estimate
&lt;/h2&gt;

&lt;p&gt;Here's realistic timeline for building streaks in-house:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1: Basic implementation.&lt;/strong&gt; Track user actions. Simple consecutive-day logic. Works in development with sample data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 2: Time zone handling.&lt;/strong&gt; Per-user timezone storage. Converting calculations to local time. Handling timezone changes when users travel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 3: Streak freezes.&lt;/strong&gt; Implementing freeze grants, consumption, and limits. Making freeze logic interact correctly with streak extension.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 4-5: Edge cases.&lt;/strong&gt; DST transitions. Grace periods. Race conditions near midnight. Historical data queries for calendar views.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ongoing: Maintenance.&lt;/strong&gt; Time zone rule updates. Bug fixes for edge cases discovered in production. Performance optimization as usage scales.&lt;/p&gt;

&lt;p&gt;That's 5+ weeks of engineering time plus ongoing maintenance. Trophy's infrastructure eliminates this timeline, reducing implementation to 1 day to 1 week of integration work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building with Trophy
&lt;/h2&gt;

&lt;p&gt;Trophy's integration is faster because streak infrastructure already exists. Here's what implementation looks like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Configure Streak Settings
&lt;/h3&gt;

&lt;p&gt;In Trophy's dashboard, configure your streak frequency (daily, weekly, or monthly) and select which metrics should extend streaks.&lt;/p&gt;

&lt;p&gt;For example, if users should maintain streaks by completing lessons, select your &lt;code&gt;lessons_completed&lt;/code&gt; metric. Trophy handles streak logic automatically when events arrive for that metric.&lt;/p&gt;

&lt;p&gt;Configure streak freezes if desired: initial freeze count for new users, freeze accumulation rate, and maximum freeze count. Trophy grants and consumes freezes automatically based on your settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Identify Users with Timezones
&lt;/h3&gt;

&lt;p&gt;When identifying users with Trophy, include their timezone for accurate streak tracking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { TrophyApiClient } from '@trophyso/node';

const trophy = new TrophyApiClient(process.env.TROPHY_API_KEY);

// Identify user with timezone
await trophy.identify('user-123', {
  name: 'Joe Bloggs'
  tz: 'America/New_York' // User's IANA timezone identifier
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy uses this timezone for all streak calculations. If users travel, update their timezone and Trophy adjusts streak windows accordingly. This ensures fair streak tracking globally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Track Metric Events
&lt;/h3&gt;

&lt;p&gt;Send events when users perform actions that should extend streaks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// When user completes a lesson
await trophy.metrics.event('lessons_completed', {
  user: {
    id: 'user-123'
  },
  value: 1 // 1 lesson completed
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy processes the event and automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Determines if it extends the user's streak based on their local timezone&lt;/li&gt;
&lt;li&gt;Applies streak freezes if the user missed days&lt;/li&gt;
&lt;li&gt;Updates streak length and history&lt;/li&gt;
&lt;li&gt;Returns the updated streak status in the response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The response includes the user's current streak information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const response = await trophy.metrics.event('lessons_completed', {
  user: {
    id: 'user-123'
  },
  value: 1 // 1 lesson completed
});

if (response.currentStreak?.extended) {
  // User extended their streak with this action
  console.log(`Streak length: ${response.currentStreak.length}`);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Display Streak Information
&lt;/h3&gt;

&lt;p&gt;Fetch user's streak status to display in your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Get user's current streak
const streak = await trophy.users.streak('user-123');

console.log({
  length: streak.length, // Current streak length (e.g., 15)
  started: streak.started, // When streak started
  expires: streak.expires, // When streak expires in user's timezone
  freezesRemaining: streak.freezes // Number of freezes available
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy's API returns comprehensive streak data including expiration time in the user's local timezone. Use this for reminder notifications or UI that shows time until streak expires.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Display Streak History
&lt;/h3&gt;

&lt;p&gt;For calendar views showing past activity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Get streak with historical data
const streak = await trophy.users.streak('user-123', {
  historyPeriods: 90 // Last 90 days of history
});

// streak.streakHistory contains daily activity for calendar display
streak.streakHistory.forEach(period =&amp;gt; {
  console.log(`${period.periodStart}: streak length ${period.length}`);
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy returns activity data for building calendar visualizations. The &lt;a href="https://docs.trophy.so/api-reference/endpoints/users/get-a-users-streak?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;user streak API documentation&lt;/a&gt; covers all available fields and query options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Edge Cases
&lt;/h2&gt;

&lt;p&gt;Production streak systems encounter edge cases that development testing misses. Trophy handles these automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Users crossing time zones&lt;/strong&gt; should have streaks follow them. If a user flies from California to Japan, their streak window shifts to Japan time. Trophy tracks user timezone and adjusts automatically when you update it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Midnight action ambiguity&lt;/strong&gt; needs clear rules. Trophy's grace period logic allows actions shortly after midnight to count for the previous day. This prevents losing streaks from being minutes late while maintaining meaningful consistency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Daylight saving transitions&lt;/strong&gt; create 23-hour or 25-hour days. Trophy's date math accounts for DST, ensuring users don't lose streaks on transition days due to calendar arithmetic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrent actions near midnight&lt;/strong&gt; could double-count or create race conditions. Trophy's event processing ensures each action counts correctly for exactly one day, regardless of concurrency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Freeze consumption timing&lt;/strong&gt; affects user experience. Trophy applies freezes automatically when users would otherwise lose streaks, transparently maintaining their investment without manual intervention.&lt;/p&gt;

&lt;p&gt;These edge cases take weeks to discover and fix when building in-house. Trophy's production experience means they're already handled correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streak Freeze Implementation
&lt;/h2&gt;

&lt;p&gt;Streak freezes let users miss days without losing streaks. &lt;a href="https://trophy.so/blog/streak-freezes-keep-users-engaged?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;How streak freezes keep users engaged&lt;/a&gt; explores the psychology and strategy behind forgiveness mechanics.&lt;/p&gt;

&lt;p&gt;Trophy's freeze system includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Initial freeze grant&lt;/strong&gt; : New users receive configured initial freezes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic accumulation&lt;/strong&gt; : Users gain freezes over time based on your configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maximum limit&lt;/strong&gt; : Freeze count caps at your configured maximum&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic consumption&lt;/strong&gt; : Trophy consumes freezes when users miss days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transparent tracking&lt;/strong&gt; : Users see remaining freeze count in API responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Configure these in Trophy's dashboard without code changes. Testing different freeze strategies requires updating configuration, not rewriting logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Streak response includes freeze information
const streak = await trophy.getStreak('user-123');

console.log({
  freezesRemaining: streak.freezes,
  maxFreezes: streak.maxFreezes,
  freezeAutoEarnAmount: streak.freezeAutoEarnAmount,
  freezeAutoEarnInterval: streak.freezeAutoEarnInterval
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Notification Strategy
&lt;/h2&gt;

&lt;p&gt;Streak reminders help users maintain consistency without creating anxiety. Trophy's &lt;a href="https://docs.trophy.so/platform/emails?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;email system&lt;/a&gt; handles notification timing automatically.&lt;/p&gt;

&lt;p&gt;Configure streak emails in Trophy's dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customize message content using a &lt;a href="https://docs.trophy.so/platform/emails?ref=trophy.ghost.io#designing-emails" rel="noopener noreferrer"&gt;no-code email builder&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Target specific user segments if needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trophy sends notifications based on user timezone, ensuring reminders arrive at appropriate local times. No manual timezone handling required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;Trophy's infrastructure is built for scale, but your integration patterns affect performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache streak data&lt;/strong&gt; for display in high-traffic areas. Streak counts don't need real-time accuracy. Cache for 30-60 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const cache = new Map();

async function getCachedStreak(userId: string) {
  const cached = cache.get(userId);
  if (cached &amp;amp;&amp;amp; Date.now() - cached.timestamp &amp;lt; 60000) {
    return cached.data;
  }

  const fresh = await trophy.users.streak(userId);
  cache.set(userId, { data: fresh, timestamp: Date.now() });
  return fresh;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Handle API errors gracefully&lt;/strong&gt;. Network issues happen. Don't block critical user flows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;try {
  const response = await trophy.metrics.event('lessons_completed', {
    user: {
      id: 'user-123'
    },
    value: 1
  });
} catch (error) {
  console.error('Failed to track event:', error);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Batch events when possible&lt;/strong&gt;. If users perform multiple tracked actions in one session, Trophy's API handles batching efficiently.&lt;/p&gt;

&lt;p&gt;Trophy handles backend scaling automatically. These client-side patterns optimize your application's performance without affecting Trophy's streak calculations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing Strategies
&lt;/h2&gt;

&lt;p&gt;Streak systems are hard to test because they depend on time. Trophy provides tools for testing without waiting days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test with different timezones&lt;/strong&gt; to ensure fair handling. Create test users in various timezones and verify streak calculations work correctly for each. Trophy's timezone support means your integration code stays simple while handling global complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test across midnight boundaries&lt;/strong&gt; by simulating actions at 11:59 PM and 12:01 AM. Verify streak extends correctly and grace periods work as expected. Trophy's APIs work consistently regardless of timing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test freeze consumption&lt;/strong&gt; by having test users miss days. Verify freezes consume correctly and streaks maintain. Trophy's automatic freeze handling should work without special code paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test DST transitions&lt;/strong&gt; by simulating actions on transition days. These are the hardest edge cases. Trophy handles them, but verify your UI displays correctly during transitions.&lt;/p&gt;

&lt;p&gt;Trophy's staging environment lets you test integration without affecting production data. The &lt;a href="https://docs.trophy.so/guides/how-to-build-a-streaks-feature?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;development guide&lt;/a&gt; includes testing strategies and example scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build vs. Buy Decision
&lt;/h2&gt;

&lt;p&gt;Here's how to think about build versus buy for streaks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build in-house when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your streak requirements are so unusual that platforms don't support them&lt;/li&gt;
&lt;li&gt;You have backend engineers with capacity for 2-3 months of work plus maintenance&lt;/li&gt;
&lt;li&gt;Engineering control over infrastructure is critical to your business&lt;/li&gt;
&lt;li&gt;Scale is small enough that simple implementations work (under 1,000 users)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Trophy when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want streaks in days, not months&lt;/li&gt;
&lt;li&gt;Time zone handling needs to be correct globally&lt;/li&gt;
&lt;li&gt;Engineering time is better spent on core features&lt;/li&gt;
&lt;li&gt;Maintenance burden should be minimized&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trophy's implementation guide walks through the complete integration process. Most teams complete basic streak functionality in 1-2 days, advanced features in 3-5 days. Compare this to 2-3 months building from scratch including time zones, freezes, and edge cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding User Behavior
&lt;/h2&gt;

&lt;p&gt;Trophy provides analytics showing streak distribution across your user base. Understanding &lt;a href="https://trophy.so/blog/what-happens-when-users-lose-streaks?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;what happens when users lose their streaks&lt;/a&gt; helps you design better forgiveness mechanics and communication strategies.&lt;/p&gt;

&lt;p&gt;Monitor these patterns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Average streak length across all users&lt;/li&gt;
&lt;li&gt;Distribution of streak lengths (how many at 7 days, 30 days, 100+ days)&lt;/li&gt;
&lt;li&gt;Freeze usage patterns (immediate consumption vs. accumulation)&lt;/li&gt;
&lt;li&gt;Restart rates after breaks (do users return after losing streaks?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trophy's analytics dashboard shows these metrics. Connecting them to your retention data reveals whether streaks drive sustainable engagement or create pressure that leads to churn.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How long does Trophy integration take for streaks?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Basic streak tracking: 1-2 days including user identification with timezones and event tracking. Advanced features like custom freeze logic or complex calendar views: 3-5 days. Compare this to 2-3 months building in-house.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if Trophy's API goes down?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Design your integration to degrade gracefully. Queue events for retry. Show cached streak data. Most teams find Trophy's uptime exceeds what they'd achieve with in-house infrastructure given resource constraints. The &lt;a href="https://docs.trophy.so/platform/events?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;event tracking documentation&lt;/a&gt; includes reliability guidance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we handle users with unreliable internet?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trophy's API is designed for reliability, but network issues happen. Implement retry logic with exponential backoff. Queue failed events locally. Trophy's idempotency support prevents duplicate processing when retrying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can we customize streak rules?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trophy supports daily, weekly, and monthly streak frequencies. Grace periods are configurable. Freeze grants and limits are configurable. This covers most use cases. If you need truly custom streak logic, building in-house gives full control, but validate this need before committing to months of development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we test without waiting days?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trophy provides test environments where you can manipulate time for testing. Create test users, track events with different timestamps, and verify streak behavior without waiting. The development guide includes testing strategies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if we want to change streak settings later?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trophy's dashboard configuration means changes don't require code deployments. Adjust freeze grants, streak frequency, or which metrics count toward streaks through configuration. Changes apply to future streak periods without affecting users' existing streaks.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
    <item>
      <title>How to Build a Leaderboards Feature</title>
      <dc:creator>Charlie Brinicombe</dc:creator>
      <pubDate>Fri, 17 Oct 2025 10:55:20 +0000</pubDate>
      <link>https://forem.com/charlie_brinicombe/how-to-build-a-leaderboards-feature-4821</link>
      <guid>https://forem.com/charlie_brinicombe/how-to-build-a-leaderboards-feature-4821</guid>
      <description>&lt;p&gt;You want leaderboards. You estimate three weeks. Six weeks later, you're still debugging edge cases around time zones and optimizing queries that timeout at scale.&lt;/p&gt;

&lt;p&gt;Leaderboards look simple. Display a sorted list of users and their scores. But production leaderboards require handling concurrent updates, efficient ranking algorithms, time-based resets, and global scale. Most teams underestimate the complexity until they're deep in implementation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://trophy.so/?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;Trophy&lt;/a&gt; solves these problems through purpose-built &lt;a href="https://trophy.so/features/leaderboards?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;leaderboard infrastructure&lt;/a&gt; that integrates in 1 day to 1 week. The &lt;a href="https://docs.trophy.so/guides/how-to-build-a-leaderboards-feature?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;complete implementation guide&lt;/a&gt; walks through the full process. But understanding what building from scratch actually involves helps you make informed build versus buy decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Technical challenges that make leaderboards harder than they appear&lt;/li&gt;
&lt;li&gt;Architecture patterns for scalable leaderboard implementation&lt;/li&gt;
&lt;li&gt;Time and cost estimates for building versus using a platform&lt;/li&gt;
&lt;li&gt;Integration patterns with Trophy for rapid implementation&lt;/li&gt;
&lt;li&gt;Code examples for common leaderboard scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Technical Reality
&lt;/h2&gt;

&lt;p&gt;Before starting implementation, understand what you're actually building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time ranking computation&lt;/strong&gt; doesn't scale naively. Sorting millions of users on every score update kills database performance. You need incremental ranking algorithms, precomputed rankings, or approximate methods. Implementing these correctly takes weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrent update handling&lt;/strong&gt; creates race conditions. Two users updating scores simultaneously can corrupt rankings if not handled atomically. You need proper transaction isolation or event-based processing. Getting this wrong creates subtle bugs that surface under load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time-based resets&lt;/strong&gt; for daily, weekly, or monthly leaderboards require accurate timekeeping across time zones. When does a daily leaderboard end globally? Trophy handles &lt;a href="https://trophy.so/blog/handling-time-zones-gamification?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;time zones automatically&lt;/a&gt;, but building this yourself means implementing per-user timezone logic and handling daylight saving transitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query optimization&lt;/strong&gt; becomes critical at scale. Fetching top 100 users is fast. Finding a specific user's rank among millions is slow without proper indexing. Getting both rankings and nearby competitors in one query requires careful optimization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data consistency&lt;/strong&gt; between score updates and rankings needs attention. Eventually consistent systems might show stale rankings. Strongly consistent systems sacrifice availability. Choosing the right consistency model matters.&lt;/p&gt;

&lt;p&gt;Building production-ready leaderboards from scratch typically takes 3-6 months including all these considerations. Trophy's infrastructure handles them, reducing implementation to 1 day to 1 week of integration work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Patterns
&lt;/h2&gt;

&lt;p&gt;If you're building in-house, these patterns help avoid common pitfalls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event-based score updates&lt;/strong&gt; separate writes from ranking computation. User actions create score events. A separate process computes rankings asynchronously. This pattern scales better than synchronous ranking updates but adds complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sorted sets in Redis&lt;/strong&gt; provide efficient ranking operations. Redis &lt;code&gt;ZADD&lt;/code&gt; and &lt;code&gt;ZRANK&lt;/code&gt; commands handle concurrent updates and ranking queries efficiently. But you still need to handle persistence, time-based resets, and integration with your application data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Precomputed materialized views&lt;/strong&gt; in your database work for smaller scale. Create a rankings table updated via triggers or batch jobs. Query this table for leaderboards. This pattern is simpler but doesn't scale as well as Redis-based approaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approximate counting&lt;/strong&gt; using HyperLogLog or similar algorithms works when exact rankings aren't critical. Users outside top 1,000 might see approximate ranks like "~50,000" which is sufficient for most use cases.&lt;/p&gt;

&lt;p&gt;Trophy uses event-based architecture with Redis-backed ranking computation, materialized in PostgreSQL for durability. This combination provides both performance and reliability without you needing to implement it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation Estimate
&lt;/h2&gt;

&lt;p&gt;Here's a realistic timeline for building leaderboards in-house with a competent backend engineer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 1-2: Basic implementation.&lt;/strong&gt; Score tracking, simple ranking logic, API endpoints. Everything works in development with small data volumes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 3-4: Performance optimization.&lt;/strong&gt; Query optimization, caching, handling concurrent updates properly. Making it work at scale reveals the real challenges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 5-6: Time-based resets.&lt;/strong&gt; Implementing daily/weekly/monthly leaderboards with proper time zone handling. This is harder than it looks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week 7-8: Edge cases and testing.&lt;/strong&gt; Race conditions, consistency guarantees, handling database failures gracefully. QA finds issues you didn't anticipate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ongoing: Maintenance and evolution.&lt;/strong&gt; As usage grows, performance degrades. New features require system evolution. This maintenance burden continues indefinitely.&lt;/p&gt;

&lt;p&gt;That's 8+ weeks of engineering time plus ongoing maintenance. At typical fully-loaded costs for a backend engineer, this represents significant development investment, plus opportunity cost of features not built.&lt;/p&gt;

&lt;p&gt;Trophy's &lt;a href="https://trophy.so/pricing?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;pricing&lt;/a&gt; is based on monthly active users. The cost comparison depends on your specific usage and engineering costs, but for many teams, platform costs become competitive with in-house development when factoring in both initial build time and ongoing maintenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building with Trophy
&lt;/h2&gt;

&lt;p&gt;Trophy's integration is significantly faster because the infrastructure already exists. Here's what implementation actually looks like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Set Up Metrics
&lt;/h3&gt;

&lt;p&gt;Leaderboards rank users based on &lt;a href="https://trophy.so/features/metrics?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;metrics&lt;/a&gt;. First, define what you want to track. In Trophy's dashboard, create a metric for the action you want to rank users by.&lt;/p&gt;

&lt;p&gt;For example, if you're building a fitness app leaderboard ranking by workouts completed, create a &lt;code&gt;workouts_completed&lt;/code&gt; metric. Trophy's dashboard makes this a form-fill operation taking seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Track Events
&lt;/h3&gt;

&lt;p&gt;Send events to Trophy when users perform tracked actions. Here's an example using Trophy's API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { TrophyApiClient } from '@trophyso/node';

const trophy = new TrophyApiClient({ apiKey: process.env.TROPHY_API_KEY });

// When user completes a workout
async function handleWorkoutComplete(userId: string) {
  const response = await trophy.metrics.event("workouts_completed", {
  user: {
    id: "18",
    email: "user@example.com",
    tz: "Europe/London",
  },
  value: 1, // 1 workout completed
});

  // Response includes updated leaderboard positions if user is ranked
  console.log(response.leaderboards);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy processes events in milliseconds and updates leaderboard rankings automatically. The response includes the user's updated leaderboard positions, which you can use to show real-time feedback.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Create Leaderboard
&lt;/h3&gt;

&lt;p&gt;In Trophy's dashboard, create a leaderboard configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose the &lt;a href="https://docs.trophy.so/platform/leaderboards?ref=trophy.ghost.io#types-of-leaderboards" rel="noopener noreferrer"&gt;type of leaderboard&lt;/a&gt; (e.g "Metric").&lt;/li&gt;
&lt;li&gt;Choose the metric to rank by (e.g., &lt;code&gt;workouts_completed&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Set the time window (e.g. every 3 days, every 6 months or perpetual)&lt;/li&gt;
&lt;li&gt;Configure maximum participants (up to 1,000)&lt;/li&gt;
&lt;li&gt;Set start/end dates if time-limited&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trophy handles all ranking computation, time zone processing, and data storage. No code required for the leaderboard logic itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Display Rankings
&lt;/h3&gt;

&lt;p&gt;Fetch leaderboard data to display in your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Get top 10 users on a leaderboard
async function getLeaderboard(leaderboardKey: string) {
  const leaderbooard = await trophy.leaderboards.get("weekly-words", {
    offset: 0,
    limit: 10,
    run: "2025-01-15" // The specific week to get rankings for
  });

  return leaderboard.rankings.map(ranking =&amp;gt; ({
    userId: ranking.userId,
    rank: ranking.rank,
    value: ranking.value
  }));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://docs.trophy.so/api-reference/endpoints/leaderboards/get-leaderboard?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;leaderboard API documentation&lt;/a&gt; covers all query options including querying historical rankings for previous leaderboard runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Show Position Changes
&lt;/h3&gt;

&lt;p&gt;Trophy tracks rank changes, making it easy to show users when they move up or down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// The metric event response includes leaderboard changes
async function handleWorkoutComplete(userId: string) {
  const response = await trophy.metrics.event('workouts_completed', {
    user: {
      id: "user-id"
    },
    value: 1
  });

  // Check if user moved up in rankings
  const leaderboard = response.leaderboards['weekly_workout_leaderboard'];
  if (leaderboard &amp;amp;&amp;amp; leaderboard.rank &amp;lt; leaderboard.previousRank) {
    const positionsGained = leaderboard.previousRank - leaderboard.rank;
    showNotification(`You moved up ${positionsGained} positions!`);
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This real-time feedback creates engaging user experiences without complex rank tracking logic on your side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time-Based Leaderboards
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.trophy.so/platform/leaderboards?ref=trophy.ghost.io#repeating-leaderboards" rel="noopener noreferrer"&gt;Time-based leaderboards&lt;/a&gt; that reset daily, weekly, or monthly require careful handling of time zones and reset logic. Trophy manages this automatically.&lt;/p&gt;

&lt;p&gt;When you create a leaderboard with a time window (e.g., "every 7 days"), Trophy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tracks user scores within the current time period&lt;/li&gt;
&lt;li&gt;Resets scores at period boundaries&lt;/li&gt;
&lt;li&gt;Handles time zones so all users get fair competition windows&lt;/li&gt;
&lt;li&gt;Finalizes rankings after all time zones complete the period&lt;/li&gt;
&lt;li&gt;Archives past period results for historical reference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your code stays simple. Just send events and fetch rankings. Trophy handles the complexity of time-based competition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Create events - Trophy handles which period they belong to
const response = await trophy.metrics.event('workouts_completed', {
  user: {
    id: "user-id"
  },
  value: 1
});

// Fetch current week's leaderboard
const currentWeek = await trophy.leaderboards.get('weekly_workout_leaderboard');

// Fetch specific past week using the 'run' parameter
const lastWeek = await trophy.leaderboards.get('weekly_workout_leaderboard', {
  run: '2025-10-10' // Start date of the period
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Historical leaderboard data persists automatically. You can display past winners or track user improvement over time without additional database design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filtered and Segmented Leaderboards
&lt;/h2&gt;

&lt;p&gt;Many products need multiple leaderboard views. Friends only. Same region. Same skill level. Trophy supports this through user attributes.&lt;/p&gt;

&lt;p&gt;First, set user attributes when identifying users:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await trophy.users.identify("user-id", {
  name: "Joe Bloggs",
  attributes: {
    region: 'north_america',
    skill_level: 'intermediate'
  }
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create filtered leaderboards in Trophy's dashboard using these attributes. The same metric events feed multiple leaderboards, but each leaderboard only ranks users matching its filters.&lt;/p&gt;

&lt;p&gt;This enables sophisticated leaderboard structures without complex query logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global leaderboard (all users)&lt;/li&gt;
&lt;li&gt;Regional leaderboards (filtered by region)&lt;/li&gt;
&lt;li&gt;Skill-level leaderboards (filtered by skill_level)&lt;/li&gt;
&lt;li&gt;Friend leaderboards (filtered by custom friend relationships)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trophy's &lt;a href="https://docs.trophy.so/platform/users?ref=trophy.ghost.io#custom-user-attributes" rel="noopener noreferrer"&gt;user attributes system&lt;/a&gt; makes segmentation straightforward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;p&gt;Trophy's infrastructure is built for scale, but your integration patterns affect performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache leaderboard data&lt;/strong&gt; on your side for read-heavy scenarios. Leaderboard rankings don't need real-time accuracy for display. Cache for 30-60 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const cache = new Map();

async function getCachedLeaderboard(key: string) {
  const cached = cache.get(key);
  if (cached &amp;amp;&amp;amp; Date.now() - cached.timestamp &amp;lt; 60000) {
    return cached.data;
  }

  const fresh = await trophy.leaderboards.get(key);
  cache.set(key, { data: fresh, timestamp: Date.now() });
  return fresh;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fetch only needed data&lt;/strong&gt; using Trophy's &lt;a href="https://docs.trophy.so/api-reference/endpoints/leaderboards/get-leaderboard?ref=trophy.ghost.io#parameter-limit" rel="noopener noreferrer"&gt;query parameters&lt;/a&gt;. Don't fetch all 1,000 participants if you only display top 10:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Efficient: Only fetch top 10
const top10 = await trophy.leaderboards.get(key, { limit: 10 });

// Inefficient: Fetch everything then filter client-side
const all = await trophy.leaderboards.get(key);
const top10 = all.rankings.slice(0, 10); // Don't do this

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trophy handles backend scaling automatically. These client-side patterns optimize your application's performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Edge Cases
&lt;/h2&gt;

&lt;p&gt;Production leaderboards encounter edge cases that development testing misses. Trophy handles these automatically, but understanding them helps you design better user experiences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tied scores&lt;/strong&gt; need tie-breaking logic. Trophy breaks ties using timestamps (earlier achievement ranks higher). Communicate this to users so ties feel fair rather than arbitrary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New users entering full leaderboards&lt;/strong&gt; need to displace the lowest-ranked user. Trophy handles this automatically when leaderboards reach their participant limit. Users must exceed the lowest score to enter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Score corrections&lt;/strong&gt; might require re-ranking. If you discover fraudulent activity and need to adjust scores, Trophy's event system lets you send corrective events that trigger re-ranking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deleted users&lt;/strong&gt; should be removed from leaderboards. When users delete accounts, Trophy can remove them from active leaderboards while preserving historical accuracy.&lt;/p&gt;

&lt;p&gt;These edge cases typically take weeks to discover and fix when building in-house. Trophy's production experience means they're already handled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build vs. Buy Decision Matrix
&lt;/h2&gt;

&lt;p&gt;Here's how to think about the &lt;a href="https://www.trophy.so/buy-vs-build?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;build versus buy&lt;/a&gt; decision for leaderboards specifically:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build in-house when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your leaderboard requirements are unusual enough that platforms don't support them&lt;/li&gt;
&lt;li&gt;You have backend engineers with spare capacity (rare)&lt;/li&gt;
&lt;li&gt;Engineering control over infrastructure is critical to your business&lt;/li&gt;
&lt;li&gt;Scale is small enough that simple implementations work (under 1,000 users)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use Trophy when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want leaderboards in weeks, not months&lt;/li&gt;
&lt;li&gt;Engineering time is better spent on core product features&lt;/li&gt;
&lt;li&gt;You need proven infrastructure that handles scale and edge cases&lt;/li&gt;
&lt;li&gt;Maintenance burden should be minimized&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams find that platform costs are competitive with the engineering time required to build and maintain leaderboards in-house, especially when considering opportunity cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration from Existing Systems
&lt;/h2&gt;

&lt;p&gt;If you've already built leaderboards in-house but want to migrate to Trophy, here's the pattern:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1: Dual write.&lt;/strong&gt; Send events to both your system and Trophy. Validate that Trophy's rankings match your implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2: Comparison.&lt;/strong&gt; Run both systems in parallel for a period. Compare rankings and ensure consistency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 3: Switch reads.&lt;/strong&gt; Start fetching leaderboard data from Trophy instead of your system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 4: Decommission.&lt;/strong&gt; Remove your leaderboard implementation after a safety period.&lt;/p&gt;

&lt;p&gt;Trophy's &lt;a href="https://docs.trophy.so/platform/events?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;event API&lt;/a&gt; makes this migration smooth. You can transition with minimal risk by running systems in parallel initially.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How long does Trophy integration actually take?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For basic leaderboards: 1 day setup including authentication and event tracking implementation. A few extra days for UI development. For complex multi-leaderboard setups with segmentation, perhaps a day more Compare this to 2-3 months building from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about vendor lock-in?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trophy's event data is yours. Export it anytime via API. If you eventually want to move to in-house infrastructure, you can migrate your event history. But most teams find Trophy's value continues to justify the cost compared to maintenance burden.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can Trophy handle our scale?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trophy processes millions of events daily and supports leaderboards up to 1,000 participants. If you need larger leaderboards or higher volume, contact Trophy's team to discuss enterprise options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if Trophy's API goes down?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Design your integration to degrade gracefully. Queue events for retry. Show cached leaderboard data. Most teams find Trophy's uptime exceeds what they'd achieve with in-house infrastructure given resource constraints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do we handle real-time updates in our UI?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trophy's APIs are fast enough for polling (every 30-60 seconds) without creating scaling issues. For true real-time updates, cache aggressively and update on user actions rather than polling. Trophy's event response includes updated rankings for immediate feedback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can we customize leaderboard rules?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trophy's metrics can track any user action. Leaderboards can rank by any metric. &lt;a href="https://docs.trophy.so/platform/users?ref=trophy.ghost.io#custom-user-attributes" rel="noopener noreferrer"&gt;User attributes&lt;/a&gt; enable filtered leaderboards. This combination covers most use cases. If you need something truly custom, building in-house might be necessary, but validate this before committing to months of development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the alternative if Trophy doesn't fit?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building in-house remains an option. Use the architecture patterns in this post. Budget 3-6 months of engineering time plus ongoing maintenance. For many teams, this investment exceeds Trophy's lifetime cost, but some requirements genuinely need custom infrastructure.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How we built a block-based template editor for gamification emails</title>
      <dc:creator>Charlie Brinicombe</dc:creator>
      <pubDate>Sun, 13 Jul 2025 22:02:22 +0000</pubDate>
      <link>https://forem.com/charlie_brinicombe/how-we-built-a-block-based-template-editor-for-gamification-emails-2l11</link>
      <guid>https://forem.com/charlie_brinicombe/how-we-built-a-block-based-template-editor-for-gamification-emails-2l11</guid>
      <description>&lt;p&gt;At the &lt;a href="https://trophy.so/?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;company&lt;/a&gt; I co-founded we recently released an interactive template editor for gamification emails (things like achievement notifications, weekly usage recaps, and re-engagement emails after periods of inactivity).&lt;/p&gt;

&lt;p&gt;The initial goal was to basically implement a Notion-style document editor, but things got complex pretty quickly...&lt;/p&gt;

&lt;p&gt;This post breaks down the different approaches we considered, where we ended up, and why we made the decisions we did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Goals &amp;amp; Potential Approaches
&lt;/h2&gt;

&lt;p&gt;We knew from the start that we wanted our template editor to have the following attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy to use, even for non-technical users&lt;/li&gt;
&lt;li&gt;Extensible so we can easily add more new block types over time&lt;/li&gt;
&lt;li&gt;Low-maintenance (not too many dependencies, not prone to those small-but-impossible-to-debug issues that can be the bane of engineers’ existence)&lt;/li&gt;
&lt;li&gt;Dynamic, with support for embedded variables and conditional rendering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We considered the following approaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Raw HTML + Handlebars With Previewer
&lt;/h3&gt;

&lt;p&gt;SendGrid uses this approach in their dynamic templates feature:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXfeOc1z3rtAU_dgYZU86h7tlg9RW1-Abo5k7ZuVz251WkZRrhNI6-Khj7GvxvtKC3KxvtWq10aEcUXJjWgKfiBUUVoh8yr9vxnTG9BhewLOIA4IVg0VnOywYKwCpUVhr8LQHdYy%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXfeOc1z3rtAU_dgYZU86h7tlg9RW1-Abo5k7ZuVz251WkZRrhNI6-Khj7GvxvtKC3KxvtWq10aEcUXJjWgKfiBUUVoh8yr9vxnTG9BhewLOIA4IVg0VnOywYKwCpUVhr8LQHdYy%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" alt="How we built a block-based template editor for gamification emails" width="1199" height="901"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;SendGrid's approach&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If we went this route, our emails would be able to contain pretty much anything, since users would have control over HTML, and something like Handlebars is powerful enough to do all sorts of conditional rendering and variable insertion.&lt;/p&gt;

&lt;p&gt;The obvious downside is that this is quite complicated for non-technical users (potentially disqualifying) and a high barrier to entry even for technical users, since it takes a long time to get a nice looking email out of raw HTML.&lt;/p&gt;

&lt;p&gt;A template library could help with this, but there’d still be more work involved in adapting a template to fit a company’s exact needs and brand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Block-Based With Previewer
&lt;/h3&gt;

&lt;p&gt;Prismic uses this approach for their marketing page editor:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXe6pAxV0UpiCidtraJUWZEppFq282YHhXx118AHADCOpvwEO3OLyzPdSBUHVMmMtVupvaTNaLkcCuwfE0Y-32fHQZBLINus0nR2NI8aNMlRS4ZYRgUzhfCqtMP42Ux5pyfRb7_lXQ%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXe6pAxV0UpiCidtraJUWZEppFq282YHhXx118AHADCOpvwEO3OLyzPdSBUHVMmMtVupvaTNaLkcCuwfE0Y-32fHQZBLINus0nR2NI8aNMlRS4ZYRgUzhfCqtMP42Ux5pyfRb7_lXQ%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" alt="How we built a block-based template editor for gamification emails" width="1600" height="929"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Prismic's approach&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The goal here is to have a set of reusable blocks that can be added and reordered on the page. The blocks are not WYSIWYG, so they can contain form fields and other controls. To see the end result, users must use the preview tool.&lt;/p&gt;

&lt;p&gt;Text fields within blocks could support rich text using TipTap and variable insertion via Mustache or Handlebars, and there could be a “Conditional” block type to support conditional rendering of blocks depending on those variables. &lt;/p&gt;

&lt;p&gt;The main advantage of an approach like this over raw HTML is that it lowers the barrier to entry drastically. Users no longer need to fiddle with HTML and CSS, nor do they need to know the intricacies of handlebars for conditional rendering. They just need to be able to drag/drop blocks around and understand the &lt;code&gt;{{variableName}}&lt;/code&gt; syntax.&lt;/p&gt;

&lt;p&gt;But, this approach is slightly less powerful than raw HTML and still requires a previewer tool to see how the email would actually look to its recipients.&lt;/p&gt;

&lt;h3&gt;
  
  
  Notion-Style Blocks (WYSIWYG)
&lt;/h3&gt;

&lt;p&gt;This style is all the rage these days.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXeViT7h-EpbdDqSBBa_QR2qZPweapSNxo2vjb9Ts-EyWEY30Oi9G1GUJMXQ3ia0sKlangNyZL_roy8EF_7SkVkaxafNMzaMdktL1K0zOpMqBkDgcWozeRY9kEtqnOd8Zy1Sg0IpMA%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXeViT7h-EpbdDqSBBa_QR2qZPweapSNxo2vjb9Ts-EyWEY30Oi9G1GUJMXQ3ia0sKlangNyZL_roy8EF_7SkVkaxafNMzaMdktL1K0zOpMqBkDgcWozeRY9kEtqnOd8Zy1Sg0IpMA%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" alt="How we built a block-based template editor for gamification emails" width="1600" height="999"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Notion's approach&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This approach maintains the advantages of the regular block-based approach, but loses the need for a previewer, since blocks display exactly as they would to recipients. The user can simply place the cursor into a block and start typing to make changes.&lt;/p&gt;

&lt;p&gt;The main downside here is specific to our use case: we really need to support conditional rendering of blocks, and it’s unclear how that could work in a design like Notion’s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experimentation
&lt;/h2&gt;

&lt;p&gt;I started out with the Notion-style approach, using TipTap’s Notion-style editor template. The entire system of blocks was within the scope of TipTap, meaning the entire email template editor &lt;em&gt;was&lt;/em&gt; the rich text editor.&lt;/p&gt;

&lt;p&gt;Every block was a TipTap node and the user could navigate through the document using the arrow keys or clicking around to different areas.&lt;/p&gt;

&lt;p&gt;This got clunky &lt;em&gt;very&lt;/em&gt; fast, for a couple of reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We needed block types that had their own text fields within them. That means we needed blocks to have an “edit state”, where when selected they’d no longer be WYSIWYG.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This sort of view &amp;amp; edit state at the block level does not work well within TipTap since we’d have to deal with the cursor navigating in and out of these, and text selection (can the user click &amp;amp; drag to highlight text from multiple blocks, even when one is in the edit state?).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Conditional blocks, as previously mentioned. There was no elegant way to get conditional rendering trees to look &amp;amp; feel good within a Notion-style editor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We &lt;em&gt;did&lt;/em&gt; however like the feel of being able to click into text and use a TipTap-powered rich text experience for italicizing, bolding, and inserting variables using Mustache syntax. TipTap is extensible enough to add things like dynamic menus that appear when you type &lt;code&gt;{{&lt;/code&gt;, showing the plain-English variable names.&lt;/p&gt;

&lt;p&gt;So rather than throwing this out completely and trying out the block-based approach with a previewer, we figured a hybrid approach would be worth a shot.&lt;/p&gt;

&lt;p&gt;Specifically I tried the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get rid of TipTap as the manager of the blocks, and instead only use it for rich text fields &lt;em&gt;within&lt;/em&gt; blocks.&lt;/li&gt;
&lt;li&gt;Create our own simple React component for managing the blocks.&lt;/li&gt;
&lt;li&gt;Have a selection state so users can select one block on the page at a time. The selected block is displayed in an edit mode that shows the block’s fields and controls. All other blocks remain in view mode which is fully WYSIWYG.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach worked!&lt;/p&gt;

&lt;p&gt;It allowed us to keep the user-friendliness of the Notion-style editor, but still support dynamic rendering (through a “Conditional” block that shows all conditional paths when in edit mode but only one when in view mode) and having multiple fields within each block. Here’s what the editor looks like in practice:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXeY6oznHvdB9YTOYf8o-CRszimtFRfxe1Fg_QEPR6Q_2b6dgWZbTelA_go0YDiJsUM5arunX9oKyHE_4yYwDo_vVr7yIMGaqYqqAYnnacb55rPEhZZvgHQ6UzTBkE5qK80MJdeU3g%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXeY6oznHvdB9YTOYf8o-CRszimtFRfxe1Fg_QEPR6Q_2b6dgWZbTelA_go0YDiJsUM5arunX9oKyHE_4yYwDo_vVr7yIMGaqYqqAYnnacb55rPEhZZvgHQ6UzTBkE5qK80MJdeU3g%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" alt="How we built a block-based template editor for gamification emails" width="1600" height="888"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Our first version&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And that conditional block I mentioned looks like this while being edited:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXcEarIFWKOI3z-q8r68H86FV9TkbNcWMWvscTPON2MaqWWfKrC3O1e579snYyzTQg3i0JboDn3_s_TZrhnqcA2llN5cZO8Y10paZIs1NxoyQCa_Mf2sLbtO8DZuf1YDzCB1xwWS%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXcEarIFWKOI3z-q8r68H86FV9TkbNcWMWvscTPON2MaqWWfKrC3O1e579snYyzTQg3i0JboDn3_s_TZrhnqcA2llN5cZO8Y10paZIs1NxoyQCa_Mf2sLbtO8DZuf1YDzCB1xwWS%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" alt="How we built a block-based template editor for gamification emails" width="1166" height="1212"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Conditional blocks&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fleshing Out the Editor
&lt;/h2&gt;

&lt;p&gt;Once we’d settled on the approach it was just a matter of building out all the block types and handling variables correctly. There are a couple interesting bits that I’ll note here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Variable Insertion
&lt;/h3&gt;

&lt;p&gt;We went with Mustache as it’s the simplest template language with basically zero logic. We want these email templates to use Conditional Blocks for logic—the text editor only needs to support rendering variables.&lt;/p&gt;

&lt;p&gt;To add Mustache to TipTap I wrote a custom extension, which also renders this nice dropdown menu with the available variables:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXeOOY38NSWMSAN8c4GJ4hI9QEn3aGL7s3O-2JyTKkxVInmP6Gry4tJ0kFI4Kz9-DXIkPFrwTHNPz-IpS9-7_cXzUP7M4p4JQKhskoaEoUybhV97F2ZdZyNgWlL-agPKcD1zEJBwng%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXeOOY38NSWMSAN8c4GJ4hI9QEn3aGL7s3O-2JyTKkxVInmP6Gry4tJ0kFI4Kz9-DXIkPFrwTHNPz-IpS9-7_cXzUP7M4p4JQKhskoaEoUybhV97F2ZdZyNgWlL-agPKcD1zEJBwng%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" alt="How we built a block-based template editor for gamification emails" width="1300" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Using variables&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I added a preview pane on the right-hand side where users can edit the preview variables used by Mustache to populate the template.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding "Smart" Blocks
&lt;/h3&gt;

&lt;p&gt;The email editor needed to support blocks powered by Trophy that you don’t find in other, more generic email template systems. So far we’ve added:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Achievement progress charts:&lt;/strong&gt; Shows a user’s progress toward a set of related achievements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXefrv3_NyG5pz3hKruymSxZoAtpGNrlD0KAagW9JSDssmE6hlNN75InLOIABUdI1k7vlXtBAm6ONdmf_FmD61ccESUwDe8Sb0hniQGZaVDrYEzxvFlhF-u0VleU_gKqOsAruMtZ%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXefrv3_NyG5pz3hKruymSxZoAtpGNrlD0KAagW9JSDssmE6hlNN75InLOIABUdI1k7vlXtBAm6ONdmf_FmD61ccESUwDe8Sb0hniQGZaVDrYEzxvFlhF-u0VleU_gKqOsAruMtZ%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" alt="How we built a block-based template editor for gamification emails" width="700" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Achievement progress chart block&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Achievement unlocked:&lt;/strong&gt; Shows an achievement that the user just unlocked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXc3nsxGKTfa9pf7iGiZmR1z9-FpfoNQ7uo5cPE8Bd4I5GvfnJUfRYAiIrq4q-xnmW202X2QeuVOMqusK6CgUCrqexRngUpjbRLa3IpVAJdzcsBek60jxaJFv1iUEjVFb5caShcb6g%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXc3nsxGKTfa9pf7iGiZmR1z9-FpfoNQ7uo5cPE8Bd4I5GvfnJUfRYAiIrq4q-xnmW202X2QeuVOMqusK6CgUCrqexRngUpjbRLa3IpVAJdzcsBek60jxaJFv1iUEjVFb5caShcb6g%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" alt="How we built a block-based template editor for gamification emails" width="1300" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Achievement unlocked block&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metric progress chart:&lt;/strong&gt; Shows a user’s progress over time on a particular metric.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXcj8gOgyKtns3PbDt92ynR1z3QI3i0Py47JBaIk08KfuIiYMUf97QmdrEslAnEAG6NtdQXIVdvv7NY_riq4A1m_L56DOaGLUYrQgQPnnd-1b7b-jXrnMKKgrwTWneMQWqSTZZfe%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXcj8gOgyKtns3PbDt92ynR1z3QI3i0Py47JBaIk08KfuIiYMUf97QmdrEslAnEAG6NtdQXIVdvv7NY_riq4A1m_L56DOaGLUYrQgQPnnd-1b7b-jXrnMKKgrwTWneMQWqSTZZfe%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" alt="How we built a block-based template editor for gamification emails" width="1300" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Metric progress chart block&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streak:&lt;/strong&gt; Shows the user’s current streak (if any).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXcRtnzla9Wa3ogW9HV6F52WxoTFq6fMJwCYo626aOteDWwMx3B4I-eXmLrjjfiC8DtoWPJ9FXqwrhCAbQFp2ONXBks4CBvNYpaHCJe1gDZFvkg_Pf0M5ykun-VVFh9hQtJ8V1HI%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh7-rt.googleusercontent.com%2Fdocsz%2FAD_4nXcRtnzla9Wa3ogW9HV6F52WxoTFq6fMJwCYo626aOteDWwMx3B4I-eXmLrjjfiC8DtoWPJ9FXqwrhCAbQFp2ONXBks4CBvNYpaHCJe1gDZFvkg_Pf0M5ykun-VVFh9hQtJ8V1HI%3Fkey%3DIeB-Htyx2JuJkgewLu25Vg" alt="How we built a block-based template editor for gamification emails" width="1300" height="720"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Streak block&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;I learned quite a lot from this!&lt;/p&gt;

&lt;p&gt;First and foremost: the Notion editor style isn’t for everyone. I think we just assumed that it would work by default, but different types of editors really do require different UX. &lt;/p&gt;

&lt;p&gt;Aside from that, I must say that TipTap is getting better and better; I can’t imagine starting a new project in 2025 and going with a different solution for rich text editing.&lt;/p&gt;

&lt;p&gt;Detailed docs on the template editor and how it works are available &lt;a href="https://docs.trophy.so/platform/emails?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;&lt;u&gt;here&lt;/u&gt;&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>email</category>
      <category>gamification</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why affiliates should get equity, not cash</title>
      <dc:creator>Charlie Brinicombe</dc:creator>
      <pubDate>Mon, 30 Jun 2025 14:11:42 +0000</pubDate>
      <link>https://forem.com/charlie_brinicombe/the-creator-equity-strategy-when-to-give-away-ownership-to-build-your-community-1dda</link>
      <guid>https://forem.com/charlie_brinicombe/the-creator-equity-strategy-when-to-give-away-ownership-to-build-your-community-1dda</guid>
      <description>&lt;p&gt;Most B2C startups approach creator partnerships with a simple playbook: offer affiliate commissions and hope for the best. But according to Tim Johnson, who's managed creator communities at three major consumer apps including Wattpad (acquired for $600M), this approach is fundamentally flawed.&lt;/p&gt;

&lt;p&gt;In a recent episode of the &lt;a href="https://trophy.so/podcast/tim-johnson-wattpad-couply-blossom?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;Levels Podcast&lt;/a&gt;, Tim shared hard-won insights from building creator communities at Wattpad, launching his own app Couply, and now scaling Blossom Social's network of 125+ finance creators. His biggest revelation? Sometimes giving away equity is the key to unlocking sustainable creator partnerships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Traditional Affiliate Models Fall Short
&lt;/h2&gt;

&lt;p&gt;The problem with pure affiliate models isn't just the uncertainty—it's human psychology. As Tim explains from his experience across multiple platforms:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Most creators are going to say no to an affiliate model... they want guaranteed cash, right? If they can take a couple thousand dollars, they'll take that over some affiliate thing because they can't, they don't have faith that they'll be able to drive enough downloads."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This reality forces early-stage startups into an impossible position. They can't compete with established platforms offering guaranteed payments, but they also can't afford to pay creators upfront without proven ROI. The solution, Tim discovered, lies in thinking beyond traditional compensation models.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Community-First Approach
&lt;/h2&gt;

&lt;p&gt;At Blossom Social, Tim and his team cracked this puzzle by building something more valuable than cash: genuine community. Their &lt;a href="https://trophy.so/blog/the-creator-economy-playbook-managing-125-creators-without-losing-your-mind?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;125-creator network&lt;/a&gt; operates on multiple levels of engagement, from tracking downloadable rewards to exclusive events and speaking opportunities.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It can be really lonely being a creator, you know? So building that community people has been the key."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the real breakthrough came when Blossom started offering creators equity in the company itself. This wasn't just about compensation—it was about alignment and belonging.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I think Blossom has seen a very significant number of their creators invest their own personal money in the company as well. So now they own a piece of the company and they're helping grow it. I think that's super powerful."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When to Deploy the Equity Strategy
&lt;/h2&gt;

&lt;p&gt;Tim's advice on timing is crucial: equity becomes most effective when you have product-market fit, but it shouldn't be reserved only for later stages. Looking back at his experience with Couply, he wishes he'd been more aggressive with equity from the beginning:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If I'd have to do Couply again, I'd have been more generous with equity, with larger creators, and I'd have brought them on probably as paid... I would have got some more salaries out there and then brought them on in like formal capacity."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The key is identifying creators who are genuinely passionate about your space and want to build something lasting. For relationship content creators working with Couply, or finance creators joining Blossom, the app represents more than just another revenue stream—it's a platform for their life's work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structuring Creator Equity Deals
&lt;/h2&gt;

&lt;p&gt;Tim recommends a multi-layered approach that combines several elements:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Equity Stakes for Key Creators&lt;/strong&gt; Offer meaningful equity to creators who demonstrate long-term commitment and alignment with your vision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Investment Opportunities&lt;/strong&gt; Allow creators to invest their own money in funding rounds, deepening their financial stake in success.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Salary + Equity for Core Partners&lt;/strong&gt; Bring top creators on as formal team members with both immediate compensation and long-term upside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Community Benefits&lt;/strong&gt; Maintain the intangible benefits that pure cash can't replicate—exclusive access, networking opportunities, and speaking platforms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Product-Market Fit Equation
&lt;/h2&gt;

&lt;p&gt;One critical insight from Tim's experience: the effectiveness of equity-based creator partnerships depends heavily on your startup's trajectory.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"All of this changes when you have product market fit, by the way... If you have product market fit and you know that your company is going to be a fucking unicorn, then it's in their best interest to get some equity and invest."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But even without proven product-market fit, Tim argues the strategy is worth pursuing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It doesn't matter anyway, because if you don't have product market fit, you're dead anyway."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This perspective reflects a fundamental truth about early-stage startups: you need to take calculated risks to break through the noise, and creator equity can be one of your most powerful tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons from the Trenches
&lt;/h2&gt;

&lt;p&gt;Tim's biggest regret with Couply wasn't the app's challenges—it was waiting too long to prioritize creator relationships:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I wish I had done that from day one... the things that stopped me from doing it was a sense of imposter syndrome... I didn't scale it. And I think if I'd have done that, really, really early on, it would have definitely helped the app grow bigger in a better way."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The lesson is clear: creator relationships aren't a nice-to-have for later-stage growth—they're foundational to building sustainable B2C products.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making It Work for Early-Stage Startups
&lt;/h2&gt;

&lt;p&gt;For founders without Blossom's resources to host cruises or major events, Tim's advice is simple: start with what matters most.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'd offered equity probably to the first few bigger creators that I could get on... equity plus affiliate plus the ability to invest in the product as well."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The goal isn't to replicate someone else's creator program—it's to build authentic relationships with people who believe in your vision and want to see it succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rethink affiliate models&lt;/strong&gt; : Pure commission-based partnerships often fail because creators prefer guaranteed income&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lead with community&lt;/strong&gt; : The most valuable thing you can offer creators is connection with like-minded peers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use equity strategically&lt;/strong&gt; : Offer ownership stakes to creators who demonstrate genuine commitment to your space&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start early&lt;/strong&gt; : Don't wait for product-market fit to begin building creator relationships&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Combine approaches&lt;/strong&gt; : The most effective creator partnerships blend equity, cash, community, and growth opportunities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Think long-term&lt;/strong&gt; : Focus on creators who want to build something lasting in your vertical, not just earn quick commissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building a creator community isn't just about growth—it's about creating a sustainable ecosystem where everyone wins. By thinking beyond traditional compensation models and embracing equity as a community-building tool, early-stage startups can compete with much larger platforms and build lasting partnerships that scale.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Want to hear more insights from Tim Johnson on building B2C apps, brand partnerships, and creator communities?&lt;/em&gt; &lt;a href="https://trophy.so/podcast/tim-johnson-wattpad-couply-blossom?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;&lt;em&gt;Listen to the full episode&lt;/em&gt;&lt;/a&gt; &lt;em&gt;of the Levels Podcast.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>podcast</category>
      <category>b2c</category>
      <category>growth</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Building Community Over Compensation: Why Creators Need More Than Money</title>
      <dc:creator>Charlie Brinicombe</dc:creator>
      <pubDate>Sun, 29 Jun 2025 14:09:32 +0000</pubDate>
      <link>https://forem.com/charlie_brinicombe/building-community-over-compensation-why-creators-need-more-than-money-46n8</link>
      <guid>https://forem.com/charlie_brinicombe/building-community-over-compensation-why-creators-need-more-than-money-46n8</guid>
      <description>&lt;p&gt;When most B2C startups think about creator partnerships, the conversation typically starts and ends with money. How much should we pay? What's the going rate? Can we afford this influencer? But according to Tim Johnson, who's managed creator relationships at three successful consumer apps including Wattpad (acquired for $600M) and currently heads brand partnerships at Blossom Social, this cash-first approach is fundamentally flawed.&lt;/p&gt;

&lt;p&gt;Tim recently joined the &lt;a href="https://trophy.so/podcast?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;Levels Podcast&lt;/a&gt; to share insights from his unique journey across multiple B2C platforms, and his perspective on creator partnerships challenges conventional wisdom. At Blossom, he oversees relationships with 125+ finance creators, and what he's learned might surprise founders who think they need deep pockets to compete for creator attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Creator Compensation Paradox
&lt;/h2&gt;

&lt;p&gt;The reality of creator partnerships is more nuanced than most founders realize. As Tim explains:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Most creators are going to say no to an affiliate model. Outside of that, there is the trips that Blossom throws. There's the speaking opportunities. There's the event series that we run... for creators, many of them have a specific creator that they might have followed or been inspired by, and getting to meet them, go on a cruise with them... It's incredibly valuable."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This insight reveals a fundamental truth: creators, like all professionals, are motivated by more than just immediate financial gain. They're building careers, seeking community, and looking for opportunities that advance their long-term goals.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Creators Really Want
&lt;/h2&gt;

&lt;p&gt;Tim's experience shows that successful creator partnerships are built on understanding what creators value beyond compensation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community and Connection&lt;/strong&gt; : "It can be really lonely being a creator," Tim notes. The isolation of content creation makes community particularly valuable. At Blossom, creators don't just get paid – they get access to a network of 125 like-minded finance creators.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Professional Development&lt;/strong&gt; : Speaking opportunities, masterminds, and events provide creators with career advancement that pure cash can't buy. These experiences help creators build their personal brands and expand their reach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exclusive Access&lt;/strong&gt; : Being part of an inner circle, getting early insights, or having first access to new features creates a sense of exclusivity that money can't replicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Equity Strategy
&lt;/h2&gt;

&lt;p&gt;For startups with limited cash but big ambitions, Tim suggests a bold approach:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If I'd have to do Couply again, I'd have been more generous with equity, with larger creators, and I'd have brought them on probably as paid... I think I would have got some more salaries out there and then brought them on in like formal capacity."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This equity-plus-affiliate model works particularly well when you have product-market fit. As Tim explains:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If you have product market fit and you know that your company is going to be a fucking unicorn, then it's in their best interest to get some equity and invest."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At Blossom, many creators have actually invested their own money in the company, creating true alignment between the platform's success and the creators' financial outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Without Big Budgets
&lt;/h2&gt;

&lt;p&gt;For early-stage companies that can't afford cruises or major events, Tim's advice is to start with what you can control:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Equity Over Cash&lt;/strong&gt; : Give creators ownership in your success story. A small equity stake in a growing company can be worth more than one-time payments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create Exclusive Experiences&lt;/strong&gt; : Even simple networking dinners or virtual masterminds can provide immense value to creators looking to connect with peers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Facilitate Creator-to-Creator Value&lt;/strong&gt; : Help your creators help each other. Introductions, collaborations, and cross-promotion opportunities cost nothing but create lasting relationships.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Long-Term Advantage
&lt;/h2&gt;

&lt;p&gt;This community-first approach creates a sustainable competitive advantage. While competitors might outbid you for individual creators, they can't easily replicate a thriving community. As Tim learned from his experience:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Community is the core of everything that Blossom does. And it's building a creative community where people come in, make friends, meet other creators, boost each other's stuff. Like it's building a big friendship network."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Lessons for B2C Founders
&lt;/h2&gt;

&lt;p&gt;Tim's insights offer several key takeaways for B2C founders looking to build creator partnerships:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with relationship building, not transactions&lt;/strong&gt; : Think long-term partnerships, not one-off campaigns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understand creator motivations beyond money&lt;/strong&gt; : Professional development, community, and exclusive access often matter more than cash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use equity strategically&lt;/strong&gt; : When cash is limited, equity can align incentives and create deeper partnerships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build systems for scale&lt;/strong&gt; : Managing 125+ creators requires structure, tracking, and clear progression paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create genuine value&lt;/strong&gt; : The best creator partnerships solve real problems for creators, not just for your startup.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;p&gt;• Creator partnerships require more than just financial compensation to be successful&lt;/p&gt;

&lt;p&gt;• Community building and professional development opportunities often outweigh pure cash incentive&lt;/p&gt;

&lt;p&gt;• Equity partnerships can align creator success with company growth, especially for startups with limited budgets&lt;/p&gt;

&lt;p&gt;• Building exclusive experiences and facilitating creator-to-creator connections creates lasting value&lt;/p&gt;

&lt;p&gt;• A community-first approach creates sustainable competitive advantages that competitors can't easily replicate&lt;/p&gt;

&lt;p&gt;• Even simple networking opportunities can provide immense value to creators seeking peer connections&lt;/p&gt;

&lt;p&gt;• Managing large creator networks requires &lt;a href="https://trophy.so/blog/the-creator-economy-playbook-managing-125-creators-without-losing-your-mind?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;structured systems&lt;/a&gt; and clear progression paths&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Want to hear more insights from Tim Johnson on building successful B2C apps? Listen to the&lt;/em&gt; &lt;a href="https://trophy.so/podcast/tim-johnson-wattpad-couply-blossom?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;&lt;em&gt;full conversation&lt;/em&gt;&lt;/a&gt; &lt;em&gt;on the Levels Podcast where he shares lessons from Wattpad's $600M exit and building brand partnerships at scale.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>podcast</category>
      <category>b2c</category>
      <category>growth</category>
      <category>mobile</category>
    </item>
    <item>
      <title>The Anti-ChatGPT Sales Approach: Why Personalization Still Wins</title>
      <dc:creator>Charlie Brinicombe</dc:creator>
      <pubDate>Sat, 28 Jun 2025 14:06:28 +0000</pubDate>
      <link>https://forem.com/charlie_brinicombe/the-anti-chatgpt-sales-approach-why-personalization-still-wins-1cj7</link>
      <guid>https://forem.com/charlie_brinicombe/the-anti-chatgpt-sales-approach-why-personalization-still-wins-1cj7</guid>
      <description>&lt;p&gt;In an era where AI-generated emails flood inboxes and ChatGPT-powered outreach has become the norm, one sales professional is bucking the trend with remarkable results. Tim Johnson, Head of Brand Partnerships at Blossom Social and former Head of Brand Partnerships at Wattpad (acquired for $753 million), has achieved something most salespeople can only dream of: an 80% response rate to his outbound emails.&lt;/p&gt;

&lt;p&gt;On the latest episode of the &lt;a href="https://trophy.so/podcast/tim-johnson-wattpad-couply-blossom?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;Levels Podcast&lt;/a&gt;, Tim shared his contrarian approach to B2B sales that prioritizes human connection over automation, quality over quantity, and genuine relationship building over transactional interactions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with AI-Powered Outreach
&lt;/h2&gt;

&lt;p&gt;Tim's frustration with the current state of sales outreach is palpable. As someone who receives dozens of B2B emails daily, he's witnessed firsthand how AI has degraded the quality of business communication.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I get outreach every single day by B2B folks. I had two which were customized. One was from Datadog and one was from TikTok. So of the outreach that I got, two were actually personalized to me and what I was doing. The rest were written by ChatGPT. And if you think that we can't tell, ChatGpt wrote your outreach email, then you're wrong. It's just so obvious, right?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The tell-tale signs are everywhere: generic weather references, obvious template language, and a complete lack of understanding about the recipient's actual business or challenges. Tim describes the typical AI-generated approach:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Like you can't even spend the time. You can only spend like one minute to actually see who I am. Why would I spend one minute in responding to you? No."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Power of Genuine Personalization
&lt;/h2&gt;

&lt;p&gt;What made those two successful outreach attempts stand out? They demonstrated actual research and provided real value. Datadog's email addressed Tim's specific business problems, while TikTok took a different but equally effective approach.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"TikTok invited me to a dinner to meet interesting creators in my vertical and meet other app founders. Are you fucking kidding me? You bet I'm going to that dinner, right? I wanna meet interesting creators, I meet app founders, like down straight, this is amazing."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This example illustrates a crucial principle: instead of pushing your message out, focus on attracting prospects to you by offering genuine value. The TikTok invitation wasn't just personalized—it was irresistible because it solved multiple problems for Tim simultaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stag Hunt vs. Squirrel Hunt Philosophy
&lt;/h2&gt;

&lt;p&gt;Tim's &lt;a href="https://trophy.so/blog/stag-hunting-vs-squirrel-hunting-a-better-approach-to-b2b-sales-for-consumer-apps?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;approach to sales&lt;/a&gt; is built around what he calls "stag hunting versus squirrel hunting"—a metaphor that fundamentally changes how you think about prospecting and client relationships.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I think a stag hunt versus a squirrel hunt feels like a very, very different thing. With a stag hunt, you're organized, you are focused, you know what you're looking at. You're looking at this big creature that's gonna feed you for a long, long time, right? And it's a very, very deliberate process going towards working with that partner. Versus a squirrel, you're sort of like chasing after it, expending a bunch of energy, and then it's not gonna last long term anyway."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This philosophy drives Tim to focus on high-value prospects who meet specific criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They have a repeating budget&lt;/li&gt;
&lt;li&gt;They're consistently looking for your audience&lt;/li&gt;
&lt;li&gt;They have a realistic timeframe for decision-making&lt;/li&gt;
&lt;li&gt;They can integrate technically with your solution&lt;/li&gt;
&lt;li&gt;Success metrics can be clearly defined and agreed upon&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building Relationships, Not Closing Deals
&lt;/h2&gt;

&lt;p&gt;Perhaps most importantly, Tim rejects the traditional "closing" mentality that dominates sales training. His approach is more collaborative and relationship-focused.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I wouldn't say that there's any like maybe the stag analogy like isn't good for the going in for the kill because there's this pressure in like sales of like closing... That's definitely not the way that I do sales because there's no big close. There's no like big reveal where you put this big price on and like you try and trap them into it somehow."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead, he uses an analogy of gradually building trust: "It's like a very ongoing process. I guess it's more like you have like this food and you're putting like the food towards the stag and the stag is coming towards you and then you're like putting like, can I put like a harness on you? And they're like, yeah, okay, you can put a harness. Okay, cool. Can I put this saddle on you? Is that okay?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The Event-Based Relationship Strategy
&lt;/h2&gt;

&lt;p&gt;One of Tim's most effective tactics involves hosting carefully curated events that bring prospects into his orbit naturally. Rather than cold outreach, he creates opportunities for meaningful connections.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"It's just as simple. It's a dinner or it's a small client event. I've got a nice bar, come, grab some cocktails and learn something and meet some people. It's a nice little mix of all three. See what your competitors are doing. You do that and you invite this person."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These events work because they address different personality types simultaneously: detail-oriented people are attracted by insights, socialites by networking opportunities, and competitive individuals by seeing what others in their space are doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quality Over Quantity: The Four-Hour Video Principle
&lt;/h2&gt;

&lt;p&gt;Tim draws inspiration from creator McKenna Murphy, who spends four hours creating a one-minute video. When questioned about this seemingly inefficient approach, Murphy explained: "I noticed if I spent four hours on my one minute video, it was more likely to get a million views. And if I spent less than four hours, then it would get between like 10K and 20K. So there's an outsize return on me spending longer on it."&lt;/p&gt;

&lt;p&gt;This principle applies directly to sales outreach. The extra time invested in genuine personalization and relationship building yields dramatically better results than mass, generic outreach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Human Element in a Digital World
&lt;/h2&gt;

&lt;p&gt;Tim's success ultimately comes down to treating prospects like human beings rather than entries in a CRM system. He emphasizes being "this fully fleshed out person that is interested in the space and has expertise" rather than "this one person that does one tool-like thing."&lt;/p&gt;

&lt;p&gt;The key is becoming a valuable resource for your prospects—sharing industry insights, making introductions, and genuinely helping them succeed, whether or not they become customers.&lt;/p&gt;

&lt;p&gt;Since joining Blossom, Tim has brought in around $2 million for the seed-stage company using these relationship-focused methods, proving that in an age of AI automation, the human touch remains irreplaceably valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;p&gt;• &lt;strong&gt;Personalization beats automation&lt;/strong&gt; : Even a few minutes of genuine research outperforms AI-generated templates&lt;br&gt;&lt;br&gt;
• &lt;strong&gt;Focus on "stag hunting"&lt;/strong&gt; : Target fewer, higher-value prospects for long-term relationships&lt;br&gt;&lt;br&gt;
• &lt;strong&gt;Create value before asking for it&lt;/strong&gt; : Host events, share insights, make introductions&lt;br&gt;&lt;br&gt;
• &lt;strong&gt;Build relationships, don't close deals&lt;/strong&gt; : Think partnership, not transaction&lt;br&gt;&lt;br&gt;
• &lt;strong&gt;Quality over quantity&lt;/strong&gt; : Better to send 10 thoughtful emails than 1000 generic ones&lt;br&gt;&lt;br&gt;
• &lt;strong&gt;Be a resource, not a vendor&lt;/strong&gt; : Establish expertise and provide ongoing value to your network&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Ready to learn more strategies for scaling your consumer app? Listen to the&lt;/em&gt;&lt;a href="https://trophy.so/podcast?ref=trophy.ghost.io" rel="noopener noreferrer"&gt; full conversation&lt;/a&gt;_ with Tim Johnson on the Levels Podcast._&lt;/p&gt;

</description>
      <category>podcast</category>
      <category>b2c</category>
      <category>growth</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Predicting the Next Big Thing: How Wattpad Spotted BTS Before Anyone Else</title>
      <dc:creator>Charlie Brinicombe</dc:creator>
      <pubDate>Thu, 26 Jun 2025 14:02:10 +0000</pubDate>
      <link>https://forem.com/charlie_brinicombe/predicting-the-next-big-thing-how-wattpad-spotted-bts-before-anyone-else-4dpk</link>
      <guid>https://forem.com/charlie_brinicombe/predicting-the-next-big-thing-how-wattpad-spotted-bts-before-anyone-else-4dpk</guid>
      <description>&lt;p&gt;In the world of B2C startups, data is often viewed through the lens of user engagement, retention metrics, and conversion rates. But what if your app's data could predict the next cultural phenomenon before it breaks into the mainstream?&lt;/p&gt;

&lt;p&gt;This isn't science fiction—it's exactly what happened at Wattpad, the storytelling platform that identified BTS as the next big thing back in 2016, years before the Korean boy band became a global sensation.&lt;/p&gt;

&lt;p&gt;On the latest episode of the &lt;a href="https://trophy.so/podcast?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;Levels Podcast&lt;/a&gt;, we sat down with Tim Johnson, former Head of Brand Partnerships at Wattpad and current Head of Brand Partnerships at Blossom Social. Tim shared fascinating insights into how consumer apps can become crystal balls for cultural trends, and more importantly, how startups can monetize these predictive capabilities.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Power of Fan Fiction as Cultural Indicator
&lt;/h2&gt;

&lt;p&gt;Wattpad's ability to spot BTS early wasn't luck—it was data science in action. As Tim explained during our conversation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We knew in advance that BTS would be absolutely massive boy-bound. We knew it in 2016. I went and presented that to media agencies and marketing agencies and said, hey guys, there's this Korean band that's about to break the internet."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The secret weapon? Fan fiction. Wattpad's platform, which Tim describes as "like YouTube, but for books and stories," became an unexpected forecasting tool for entertainment trends. The platform's data showed an exponential rise in BTS fan fiction, signaling massive fan engagement before the band hit mainstream Western audiences.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We were predicting that back in 2015, 2016. So yeah, that's kind of the way to leverage some of the insights... we could see the fan fiction start to rise exponentially. And with that, that meant it was an early signal that this band, this group was coming in."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  From Data to Dollars: Monetizing Predictive Insights
&lt;/h2&gt;

&lt;p&gt;Identifying trends is one thing; turning that knowledge into revenue is another. Tim revealed how Wattpad &lt;a href="https://trophy.so/blog/why-70-of-blossoms-revenue-comes-from-brand-partnerships-not-subscriptions?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;monetized&lt;/a&gt; their cultural forecasting abilities by packaging insights with other services for entertainment companies.&lt;/p&gt;

&lt;p&gt;The value proposition was compelling: entertainment companies invest massive upfront costs in movies, TV shows, and other content, hoping for returns years down the line. Having early indicators of what will be popular provides a significant competitive advantage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"For a movie company, you want to understand like what trends are happening and why are they happening. So that was very, very interesting to the entertainment industry to be able to predict things that are going to be big next year."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tim noted that books serve as particularly strong early predictors for other entertainment formats. The BTS prediction proved prescient not just for the band itself, but for the broader wave of Korean cultural content that followed, including hits like Squid Game.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"That meant K-pop was gonna be getting big. That meant perhaps other things around like Korea or interest around Korea was gonna get big. You start to see a lot more Korean based books. And I mean, that was been absolutely true."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The ASSET Framework for Network Effects
&lt;/h2&gt;

&lt;p&gt;Wattpad's predictive capabilities stem from their network effects, which Tim breaks down using the &lt;a href="https://trophy.so/blog/the-network-effects-playbook-how-consumer-apps-build-unstoppable-growth-engines?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;ASSET framework&lt;/a&gt; developed by Wattpad's founders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A&lt;/strong&gt; tomic unit: The core value driver (books on Wattpad, portfolios on Blossom)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S&lt;/strong&gt; eed supply: Bringing on creators and content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S&lt;/strong&gt; cale demand: Advertising to bring in users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E&lt;/strong&gt; nlarge network effects: Users become contributors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;T&lt;/strong&gt; rack proprietary insights: Data becomes valuable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "T" in this framework—tracking proprietary insights—is where the magic happens for trend prediction. As Tim explains:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This is the idea of now your data that you're building, your data set becomes valuable... These proprietary insights become very valuable as well."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Lessons for B2C Startups
&lt;/h2&gt;

&lt;p&gt;For startup founders, Wattpad's story offers several key takeaways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Your User Data is More Valuable Than You Think&lt;/strong&gt; Consumer behavior patterns on your platform might reveal trends beyond your immediate industry. Whether it's reading preferences, investment choices, or relationship patterns, user behavior can signal broader cultural shifts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Package Insights with Core Services&lt;/strong&gt; Rather than selling raw data, Tim suggests packaging insights with other valuable services. Wattpad bundled trend predictions with IP partnerships and first-look deals for entertainment companies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Focus on Leading Indicators&lt;/strong&gt; Books proved to be leading indicators for broader entertainment trends. Consider what leading indicators might exist within your user base and industry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Think Beyond Direct Monetization&lt;/strong&gt; While Wattpad's primary revenue came from brand partnerships, their trend prediction capabilities added significant value to these relationships and opened new revenue streams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Your Own Crystal Ball
&lt;/h2&gt;

&lt;p&gt;Tim's experience at both Wattpad and now Blossom Social demonstrates that any consumer platform with engaged users can potentially develop predictive capabilities. At Blossom, they're applying similar principles to investment trends:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"With Blossom, we can see what investors are investing in in real time, which is even more valuable because we can see where the market is going immediately. We can also see sentiment across different verticals of investing."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The key is recognizing that your users aren't just customers—they're early adopters whose collective behavior might predict broader market movements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Consumer app data can serve as powerful early indicators for cultural and market trends&lt;/li&gt;
&lt;li&gt;Fan fiction and user-generated content often signal mainstream trends before they break&lt;/li&gt;
&lt;li&gt;Predictive insights become valuable when packaged with other services for B2B clients&lt;/li&gt;
&lt;li&gt;The ASSET framework helps identify where proprietary insights fit into your network effects&lt;/li&gt;
&lt;li&gt;Entertainment companies and other industries value early trend identification for strategic planning&lt;/li&gt;
&lt;li&gt;Books and storytelling platforms can be particularly strong predictors for broader entertainment trends&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Want to hear more insights from successful B2C founders? Listen to the&lt;/em&gt; &lt;a href="https://trophy.so/podcast/tim-johnson-wattpad-couply-blossom?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;&lt;em&gt;full conversation&lt;/em&gt;&lt;/a&gt; &lt;em&gt;with Tim Johnson on the Levels Podcast, where we dive deep into brand partnerships, network effects, and building consumer apps that scale.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>podcast</category>
      <category>b2c</category>
      <category>growth</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Why I shelved my mobile app after 500k downloads</title>
      <dc:creator>Charlie Brinicombe</dc:creator>
      <pubDate>Thu, 26 Jun 2025 13:55:13 +0000</pubDate>
      <link>https://forem.com/charlie_brinicombe/from-500k-downloads-to-profitable-side-project-the-couply-pivot-story-4inn</link>
      <guid>https://forem.com/charlie_brinicombe/from-500k-downloads-to-profitable-side-project-the-couply-pivot-story-4inn</guid>
      <description>&lt;p&gt;Not every startup needs to become a unicorn to be successful. Sometimes the most valuable lesson an entrepreneur can learn is when to pivot from venture-scale ambitions to building a sustainable, profitable business. This is exactly what Tim Johnson discovered with Couply, his couples relationship app that achieved impressive early metrics but ultimately taught him that product-market fit doesn't always equal venture scalability.&lt;/p&gt;

&lt;p&gt;In a recent episode of the &lt;a href="https://trophy.so/podcast?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;Levels Podcast&lt;/a&gt;, Tim Johnson—former head of brand partnerships at Wattpad (acquired for $753M CAD) and current head of brand partnerships at Blossom Social—shared the candid story of how he transitioned Couply from a struggling startup to a profitable side project. His journey offers valuable insights for B2C founders facing similar crossroads.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Early Promise and Hidden Challenges
&lt;/h2&gt;

&lt;p&gt;Couply started with all the right signals. Tim had identified a clear gap in the market while reading relationship books with his girlfriend:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I thought, it'd be really cool if I could like go through this with my girlfriend. But I don't want to go through it with her in a book. Why do we have to mark this stuff down on paper and answer, do these quizzes, and then answer it up at the bottom? Why don't we do this in an app?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The initial validation was strong. Tim bought the domain, created a landing page, and ran Google ads that achieved a remarkable 5% click-through rate for waitlist signups. The app eventually reached 500,000 downloads organically and became the number one couples app on Google Play Store.&lt;/p&gt;

&lt;p&gt;However, beneath these impressive metrics lay a fundamental problem that would prove impossible to solve within their runway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Horsemen of Churn
&lt;/h2&gt;

&lt;p&gt;Tim discovered what he calls the "&lt;a href="https://trophy.so/blog/the-four-horsemen-of-churn-why-even-loved-products-lose-users?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;four horsemen of the churn apocalypse&lt;/a&gt;"—a unique retention challenge that plagues problem-solving consumer apps, particularly those requiring two users:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"People wanted to use Couply to solve their problems, and then they churn. So they both like it. They both like it, and it works. They churn. Neither of like it. They churn. One likes it, one doesn't like it. Well, it's a couples app, so it needs both of you doing stuff. They churn. The other one likes it, the other one doesn't. Same thing, churn."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This insight reveals a critical challenge for consumer apps that solve immediate problems rather than providing ongoing entertainment or utility. Couply became a tool that couples used when their relationship was "in a pickle," but once the problem was solved, users naturally moved on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recognizing the Inflection Point
&lt;/h2&gt;

&lt;p&gt;The decision to pivot wasn't made lightly. Tim and his co-founder had built a product that users genuinely loved and that effectively solved relationship problems. But loving a product and continuing to use it are two different things:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The more I spoke to users, so Couply is an app that helps couples get out of a, when they're in a bit of a pickle. So every relationship goes through ups and downs, right? And when your relationship is going through a down phase, you need help, you need help fast."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tim realized that Couply had become the relationship equivalent of emergency services—essential when needed, but not something people wanted to keep around once the crisis passed. The app's success as a problem-solver was actually limiting its potential for sustained engagement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Strategic Transition
&lt;/h2&gt;

&lt;p&gt;Rather than continuing to burn through runway fighting an unsolvable retention problem, Tim made the strategic decision to transition Couply to a side project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I felt like it would be better served as a more of like a side project. So the company became a side project for us and that left me open to get back into the game."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This wasn't a failure—it was a recognition that different business models serve different purposes. The transition allowed Tim to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preserve the value they'd already created&lt;/li&gt;
&lt;li&gt;Generate sustainable passive income&lt;/li&gt;
&lt;li&gt;Free up time and energy for new opportunities&lt;/li&gt;
&lt;li&gt;Work toward paying back investors&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building Sustainable Passive Income
&lt;/h2&gt;

&lt;p&gt;Today, Couply operates as a profitable side project that demonstrates how consumer apps can generate sustainable revenue without venture-scale growth:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Couply is profitable, it's making money, it's helping couples, and we are very close to paying all our investors back. And then it will make me and my co-founder an incredible passive income for as long as we keep it going."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The team maintains the app with minimal time investment—meeting for a few hours every weekend and conducting monthly team meetings. They've also maintained brand partnerships, working with companies targeting millennial couples, including therapy platforms like BetterHelp and Regain, as well as home insurance and mortgage companies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for B2C Founders
&lt;/h2&gt;

&lt;p&gt;Tim's experience with Couply offers several critical insights for consumer app founders:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understand your app's natural usage pattern:&lt;/strong&gt; Problem-solving apps have different retention characteristics than entertainment or utility apps. If your app solves a specific problem effectively, users may naturally churn after success.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revenue diversification matters:&lt;/strong&gt; Even as a side project, Couply maintains multiple revenue streams through subscriptions and brand partnerships, providing stability and growth potential.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Know when to pivot business models:&lt;/strong&gt; Sometimes the most successful outcome isn't venture scale but sustainable profitability. Recognizing this early can preserve value and open new opportunities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leverage your experience:&lt;/strong&gt; Tim's experience building Couply made him "so much more of a weapon" for subsequent startups, contributing to his success at Blossom Social.&lt;/p&gt;

&lt;p&gt;Tim's journey from startup founder back to early-stage team member at Blossom demonstrates that there's no single path to success in consumer apps. Sometimes the most valuable thing you can build is the experience and expertise that will power your next venture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Product-market fit doesn't automatically equal venture scalability—some successful products are better suited to different business models&lt;/li&gt;
&lt;li&gt;Problem-solving apps face unique retention challenges, especially those requiring multiple users&lt;/li&gt;
&lt;li&gt;The "four horsemen of churn" can affect any app where success leads to natural user departure&lt;/li&gt;
&lt;li&gt;Transitioning to a side project can preserve value while generating sustainable passive income&lt;/li&gt;
&lt;li&gt;Brand partnerships can provide significant revenue even for smaller consumer apps&lt;/li&gt;
&lt;li&gt;Founder experience from "failed" startups often becomes invaluable for future ventures&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Ready to dive deeper into consumer app insights? Listen to the&lt;/em&gt; &lt;a href="https://trophy.so/podcast/tim-johnson-wattpad-couply-blossom?ref=trophy.ghost.io" rel="noopener noreferrer"&gt;&lt;em&gt;full conversation&lt;/em&gt;&lt;/a&gt; &lt;em&gt;with Tim Johnson on the Levels Podcast where he shares more about building network effects, scaling brand partnerships, and growing B2C platforms.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>podcast</category>
      <category>b2c</category>
      <category>growth</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
