◉RORK LABJP
●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●RORK — GPT-6.1 Sol (Sep 29) and Claude Sonnet 5.5 (Sep 28) are now in the model menu●RN 0.88 — The stable release was still unpublished on its Oct 12 target. rc.4 (Oct 7) is the newest; 0.87.1 is Latest●Q&A — Readers report expo-widgets showing a black widget in production builds only●WINDOWS — A walkthrough shows how to ship an iPhone app from Windows only, with no Mac●NEW — Three checks to run first when notifications stop arriving in Expo Go●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●RORK — GPT-6.1 Sol (Sep 29) and Claude Sonnet 5.5 (Sep 28) are now in the model menu●RN 0.88 — The stable release was still unpublished on its Oct 12 target. rc.4 (Oct 7) is the newest; 0.87.1 is Latest●Q&A — Readers report expo-widgets showing a black widget in production builds only●WINDOWS — A walkthrough shows how to ship an iPhone app from Windows only, with no Mac●NEW — Three checks to run first when notifications stop arriving in Expo Go
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 dev30Rork578

✦ 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-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-05-03
Making a Rork App Sell on the App Store — ASO, Pricing, and IAP Strategy
How to take a Rork-built app from invisible to ranked, priced right, and earning real money on the App Store. ASO, price psychology, IAP design, and subscription tactics drawn from running indie apps solo.
⟐ Business2026-05-03
From Rork to $1,000/month with AdMob — A Practical Indie Developer's Monetization Playbook
How to take a Rork-built mobile app toward meaningful monthly AdMob revenue: category fit, getting the ATT-before-initialize order right, time-based interstitial pacing, judging by ARPDAU, app-ads.txt, and running a small portfolio.
📚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