Rorkで作る引き寄せ・運勢系アプリ — 累計5,000万DLから学んだ日替わり配信とリピート設計
「占い系アプリを作ったが、3日後にはほとんど起動されていない」——個人開発を始めたばかりの頃、私自身が直面した壁です。インストール数のグラフは初日に大きく跳ねるのですが、3日目を境にだいたい1/5から1/10に落ち込みます。広告収益のグラフも同じ形で落ちますから、月末になって「あの初動は何だったのだろう」と振り返ることになります。
このジャンル特有の構造があります。引き寄せ・運勢・占いアプリは「コンテンツが日々入れ替わる」という前提で設計しないと、ユーザーの中で役割を持たないまま忘れられていきます。今回は、2014年から個人開発でアプリを運用してきた経験と、累計5,000万DLを超えたアプリ事業の現場で繰り返し検証してきた「日替わり配信」の設計を、Rorkでどう組み立てるかという視点で整理します。
私は壁紙・癒し・引き寄せ系のアプリを長く運営してきました。AdMob 月収のピークは150万円を超える月もありましたが、その月の裏側には「日替わりで届くコンテンツの設計」を地道に直し続けた時期が必ずあります。一発でうまくいったケースは、振り返ってもひとつもありませんでした。
なぜ運勢系アプリはリピートで負けるのか — 構造的な3つの理由
まず、なぜこのジャンルが特にリピートで脱落しやすいのかを言語化しておきます。原因が見えていないと、対策も的を外します。
理由1: 「結果型コンテンツ」と「日課型コンテンツ」が混同される
占いも引き寄せも、ユーザー側の体験としては「結果を知って終わる」コンテンツに見えがちです。今日の運勢を見たら、その日はもう開く理由がありません。これを翌日も開いてもらう構造に変えるには、「結果」を「日課」に翻訳する設計が必要になります。
理由2: 通知のタイミングが「思想」ではなく「実装の都合」で決まる
朝の通知を朝9時に送るアプリは多いですが、9時という時刻が選ばれた理由を説明できる開発者は意外と少ないです。実際には「夜は反応が悪いから昼にしておこう」程度の理由で決まっていることが多く、結果としてジャンルとの相性が悪い時間に届きます。
理由3: コンテンツの「ストック」と「フロー」が設計されていない
毎日新しいコンテンツを作るのは個人開発者にとって持続不可能です。ストック(基礎データセット)から日替わりで切り出すフロー(提示順序)を分けて設計しないと、3週間でネタが尽きます。
この3つは、ジャンル特有の落とし穴です。技術力ではなく設計判断の問題で、ここを言語化しないままRorkで実装に入ると、必ず似たような形で詰まります。
日替わり配信の核心 — 「ユーザーごとに固有・かつ再現可能」を成立させる
日替わり配信の中心にあるのは「ユーザーAさんに今日表示される運勢は、Aさんにとって唯一であり、かつ同じ日中は何度開いても同じ結果を返す」という性質です。これを成立させるためには、サーバー側で毎朝バッチ処理を回す方法と、クライアント側でシード付き擬似乱数を使う方法の2通りがあります。
個人開発の規模では、後者を強く勧めます。理由は単純で、サーバー側で毎ユーザー分のレコードを毎日作るより、クライアント側で「ユーザーID + 日付」をシードとして擬似乱数を生成するほうがコストが明確に小さいからです。
ここでまず、シード付き擬似乱数のコア部分を実装します。Rorkはローカル状態管理にTypeScriptを使いますから、Mulberry32という軽量な決定論的擬似乱数を使います。
// src/lib/dailyFortune.ts
// 日付ベースで決定論的に運勢を返す擬似乱数
/**
* Mulberry32: 軽量な決定論的擬似乱数
* 同じシードからは同じシーケンスが返る性質を利用する
*/
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;
};
}
/**
* 文字列を32bit整数のハッシュに変換する (FNV-1a)
* userId と日付文字列をシードに変換するために使う
*/
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; // "2026-05-11"
rank: number; // 1-12 (1が大吉、12が凶)
rankLabel: string; // "大吉" など
themeId: string; // テーマ別の固有ID(恋愛・仕事・健康など)
message: string; // メッセージ本文
};
const RANK_LABELS = [
"大吉", "中吉", "吉", "小吉", "末吉", "半吉",
"平", "末小吉", "下吉", "小凶", "末凶", "凶",
] as const;
export function generateDailyFortune(
userId: string,
dateISO: string, // "2026-05-11"
messages: { themeId: string; rankMin: number; rankMax: number; body: string }[],
): FortuneResult {
const seed = hash32(`${userId}:${dateISO}`);
const rand = mulberry32(seed);
// 12段階の運勢ランク(重み付きで「大吉」「凶」を出にくくする)
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; // 3-6
else if (rankRoll < 0.85) rank = Math.floor(rand() * 4) + 6; // 6-9
else if (rankRoll < 0.97) rank = Math.floor(rand() * 2) + 9; // 9-10
else rank = 12;
// ランクに合うメッセージ群から1件選ぶ
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] ?? "吉",
themeId: picked.themeId,
message: picked.body,
};
}
このコードのポイントは、サーバー通信を一切必要としないことです。userIdは初回起動時にローカルで発行したUUIDで構いません。同じ日付の間は何度呼び出しても同じ結果を返しますし、日付が変われば自動的に別の結果が出ます。
実装直前にハマりやすい点を一つ挙げておくと、new Date().toISOString().slice(0, 10)を使ってISO日付を取り出すと、UTC基準になるため、ユーザーの体感日付とずれる場合があります。私のアプリではJSTオフセットを明示的に加える形に切り替えてから、深夜帯のユーザーから「日付が変わってないのに運勢が変わった」というレビューがなくなりました。
// src/lib/dateInUserTimezone.ts
// ユーザーのローカルタイムゾーンで「今日」を返す
export function todayInUserTimezone(now: Date = new Date()): string {
// Intl.DateTimeFormat を使ってローカルタイムゾーンでフォーマット
const fmt = new Intl.DateTimeFormat("en-CA", {
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
});
// "2026-05-11" 形式(en-CA は YYYY-MM-DD 区切り)
return fmt.format(now);
}
Intl.DateTimeFormatは端末のタイムゾーンを自動的に取得しますから、海外ユーザー対応も同じコードで済みます。私の引き寄せ系アプリ(『引き寄せの法則アプリ』)は台湾・タイ・ベトナムにユーザーが広がっていますが、この実装にしてから時差クレームが消えました。
通知タイミングは「思想」で決める — BJ Fogg の行動デザインを踏まえる
朝に届けるか、夜に届けるか、寝る前か、起きた直後か——通知のタイミングは、リテンションに直接効きます。私が個人開発で行き着いた結論は、引き寄せ・運勢系では「朝7:00〜7:30」と「夜21:30〜22:00」の二択で、用途で使い分けるというものです。
朝の通知は「今日を始める儀式」として機能します。BJ Foggの行動デザインモデルでは、新しい習慣は「既存の習慣の直後に配置する」と定着しやすいとされています。朝の通知は「歯を磨く・コーヒーを淹れる」という既存の習慣の流れに乗せるためのトリガーです。
夜の通知は「明日への準備」として機能します。引き寄せ系の場合、「明日のテーマ」や「就寝前のアファメーション」を届けると、コンテンツの性質と利用シーンが噛み合います。
ここで実装の都合と相談しないといけないのは、ユーザーごとに通知時刻をパーソナライズするかどうかです。私の経験では、初期リリース時は固定時刻で配信し、最低限のデータが溜まってから個別最適化を入れるほうが、開発コストとリターンが見合います。
Rorkで通知を実装するときは、expo-notificationsをベースに、ローカルスケジューリングと、サーバープッシュの二段構えにします。
// src/lib/scheduleDailyNotification.ts
// expo-notifications を使った日次のローカル通知スケジュール
import * as Notifications from "expo-notifications";
type DailyNotificationConfig = {
hour: number; // 0-23
minute: number; // 0-59
title: string;
body: string;
identifier: string;
};
/**
* 既存のスケジュールを破棄して、新しい日次通知を登録する
* 「毎日 hour:minute」に通知が届く
*/
export async function scheduleDailyNotification(
config: DailyNotificationConfig,
): Promise<void> {
// 権限確認
const { status } = await Notifications.getPermissionsAsync();
if (status !== "granted") {
const req = await Notifications.requestPermissionsAsync();
if (req.status !== "granted") {
console.warn("通知権限が得られませんでした");
return;
}
}
// 既存の同一識別子スケジュールを破棄
await Notifications.cancelScheduledNotificationAsync(config.identifier);
// 新しい日次トリガーを登録
await Notifications.scheduleNotificationAsync({
identifier: config.identifier,
content: {
title: config.title,
body: config.body,
sound: "default",
},
trigger: {
hour: config.hour,
minute: config.minute,
repeats: true,
},
});
}
// 利用例: 朝7:15に「今日の運勢が届きました」通知
// await scheduleDailyNotification({
// hour: 7,
// minute: 15,
// title: "今日の運勢が届きました",
// body: "今日のあなたへのメッセージを開いてみてください",
// identifier: "morning-fortune",
// });
このコードを書く前と書いた後で、私のアプリの7日継続率は約1.4倍になりました。Before(通知なし版)は7日継続率が約8%、After(固定時刻通知版)は約11%、さらに通知タイトルを「今日のメッセージが届きました」から「今日のあなたへの一言が届きました」に変えたあとは約13%でした。文言の差は侮れません。
注意点として、Notifications.scheduleNotificationAsyncのtriggerの型はexpo-notificationsのバージョンによって微妙に違います。私が確認した範囲では、SDK 51以降では{ hour, minute, repeats }の形式で安定して動作しますが、それ以前のバージョンではtype: "daily"を明示的に指定する必要があるケースがありました。Rorkで生成されたコードがそのまま動かないときは、まずSDKバージョンを確認してください。
ロック画面ウィジェットで「開かなくても触れる」体験を作る
iOS 16以降のロック画面ウィジェット、Android 12以降のホーム画面ウィジェットは、引き寄せ・運勢系アプリと相性が良いUIです。アプリを開かなくても、ロック画面に「今日のメッセージ」が見える状態を作ると、ユーザーは1日に何度もそのメッセージに触れることになります。
ここで重要なのは、ウィジェットがアプリの起動代わりにならないように、ウィジェットの情報量を意図的に絞ることです。全文をウィジェットに出してしまうと、アプリを開く動機が消えます。私が実験した範囲では、「ランクとアイコンだけウィジェット」「テーマ名だけウィジェット」「冒頭1行だけウィジェット」の3パターンを比較すると、冒頭1行パターンが最もアプリ起動率を上げました。
// ios/FortuneWidget/FortuneWidget.swift
// WidgetKit を使ったロック画面ウィジェット最小実装
import WidgetKit
import SwiftUI
struct FortuneEntry: TimelineEntry {
let date: Date
let rankLabel: String // "大吉" など
let firstLine: String // "今日は新しい縁が動き出します..."
}
struct FortuneProvider: TimelineProvider {
func placeholder(in context: Context) -> FortuneEntry {
FortuneEntry(date: Date(), rankLabel: "吉", firstLine: "今日のあなたへのメッセージ")
}
func getSnapshot(in context: Context, completion: @escaping (FortuneEntry) -> Void) {
completion(placeholder(in: context))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<FortuneEntry>) -> Void) {
// App Group経由でホストアプリから今日の運勢を取得
let defaults = UserDefaults(suiteName: "group.your.bundle.id")
let rankLabel = defaults?.string(forKey: "todayRankLabel") ?? "吉"
let firstLine = defaults?.string(forKey: "todayFirstLine") ?? "アプリを開いてメッセージを受け取ろう"
let entry = FortuneEntry(date: Date(), rankLabel: rankLabel, firstLine: firstLine)
// 明日の0:30に更新
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date())!
let nextUpdate = Calendar.current.date(bySettingHour: 0, minute: 30, second: 0, of: tomorrow)!
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
struct FortuneWidgetEntryView: View {
var entry: FortuneProvider.Entry
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(entry.rankLabel)
.font(.system(size: 18, weight: .bold))
.foregroundColor(.primary)
Text(entry.firstLine)
.font(.system(size: 12))
.lineLimit(2)
.foregroundColor(.secondary)
}
.padding(8)
}
}
@main
struct FortuneWidget: Widget {
let kind: String = "FortuneWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: FortuneProvider()) { entry in
FortuneWidgetEntryView(entry: entry)
}
.configurationDisplayName("今日の運勢")
.description("ロック画面で今日の運勢を確認できます")
.supportedFamilies([.systemSmall, .accessoryRectangular])
}
}
App Groupの設定でホストアプリとデータを共有する点と、Timeline(entries:policy:)で明日0:30まで更新を保留する点が要諦です。WidgetKitはこちらが指定したタイミングよりも早く更新を要求してくることもありますが、policy: .after(nextUpdate)を渡しておくと「無駄な再描画を避けてください」というヒントとして機能します。
Rork WidgetKit ロック画面ウィジェット実装ガイドでウィジェットの基礎を整理していますので、初めて触る場合は先にそちらを参照してください。
マネタイズ設計 — 無料配信 × プレミアム解釈の二層構造
引き寄せ・運勢系アプリの収益化は、「日替わり配信を無料で続ける×深い解釈や個別鑑定を有料化する」という二層構造が、私の経験では最もバランスが取れます。
無料層を厚くすることに抵抗があるかもしれませんが、このジャンルではむしろ無料で配信を続けることが「信頼の蓄積」になります。アンインストールされたら課金してもらう機会自体が失われるので、まず日課になってもらうところに全力を投じます。
プレミアム層は、「年間運勢」「相性占い」「テーマ別の深堀り解釈」のように、無料層では物理的に提供できない深さや個別性を打ち出します。
価格設計の例は、私の実運用ベースで以下のような形に落ち着きました。
- 月額サブスクリプション 480円(税込) — 全プレミアム機能
- 年間鑑定(買い切り) 1,800円(税込) — 365日分の年間運勢書
- 相性占い 単発 250円(税込) — 名前と生年月日で二人の相性を表示
サブスクと買い切りを混在させるのが、このジャンルでは結構効きます。スピリチュアル系のユーザーには「毎月課金は怖い」と感じる層が一定数おり、買い切りオプションを置くと心理的なハードルが下がります。
私のアプリで実測した範囲では、「サブスクのみ」期間と「サブスク+買い切り併存」期間を比較すると、ARPPU(課金ユーザー当たり収益)は併存期間が約1.6倍になっていました。同じ機能でも、提示の仕方ひとつで収益が変わる典型例だと思います。
Rork サブスクリプション×買い切りハイブリッド収益モデルで混在モデルの設計を詳述しています。
スピリチュアル文脈と技術実装をどう接続するか — 個人開発者の倫理
このジャンルで技術を使う以上、避けて通れない論点があります。「占いの結果」をアルゴリズム的に生成しているのに、ユーザーには「あなただけのメッセージ」として届けることの倫理的な扱いです。
私自身は、宮大工だった両祖父の影響もあり、「手を動かして作るものは、その手が信じているものを反映する」という感覚を持っています。アート活動でも、作品の主題として「日本特有の祈りを背景に、集団心理と認知世界の構造、根源意識を探ること」を選び続けてきました。技術で占いを実装するというのは、その延長線上にあると考えています。
具体的な実装方針として、私が意識していることは3つあります。
ひとつは、「占いの結果」を「絶対的な予言」として書かないこと。メッセージの語尾は「〜かもしれません」「〜のヒントになります」「〜を意識してみてください」のような提案形にして、ユーザーの主体性を残します。これはコピーライティングの問題に見えますが、本質的にはアプリの設計思想の問題です。
ふたつめは、ネガティブな結果をフックにして課金を煽らないこと。「凶」を引いたユーザーに「詳細解釈は有料です」と出すような設計は、短期的には課金率が上がるかもしれませんが、長期的にはレビュー評価とブランドを壊します。私のアプリでは、ネガティブ寄りの結果が出たときほど、無料で受け取れる励ましの文章を厚くしています。
みっつめは、「鑑定の根拠」をブラックボックスにしないこと。「あなたの生年月日と今日の日付からこのメッセージを選んでいます」という旨を、アプリ内のどこかにそっと書いておきます。完全に透明化する必要はありませんが、「魔法のように見せている」状態は健全ではないと考えています。
このあたりは答えがひとつではない領域です。私自身まだ模索中ですが、判断軸を持って実装に向き合うこと自体が、このジャンルで長く続けるための条件だと感じています。
海外ユーザーをひとつのコードベースで支える — 多言語・タイムゾーン・通貨
引き寄せ・運勢アプリの面白いところは、ある程度の国際展開を最小コストで狙えることです。占いの文化は地域差が大きいので、すべての国に深く適応するわけにはいきませんが、英語・繁体中国語・タイ語あたりまでなら、設計次第で一つのコードベースで成立します。
私の経験では、海外展開を最初に判断する基準は「メッセージ群を翻訳できるか」ではなく、「タイムゾーンと曜日感覚をズラさないか」でした。日本の19時と台湾の19時はほぼ同じ感覚で機能しますが、ベトナム時間で深夜に通知を受け取るユーザーは離脱します。先ほどのtodayInUserTimezoneで日付ベースを揃えるのと同じ要領で、通知時刻もユーザーのローカル時刻基準で設定するように設計します。
通貨表示は意外と落とし穴になりやすい領域です。Rorkで生成されたコードは円表示固定になっていることがあり、海外ユーザーに対して「¥480/month」と表示されてしまうと、心理的にちらつきが生じます。Intl.NumberFormatを使った通貨フォーマットへの差し替えは早めに入れておくと、課金率に直接効きます。
// src/lib/formatPrice.ts
// ユーザーのロケールに合わせて価格を表示する
export function formatLocalizedPrice(
amountInJpy: number,
locale: string = Intl.DateTimeFormat().resolvedOptions().locale,
): string {
// ストア取引はJPY建てだが、表示だけはローカル通貨換算した目安に置き換える
// 公平な為替レートはストアが返すものを使う想定。ここではフォールバック表示
const fmt = new Intl.NumberFormat(locale, {
style: "currency",
currency: "JPY",
});
return fmt.format(amountInJpy);
}
App Store Connect / Google Play Console の価格表設定(自動換算)と、アプリ内の表示を一致させるのが基本的な指針です。「表示は目安、ストア決済時に正確な金額が出る」という運用にしておくと、為替変動時のクレームも減ります。
ストアレビューを「コンテンツの一部」として扱う
引き寄せ系アプリで意外なほど効くのが、ストアレビューへの返信を「コンテンツ運用の一部」として捉えることです。レビューはアプリ品質のフィードバックであると同時に、未来のユーザーが見る公開コンテンツでもあります。
私のアプリでは、星3つ以下のレビューには48時間以内に返信することをルールにしていました。返信の内容は、不満点を認め、改善する箇所と次のバージョンで対応する箇所を分けて書きます。「ご指摘ありがとうございます。通知時刻のカスタマイズは次回バージョンでの対応を予定しています」のように、具体的な対応予定を書くと、後でその通りに実装したときに更新通知の形で返信できます。
このコミュニケーションの積み重ねは、新規ユーザーがレビューを見たときの「このアプリは育っている」という印象に繋がります。引き寄せ・運勢系のジャンルは、機械的な印象と相性が悪いので、「人が運用している」という空気を意図的に作るのが効きます。
個人開発で踏みやすい3つの落とし穴
最後に、私自身が踏み抜いてきた落とし穴を3つ共有します。
落とし穴1: 「日替わり」のはずなのに、コンテンツが循環していると気づかれる
メッセージ群を100件しか用意せず、Math.floor(rand() * 100)で選ぶと、ユーザーは3〜4週間で「これ前にも見た」と気づきます。ジャンル特性として「同じものに2度遭遇すること」がユーザー満足度を大きく下げます。対策はシンプルで、メッセージプールを最低でも300〜500件用意し、過去30日分の表示履歴をローカルに保存して重複を避ける処理を入れます。
// src/lib/avoidRecentRepeats.ts
// 過去30日分のテーマ重複を避けてピックする
import AsyncStorage from "@react-native-async-storage/async-storage";
const HISTORY_KEY = "fortune-history-v1";
const HISTORY_DAYS = 30;
type HistoryEntry = { date: string; themeId: string };
export async function pickNonRepeating(
candidates: { themeId: string; body: string }[],
rand: () => number,
today: string,
): Promise<{ themeId: string; body: string }> {
const raw = await AsyncStorage.getItem(HISTORY_KEY);
const history: HistoryEntry[] = raw ? JSON.parse(raw) : [];
// 30日より古いエントリを切り捨て
const cutoff = new Date(today);
cutoff.setDate(cutoff.getDate() - HISTORY_DAYS);
const recent = history.filter((h) => new Date(h.date) >= cutoff);
const recentThemes = new Set(recent.map((h) => h.themeId));
// 直近30日に登場していない候補のみに絞る
const fresh = candidates.filter((c) => !recentThemes.has(c.themeId));
const pool = fresh.length > 0 ? fresh : candidates;
const picked = pool[Math.floor(rand() * pool.length)];
// 履歴に追記して保存
recent.push({ date: today, themeId: picked.themeId });
await AsyncStorage.setItem(HISTORY_KEY, JSON.stringify(recent));
return picked;
}
落とし穴2: 通知をオフにされた後の動線がない
通知権限を最初に拒否されたユーザーは、設定アプリを開いて手動でオンにしない限り、戻ってきません。この層に対しては、アプリ内で「設定の開き方をスクリーンショット付きで案内する画面」を用意するのが現実的です。ExpoではLinking.openSettings()で設定アプリを直接開けますが、その前に「なぜ通知が必要か」を1画面挟むかどうかでオン化率が変わります。
落とし穴3: 朝の通知が「課金導線」になっている
ストアレビューで頻繁に見る不満が、「通知を開いたら課金画面が出てきた」というものです。日替わりコンテンツを謳っているのに、無料の今日の運勢ではなくサブスクの案内が先に出るアプリは、信頼を失います。通知のディープリンクは、必ず「今日の無料コンテンツ」にまっすぐ着地させるべきです。プレミアム導線はその後、ユーザーが満足してから提示します。
レビュー文言と「言葉の温度」のチューニング
技術的な実装の話から少し離れますが、運勢系アプリのリテンションを上げる上で、メッセージ本文の文体は侮れません。同じ内容でも、語尾・主語・温度感が違うだけで、ユーザーが日課にするかどうかが変わります。
私が試して効いたパターンを共有しておきます。
ひとつめは、「あなた」を明示しすぎないこと。「今日のあなたは」「あなたには」を多用すると、機械的な印象が強くなります。「今日の流れは」「今日見えてくるテーマは」のような書き出しに置き換えると、押し付けがましさが減ります。
ふたつめは、命令形を避けること。「〜しなさい」「〜してください」は、占いの結果としては強すぎます。「〜してみるのも良いかもしれません」「〜に意識を向けてみてください」のような提案形にすることで、ユーザーの主体性が残ります。
みっつめは、「無理な前向きさ」を避けること。引き寄せ系のメッセージで「素晴らしい一日になります」「最高の出会いが待っています」のような断定は、達成されなかったときに信頼を失います。私のアプリでは、「〜を感じるかもしれません」「〜のヒントを受け取る一日です」のように、ユーザー自身の体験に寄り添う表現に統一しています。
文体ガイドラインを言語化してチームで共有しておくと、メッセージプールを増やす作業を外部に頼むときも、品質が保たれます。私は500件のメッセージ群を整備するときに、最初の50件で「文体サンプル」を作り、残り450件はその文体に合わせる形で執筆を進めました。
Before / After — 自分のアプリで何が起きたか
最後に、ある引き寄せ系アプリで実際に何が起きたかをBefore/Afterで残しておきます。Rorkに移行する前後の話ですが、設計の本質は同じです。
Before(初期実装、2024年頃)
- メッセージプール 100件、純粋ランダム
- 通知なし
- ウィジェットなし
- 全結果が表示された直後にサブスク案内が出る
- 7日継続率 約8%、30日継続率 約2%、ARPPU 約180円/月
After(設計を直したあと、2025年中盤)
- メッセージプール 480件、シード付き擬似乱数 + 30日重複回避
- 朝7:15固定の日次通知
- ロック画面ウィジェット(冒頭1行表示)
- 無料コンテンツ閲覧後、1日2回までCTA表示、それ以上は出さない
- 7日継続率 約19%、30日継続率 約6%、ARPPU 約430円/月
特に効いたのは「メッセージプールの拡張」と「通知の文言調整」でした。技術的に派手な変更ではありませんが、ジャンルとの噛み合わせを意識した小さな調整が、累積でこの差を生んでいます。
Rorkオンボーディング初日離脱を防ぐ設計も合わせて読むと、初日〜30日の連続したリテンション設計が組み立てやすくなります。
トリガー・行動・報酬・投資という4段階の枠組みは、引き寄せ系アプリにも違和感なく適用できる視点を提供してくれます。
全体を振り返ってに代えて — 明日試せる1つのこと
ここまで設計の話を続けてきましたが、明日まず試すなら、自分のアプリのメッセージプール件数を確認してみてください。100件未満なら、まず300件まで増やすところから始めると、リピート率に明確な変化が出ます。テクニックよりも前に、コンテンツの厚みがこのジャンルの土台になります。
私自身、累計5,000万DLを超えてきたとはいえ、新しいアプリを出すたびに同じ設計の問題と向き合っています。完成した方法論はなく、毎回小さな仮説検証の積み重ねです。共に学んでいけたら嬉しいです。お読みいただきありがとうございました。