何を解決するものか
資格の勉強や筋トレ、早起きのような「毎日の積み上げ」は、ひとりだと続きません。かといって、ふつうの SNS に投稿すると、いいねの数やフォロワーと比べてしまい、かえって疲れます。
KOTSULY(コツリー)は、比べずに、ゆるく見守り合えることを目指した自己研鑽SNSです。
- 記録:取り組み(簿記2級・毎朝ラン など)を選んでひとこと書くと、Vlog のワンシーンのようなカードになります。写真がなくても、取り組みや時間帯に合わせた絵で仕上がります
- 作業スペース:カフェ・ワークラウンジ・図書館のフロアから席を選んで作業します。話さなくても、近くでがんばる人の気配が伝わります
- 作業ルーム(Study with me):誰かが開いたタイマーに途中から参加して、みんなで同じポモドーロを回します
- モーニング・ナイトルーティン:朝と夜にやることを並べて、タイマーに合わせてひとつずつ進めます。終わるまでタイムラインは見せません
- ふりかえり:毎週日曜の夜に、その週の積み上げを先週の自分と比べて届けます
いまはベータテストの準備中です。iOS / Android のアプリを先に出し、Web 版も同じコードから準備しています。
担当範囲
企画・要件定義・UI設計・実装(アプリ・Web 版・公式サイト)・DB設計・インフラ・運用のすべて。利用規約とプライバシーポリシーの下書きも自分で作っています。
開発は Claude Code を中心に進めていて、セッションを「モバイルアプリ開発部」「デザイン部」「経営企画室」のような部署に分けて回しています。わたしの役目は、決めることと、実機で確かめることです。くわしくは「Claude Code に『部署』を作って、ひとりで個人開発を回している話」に書きました。
技術的に難しかった点と、どう解いたか
1. 「比べない」を、画面ではなく DB で守る
このサービスの方針は「数で比べさせない」ことです。応援の数は記録した本人にしか見せず、ランキングも置きません。
ただ、画面で隠すだけだと、API を直接たたけば他人の応援の数が取れてしまいます。そこで Supabase の RLS(行ごとのアクセス制御)で、応援を読めるのは「記録の本人」と「応援した本人」だけにしました。アプリに不具合があっても、DB が返さないので漏れません。
2. 公開範囲とブロックを、すべてのテーブルで一貫させる
記録には「全体 / フォロワー / 自分だけ」の公開範囲があり、さらにブロックが絡みます。これを記録・コメント・応援それぞれの決まりに書き散らすと、どこか一箇所で必ず抜けます。
判定は can_see_post() という関数1つにまとめ、コメントも応援も「元の記録が見えるか」をこの関数に聞く形にしました。宣伝目的の新しいアカウント対策(リンクを貼れるのは記録が7日以上ある人だけ)のような決まりも、アプリと同じものを DB に置いています。
3. 「誰がどの席にいるか」を、DB に書かずに配る
作業スペースは、いつも誰かがいる前提の機能です。素直に作ると、座る・席を移る・休憩するたびに書き込みが走ります。
在席は Supabase Realtime の Presence、スタンプとひとことは Broadcast にして、どちらも DB には書きません。Presence はつなぎが切れると自動で消えるので、退室の処理も、残った行の片付けもいりません。
- 「座りました」「休憩に入りました」は送らず、Presence の前と今の差から各端末で作る
- 混んだら2号店・3号店が自動で開く。号店ごとにチャンネルは増やさず、在席の中身の番号で分ける
- みんなで囲む席のポモドーロは、時刻を配らず、各端末が「毎時0分・30分」から計算する
4. 作業中のタイマーを、スマホと Web のあいだで同期する
iPhone で始めた作業を、パソコンの Web 版で続きから見て、そこで終えて記録できるようにしています(Web 版は準備中です)。
- 残り時間は送らない:送るのは始めた時刻だけで、経過やポモドーロの残りは各端末が計算する
- 勝ち負けは DB が決める:「作業中は1人1つまで」のユニーク索引と、「まだ終わっていない行だけ更新する」条件で、2台で同時に始めた・終えたときも二重の記録にならない
- 放置された作業は、端末とサーバーの両方で終える:3時間ごとに「まだ作業中ですか?」と聞き、どの端末も開いていないときは pg_cron が終える
5. アプリと Web 版を、同じコードから作る
最初は Vite + React の Web の試作品で、「数タップで記録できるか」「応援が届くと続けたくなるか」を確かめました。そのあと、型・データ・計算・状態管理を同じ関数名のまま Expo に移し、画面だけを React Native で書き直しています。
Web 版も、このモバイルアプリのコードから書き出す形にしていて、iOS / Android / Web を1つのコードで回せるようにしています。サーバーにつないでいないときは端末の中だけで動く「デモモード」になるので、サーバーがなくても全画面を触れます。
6. 重いファイルは R2、リアルタイムは Supabase
個人で運用する以上、1,000人規模でも月数千円に収めることを前提にしました。
- 記録の写真は Supabase Storage の非公開の置き場所に置き、期限つきの URL で見せる。アップロードの前に端末で縮め、位置情報などを消す
- 作業BGM(自作の曲)は Cloudflare R2 の非公開の置き場所に置き、Edge Function が期限つきの URL を出す
- 認証メールは自分のドメインから Resend で送る
学んだこと / 次にやること
SNS は「何を作るか」以上に「何をさせないか」の設計が品質を決める、というのがいちばんの学びでした。比べさせない・さらさせない・荒らさせないといった決まりは、画面ではなく DB に置いて初めて守れます。
リアルタイムの機能も同じで、在席は保存しない、出来事は送らない、時刻は配らない、と送らないものを決めるほど、壊れる場所も費用も減っていきました。
次は、身近な人に使ってもらうベータテストを始め、そのあと App Store と Google Play で公開する予定です。開発のようすは X(@kaede3eng) でときどき書いています。