RORK LABJP
DEADLINE — Four days remain until Google Play requires Android 16 (API level 36). From August 31 it applies to new apps and to updates of existing ones alikeRULES — The submission rule and the visibility rule are separate. An app you have stopped updating still disappears for new users on newer devices if it targets below API 35EXTENSION — An extension keeps you shipping to all users until November 1, but the form lives in Play Console and has to be filed before the deadline passesEXPO — Expo SDK 57 moves React Native from 0.85 to 0.86 while React stays at 19.2, and 0.86 is intended to land without breaking changesHERMES — 57.0.9 updates React Native to 0.86.2 and clears the Hermes V1 memory regression from SDK 56, which shows up in apps importing reanimated or workletsPREBUILD — expo prebuild now clears and regenerates the native android and ios directories by default, so hand-edited native changes vanish unless you audit for them firstDEADLINE — Four days remain until Google Play requires Android 16 (API level 36). From August 31 it applies to new apps and to updates of existing ones alikeRULES — The submission rule and the visibility rule are separate. An app you have stopped updating still disappears for new users on newer devices if it targets below API 35EXTENSION — An extension keeps you shipping to all users until November 1, but the form lives in Play Console and has to be filed before the deadline passesEXPO — Expo SDK 57 moves React Native from 0.85 to 0.86 while React stays at 19.2, and 0.86 is intended to land without breaking changesHERMES — 57.0.9 updates React Native to 0.86.2 and clears the Hermes V1 memory regression from SDK 56, which shows up in apps importing reanimated or workletsPREBUILD — expo prebuild now clears and regenerates the native android and ios directories by default, so hand-edited native changes vanish unless you audit for them first
Articles/Business
Business/2026-05-11Advanced

Building Fortune & Manifestation Apps with Rork — Daily Delivery Design That Survives Day Three

Fortune apps lose most users in three days. A Rork design for daily delivery, push timing, and widgets that turns the niche into a habit.

manifestation appfortune appastrology appretention10push notifications11widgets3indie dev30Rork545

Premium Article

Building Fortune & Manifestation Apps with Rork — Daily Delivery Design That Survives Day Three

"I shipped a fortune app, the first day looked great, and by day three almost nobody was opening it." That was me, back in my early years of indie app development. The install graph spikes hard on day one and craters by day three, usually down to 10–20% of the peak. The ad revenue chart follows the same shape, and by the end of the month you're left wondering what that initial bump was even for.

There's a structural reason this niche is brutal on retention. Fortune, manifestation, and astrology apps die when you treat them like one-shot content. You have to design them so that the content visibly turns over every day, otherwise the user has no reason to come back. What follows is the playbook I keep returning to after years of running wallpaper, healing, and manifestation-genre apps as a solo developer.

The months where revenue actually moved were never the months I shipped a flashy feature. They trailed, by two or three months, a stretch of quietly fixing how the daily content reached people. Not once did any of this work out in one shot.

This is the design playbook I wish someone had handed me before my first fortune app. I'll show you the seed-based daily content generation that lets you skip server costs entirely, the notification timing philosophy that doubled my opt-in rate, the lock-screen widget pattern that increased app opens without cannibalizing them, the two-tier monetization model that earned more than pure subscription, and the small ethical decisions that determine whether your app feels trustworthy six months in. Everything here has been validated in shipped apps with real users.

Why Fortune Apps Lose at Retention — Three Structural Reasons

Before getting into solutions, it's worth naming the underlying causes. If you can't see them clearly, the fixes will keep missing the mark.

Reason 1: "Result content" and "daily habit content" get conflated

From the user's side, divination and manifestation look like content with a fixed result. "Today's reading" feels like something you consume once and you're done. To make tomorrow's session feel worth opening, you have to translate the "result" framing into a "daily habit" framing. That's a design choice, not a UI tweak.

Reason 2: Notification timing is chosen by implementation convenience, not philosophy

Plenty of apps fire morning notifications at 9 AM. Ask the developer why 9 AM, and the answer is often "evenings have low response rates, so we picked morning." This logic gives you a time that doesn't match the genre. Manifestation content and 9 AM rush hour share almost no emotional context.

Reason 3: Content "stock" and "flow" aren't designed separately

Writing fresh content every day is unsustainable for a solo developer. You need a stock (the base dataset of messages) and a flow (the rule that picks which message gets shown when). If you don't separate them, you'll burn through your content pool in three weeks.

These three are genre-specific. None of them are about technical skill — they're design decisions. Skip past them and dive into Rork implementation, and you'll get stuck in the same place every time.

The Core of Daily Delivery — "Unique Per User, Reproducible Within the Day"

The defining property of daily delivery is this: "The reading shown to User A today is uniquely theirs, and stays the same no matter how many times they open the app within that day." You can achieve this two ways: run a server batch job for every user every morning, or use a seeded pseudo-random generator on the client.

For indie scale, I strongly recommend the second approach. The reason is simple: maintaining one row per user per day on a server is enormously more expensive than seeding userId + date into a deterministic PRNG on the client.

