これは、彼女の誕生日プレゼントとして作った、二人専用SNS「Sync」の開発記録です。
完成した機能だけでなく、本番環境でログインできなくなった失敗や、写真表示を約96%軽量化した改善までまとめます。
はじめに
彼女の誕生日に、何か形に残るものを贈りたかった。
写真をまとめたアルバムもいい。でも、これから先の思い出も増やしていけるものにしたい。そこで作り始めたのが、二人だけが使えるSNS型Webアプリ「Sync」です。
過去の思い出を投稿する Memory と、これから行きたい場所ややりたいことを残す Wish。昨日の「楽しかったね」と、明日の「次はここに行こう」を同期する、という意味を込めて名付けました。
開発を始めたのは誕生日のおよそ2週間前。URLは当日に渡し、それまでの思い出は先に自分で投稿しておく。そんな、締切が絶対に動かせない個人開発でした(思い出アプリ開発始動!)。
作ったもの
Syncは、一般公開を前提としないクローズドなSNSです。アカウントは二人分だけを事前に用意し、ログインしたユーザーだけが投稿や写真を閲覧できます。
主な機能は次のとおりです。
- Memory: 写真と文章、実際の出来事の日付をセットで投稿するタイムライン
- Wish: 行きたい場所や、これからやりたいことを気軽に残す投稿欄
- Saved: 気に入ったMemoryやWishを保存して後から見返す機能
- My Page: 自分の投稿数、最近のアクティビティ、投稿一覧を確認する画面
- Birthday UI: 特定ユーザーのログイン時に表示する誕生日モーダル
Memoryでは、投稿を作成した日時とは別に「その出来事があった日」を持たせました。昔の写真を後から登録しても、思い出そのものの時系列で並べられるようにするためです。小さな仕様ですが、このアプリでは技術的な都合よりも体験を優先した重要な設計でした。
画面はスマートフォンで片手操作できることを前提に、下部ナビゲーションを置いたTwitterやThreadsに近い構成にしています。色は彼女に選んでもらった7色の緑をUIへ反映しました。ユーザー自身の好みが、そのままデザインシステムになっています(UIとHono導入、Happy Birthdayモーダル)。
技術構成
フロントエンドからデータベース、画像配信まで、Cloudflare上へまとめました。
- Frontend: Next.js App Router / React / TypeScript / Tailwind CSS
- API: Hono
- Database: Cloudflare D1
- Image Storage: Cloudflare R2
- Image Optimization: Cloudflare Images binding
- Deploy: OpenNext for Cloudflare / Cloudflare Workers
- Schema: Drizzle ORM
当初はフロントエンドとAPIを分ける構想もありましたが、最終的にはNext.jsのCatch-all Route HandlerにHonoを載せ、OpenNextで一つのWorkerとしてデプロイしました。小規模なアプリでは、認証Cookie、API、画面を同一オリジンにまとめられることが大きな利点でした。
D1には users、memories、wishes、saved_memories、saved_wishes などのテーブルを置き、R2にはMemoryとWishの画像を保存しています。画像URLそのものを公開するのではなく、認証付きAPIを経由して返す設計です。
誕生日という締切が、MVPを決めてくれた
個人開発では、作りたいものが際限なく増えがちです。しかし今回は誕生日が締切なので、最初にMVPを明確にしました。
- ログインできる
- 思い出を投稿・編集・削除できる
- Wishを投稿できる
- 投稿を保存できる
- スマートフォンで問題なく使える
- 誕生日らしい体験がある
アイコン設定や高度なリンクプレビューなどは後回しにしました。一方で、実際の日付を選べることや、改行した文章をそのまま読めることは早い段階で実装しています。機能の派手さではなく、「二人で使うときに気持ちよいか」を優先した結果です。
Wish投稿と保存が動いた時点でMVPが成立し、My Pageの「すべて見る」とスマートフォン実機確認を追加しました(wish投稿と投稿の保存、mypageの「すべて見る」を表示)。
認証をモックからD1へ移す
最初のログイン実装ではテスト用ユーザーをコード内で参照していました。その後、ユーザー情報をD1へ移したものの、API側が古いテストデータを見続けていたためログインに失敗しました。
原因は単純でしたが、「画面が完成しているように見えても、データの参照先が本番設計へ切り替わっているとは限らない」という典型的な移行ミスでした。D1からユーザーを検索し、パスワードを照合し、HttpOnly Cookieでログイン状態を維持する形へ変更しました(認証機能)。
🔐この認証は、二人だけで使うMVPに合わせた簡易実装です。一般ユーザー向けに展開するなら、強いパスワードハッシュ、署名付きまたは不透明なセッション、失効管理、CSRF対策、レート制限などを追加する必要があります。記事やリポジトリには実際の認証情報を載せないことも重要です。
一番苦戦したのは、本番だけログインできない問題
R2を導入して画像保存まで動いた直後、CloudflareへデプロイするとログインAPIが500を返すようになりました。ローカルでは動くのに、本番では毎回失敗します。
Workerのログに出ていたのは、次のような情報量の少ないエラーでした。
TypeError: Cannot read properties of undefined (reading 'default')
最初に疑ったのはD1のデータ不足、bindingの設定、JSON bodyのパース、Cookie処理、Honoとの相性でした。しかし、D1やR2へのアクセスを外しても500は消えません。
そこでRoute Handlerを、固定のJSONを返すだけの最小実装まで削りました。それでも失敗する。つまり、問題は認証ロジックの中ではなく、モジュールが実行される前の層にあると切り分けられました。
最後に、Route HandlerのRuntimeを EdgeからNode.jsへ変更。これで正常にレスポンスが返り、元のAPIを戻しても本番ログインが成功しました。
このデバッグで一番効いたのは、知識ではなく順番でした。
- ログで再現条件を固定する
- 外部依存を一つずつ外す
- 最小のRoute Handlerでも壊れるか確認する
- アプリケーションコードより外側のRuntimeを疑う
「全部怪しい」状態から、層を一枚ずつ剥がしていく。今後も使える切り分け方を学べた出来事でした(R2導入、API Runtime Troubleshooting Report)。
写真SNSなのに、写真が遅い
完成後に実際の写真を増やすと、別の問題が見えてきました。表示領域は最大でも約600pxなのに、スマートフォンで撮った数MBの原画像をそのまま取得していたのです。
改善前のホーム画面では、初期表示だけで少なくとも24件の画像リクエストが発生していました。比較した3枚の原画像は合計約5.8MB。二人だけのアプリでも、写真を見るたびにこれだけ通信するのは重すぎます。
そこで、認証付きの画像配信APIへ幅と品質を渡し、Cloudflare Images bindingでリサイズしてWebPへ変換するようにしました。フロントエンドではNext.js Imageのカスタムローダーとsizesを使い、ブラウザが画面幅とDPRに合う候補だけを選択します。
配信の流れは次のようになります。
- ブラウザが必要な画像幅を選ぶ
- 同一オリジンの画像APIへ幅と品質を指定する
- APIがCookieを検証する
- R2から原画像を取得する
- Cloudflare Imagesで縮小し、WebPで返す
- 変換に失敗した場合は原画像へフォールバックする
重要なのは、最適化のために画像を公開しなかったことです。変換処理より先に認証を行うため、画像URLを知っているだけでは取得できません。
結果は、初期画像リクエストが 24件以上から7件、比較対象の転送量が 約5.8MBから245KB になりました。約96%の削減です。さらに新規アップロード時にも長辺を1280pxへ抑え、WebPへ圧縮するようにしました(画像配信パフォーマンス改善)。
作ってみて分かったこと
1. 個人開発の強さは、ユーザーが近いこと
テーマカラーを直接聞ける。使うスマートフォンが分かる。どんな思い出を残したいかも分かる。大規模サービスのような調査はなくても、目の前の一人に深く合わせられます。
2. 締切はスコープを削る味方になる
誕生日は延期できません。だからこそ、必要な機能と「いつか欲しい機能」を分けられました。MVPは小さくても、相手に届けば価値になります。
3. 本番環境は、ローカルとは別のプロダクト
D1やR2のbinding、OpenNextの変換、Worker Runtime、CookieのSecure属性。Cloudflare上で動かして初めて分かる問題がありました。デプロイを最後の儀式にせず、開発初期から繰り返すべきでした。
4. パフォーマンスは実データで測る
サンプル画像が少ない間は軽く見えても、スマートフォンの原画像を並べると状況は変わります。「なんとなく速くなった」ではなく、リクエスト数、転送量、画像寸法を比較したことで、改善の効果を説明できました。
おわりに
Syncは、世界中の人に使ってもらうためのSNSではありません。ユーザーは二人だけです。
でも、誰のために作るのかがはっきりしていたから、日付の持ち方、画面の色、認証、画像の扱いまで、一つひとつの技術判断に理由が生まれました。
「彼女の誕生日に何か作りたい」という個人的な動機から始まり、Next.js、Hono、D1、R2、Cloudflare Workersを横断する実践的な開発になりました。何より、完成した日が終わりではありません。これから思い出やWishが増えるたびに、このアプリも二人の時間と一緒に育っていきます。
昨日の思い出と、明日の約束を同期する。
それが、二人だけのSNS「Sync」です。