Let's start with the core seeded PRNG. Rork uses TypeScript for local state, so I'll use Mulberry32, a tiny deterministic PRNG.

// src/lib/dailyFortune.ts
// Returns a deterministic fortune for (userId, date)
 
/**
 * Mulberry32: a tiny deterministic PRNG.
 * Same seed always yields the same sequence — that's exactly what we need.
 */
function mulberry32(seed: number): () => number {
  return function () {
    seed |= 0;
    seed = (seed + 0x6d2b79f5) | 0;
    let t = seed;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
 
/**
 * Hash a string to a 32-bit integer (FNV-1a).
 * We use this to convert (userId, dateString) into a numeric seed.
 */
function hash32(input: string): number {
  let h = 0x811c9dc5;
  for (let i = 0; i < input.length; i++) {
    h ^= input.charCodeAt(i);
    h = Math.imul(h, 0x01000193);
  }
  return h >>> 0;
}
 
export type FortuneResult = {
  date: string;
  rank: number;       // 1-12 (1 = best, 12 = worst)
  rankLabel: string;
  themeId: string;
  message: string;
};
 
const RANK_LABELS = [
  "Excellent", "Great", "Good", "Fair", "Mild", "Steady",
  "Neutral", "Cautious", "Light Dip", "Mild Dip", "Caution", "Be Careful",
] as const;
 
export function generateDailyFortune(
  userId: string,
  dateISO: string,
  messages: { themeId: string; rankMin: number; rankMax: number; body: string }[],
): FortuneResult {
  const seed = hash32(`${userId}:${dateISO}`);
  const rand = mulberry32(seed);
 
  // 12-tier rank with weights that make extremes rare
  const rankRoll = rand();
  let rank = 7;
  if (rankRoll < 0.05) rank = 1;
  else if (rankRoll < 0.20) rank = 2;
  else if (rankRoll < 0.55) rank = Math.floor(rand() * 4) + 3;
  else if (rankRoll < 0.85) rank = Math.floor(rand() * 4) + 6;
  else if (rankRoll < 0.97) rank = Math.floor(rand() * 2) + 9;
  else rank = 12;
 
  const candidates = messages.filter(
    (m) => rank >= m.rankMin && rank <= m.rankMax,
  );
  const picked = candidates[Math.floor(rand() * candidates.length)] ?? messages[0];
 
  return {
    date: dateISO,
    rank,
    rankLabel: RANK_LABELS[rank - 1] ?? "Good",
    themeId: picked.themeId,
    message: picked.body,
  };
}

The crucial property of this code is that it needs zero server roundtrips. The userId can simply be a UUID generated locally on first launch. Same date in, same result out — open the app a hundred times today and you get the same reading. Tomorrow gives you something else automatically.

One pitfall right at the implementation boundary: pulling new Date().toISOString().slice(0, 10) gives you a UTC-based date, which drifts off the user's perceived "today" near midnight. In my own apps, I switched to explicit local-time formatting and the reviews complaining "my fortune changed before midnight" disappeared.

// src/lib/dateInUserTimezone.ts
// Returns "today" in the user's local timezone, regardless of UTC offset
 
export function todayInUserTimezone(now: Date = new Date()): string {
  const fmt = new Intl.DateTimeFormat("en-CA", {
    timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  });
  // en-CA produces YYYY-MM-DD; ideal for our seed key
  return fmt.format(now);
}

Intl.DateTimeFormat picks up the device's timezone automatically, so international users get the right date with the same code. Several of my apps have meaningful user bases in Taiwan, Thailand, and Vietnam, and switching to this implementation killed the timezone-mismatch complaints for good.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Turn the all-too-common 'day-one install spike then 90% drop in three days' pattern into a habit-forming app, with a concrete daily-delivery design you can adapt and ship
Get working Rork code for seeded daily content generation, notification scheduling, and lock-screen widget integration that you can adjust and ship today
Develop a personal compass for balancing the spiritual context of these apps with technical implementation, monetization ethics, and long-term brand trust
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Business2026-06-22
A Wallpaper App's Real Work Starts After Launch — Content, Retention, and Revenue Notes from Running One on Rork
A wallpaper app is decided less by its launch and more by how you tend it afterward. Building on a Rork implementation, here are field notes on scheduled publishing that keeps content fresh, onboarding that survives the first week, and a revenue setup whose per-download return doesn't thin out over time.
Business2026-04-11
Retention and LTV for Rork Apps — Separating the Two Numbers I Kept Confusing
Install retention and subscription retention use different denominators. Starting from the LTV miscalculation that came from mixing them, this walks through reading Day1/Day7/Day30 correctly, costing in generation credits, and catching churn signals early — from an indie developer's desk.
Business2026-03-29
Growth Strategy for Rork Apps — A from User Acquisition to Retention
A systematic guide to growing apps built with Rork. Learn user acquisition channel design, onboarding optimization, push notification strategies, and retention improvement implementation patterns for data-driven growth.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →