utily.net は、バックエンドサーバーもデータベースも持ちません。6本のツール(履歴書作成、クリップボード管理、占い、時間計算、マニュアル管理、可変速度時計)すべてがブラウザ内で完結し、ユーザーのデータはサーバーには一切送られません。
この設計は最初から意図したものです。リードエンジニアとして業務システムを扱う日々の中で、「個人情報をどこかのサーバーに置くことへの不快感」を強く感じていました。自分が使いたいツールを作るなら、その不快感を解消する設計にしたかったのです。
この記事では、サーバーレス設計を選んだ理由、実際にどの技術でどの問題を解いたか、そしてこの設計の限界と向き合い方について書きます。
なぜサーバーを持たないことにしたか
理由1:個人情報をサーバーに送りたくなかった
Resume(履歴書作成ツール)を作るとき、最初に考えたのはこれです。履歴書には名前・住所・職歴・学歴など、極めて個人的な情報が含まれます。既存のオンライン履歴書サービスを使えばいいと思っていましたが、「データがどこに保存されるのか」が不透明なサービスには入力する気になれませんでした。
「ブラウザの中だけで処理して、PDF出力までできれば外部サービスは不要では」という発想から開発を始めました。結果として @react-pdf/renderer を使い、サーバー通信ゼロでPDFを生成できるようになりました。
理由2:個人開発のランニングコストを最小化したかった
バックエンドサーバーを持つとなると、VPSやクラウドの費用、データベースの管理コスト、セキュリティアップデートの継続作業が発生します。個人開発者が複数サービスを長期運営するには、これが大きな障壁になります。
現状の構成は Docker + Nginx のリバースプロキシで静的ファイルを配信するだけなので、VPS1台で6ツール全部が動きます。ランニングコストは月数百円で済んでいます。
理由3:スケールを考えなくてよい
サーバーサイド処理がなければ、アクセス増加に対してスケールアウトを考える必要がありません。1000人同時アクセスが来ても、静的ファイルを返すだけなので問題が起きません。個人開発では「突然バズった」ときに詰まるケースが多いですが、この構成ではそのリスクがほぼありません。
ツールごとのストレージ設計
データをサーバーに送らない場合、データはどこに置くのでしょうか。utily.net の6ツールは用途に応じて3種類のストレージを使い分けています。
| ツール | ストレージ | 選んだ理由 |
|---|---|---|
| Resume | localStorage | 入力データは文字列のみ。複数タブ同時編集は想定しない |
| Palette | URLエンコード | チームへの共有がコアユースケース。URLだけで状態を伝えたい |
| Fortune | localStorage | 「今日の占い結果」を保持するだけ。シンプルで十分 |
| FlowTick | localStorage | 作業記録は文字列・数値のみ。カレンダービューも JSON で完結 |
| GuideKnot | IndexedDB | 画像を含む手順書を扱う。5MBを超える可能性があるため |
| Modtimer | localStorage | 設定値(速度・音)を保持するだけ。最軽量で十分 |
Palette だけ URLエンコードを選んだ背景
Palette の核心的な価値は「チームでのクリップボード共有」です。localStorage に保存しても、自分のブラウザ内でしか使えません。「このクリップボードセットをSlackで送りたい」というユースケースには対応できません。
URLの状態にデータを埋め込めば、URLを渡すだけで相手の画面に同じ状態を再現できます。データ圧縮(LZ-String)を使って、そこそこの量のテキストを2KB程度のURLに収めることができました。URLの長さ制限(約2000文字)には注意が必要ですが、クリップボード管理ツールとして実用的な量には対応できています。
GuideKnot だけ IndexedDB を選んだ背景
GuideKnot はステップ式の手順書を管理するツールです。手順の各ステップに画像(スクリーンショットなど)を添付できます。ここが localStorage との分岐点になりました。
localStorage は文字列のみ扱えます。画像を Base64 に変換して localStorage に保存することは技術的には可能ですが、5〜10MB という容量制限がすぐに問題になります。スクリーンショット数枚で上限に達してしまいます。
IndexedDB はブラウザが管理するデータベースで、バイナリデータをそのまま格納できます。容量もディスクの数十〜数百GBまで使えるため、実質的な制限はありません。Dexie.js を使って非同期APIを使いやすくラップしました。
実際に遭遇したバグ:GuideKnot でデータが消えるバグが発生しました。原因は IndexedDB のスキーマバージョニングです。Dexie.js は db.version(N).stores({...}) でスキーマ変更を管理しますが、バージョン番号を更新せずにスキーマを変えると既存データにアクセスできなくなります。開発中に何度かスキーマを変えた際、バージョン番号を上げ忘れていました。本番反映後にユーザーのデータが消える前に気づいたのは幸運でした。
状態管理の設計
Resume と Palette は Jotai を選んだ
React の状態管理ライブラリは選択肢が多いですが、履歴書ツールとクリップボード管理ツールには Jotai を選びました。選んだ理由は2つあります。
1つ目は atomWithStorage の存在です。これを使うと、atom の値が自動的に localStorage に永続化されます。「ページを閉じても状態が残る」という挙動を、追加のコードなしに実現できます。
2つ目は Resume の「元に戻す(Undo)」機能です。Jotai には atomWithHistory(jotai-optics + 独自実装)で操作履歴を管理できる仕組みがあり、CtrlZ で状態を巻き戻す機能を比較的少ないコードで実装できました。Zustand や Redux と比較検討しましたが、React のコンポーネントツリーに自然に溶け込む atom モデルが一番しっくりきました。
FlowTick・Fortune・Modtimer は素の localStorage
状態が単純なツールには、ライブラリを入れずに localStorage.getItem / localStorage.setItem を直接使っています。ただし必ず try-catch で囲みます。
Safari のプライベートブラウジングは localStorage の書き込みで例外を投げることがあります。この try-catch がないと Safari ユーザーだけアプリが落ちます。実際に開発初期に踏みました。
この設計のトレードオフ
サーバーレス・ブラウザ完結という設計には明確な得失があります。
得たもの
- プライバシーの担保(データがサーバーに行かない)
- ランニングコストの最小化
- スケール問題からの解放
- オフライン動作(一部ツール)
- サーバーセキュリティの心配が不要
諦めたもの
- 複数デバイス間のデータ同期
- アカウント機能(ログイン・登録)
- プッシュ通知
- ユーザーデータの分析・改善フィードバック
- サーバー側でのバックアップ
「複数デバイス間の同期」は何度か要望をいただいています。スマートフォンで登録したクリップボードをPCでも使いたい、という要望はよく理解できます。ただ、これを実現するには認証基盤とサーバーサイドDBが必要になり、現在の設計思想と真っ向から対立します。
現時点での回答は「URLで共有する」(Palette)か「エクスポート/インポートで手動移行する」です。完全な解ではありませんが、サーバーを持たないという制約の中での現実的な妥協点として受け入れています。
PDF 生成だけは特殊解が必要だった
Resume のPDF出力は、サーバーレス設計の中で最も困難だった部分です。
最初に試した window.print() はCSSで印刷レイアウトを制御できず、実用的なPDFになりませんでした。html2canvas + jsPDF はテキストが画像化され、PDFの文字がコピーできなくなります。jsPDF 単体では日本語フォントが豆腐(□)になります。
最終的に採用したのは @react-pdf/renderer です。React コンポーネントとして PDF のレイアウトを記述し、ブラウザ内で PDF バイナリを生成できます。日本語フォント(Noto Sans JP)のサブセットを遅延ロードすることで、初期表示を遅らせずにフォント埋め込みPDFを生成できました。
ただし @react-pdf/renderer の学習コストは高いです。HTML/CSS の知識はほぼ役立たず、flexbox ベースの独自レイアウトシステムを理解する必要があります。また、ライブラリ自体のバンドルサイズが大きい(約500KB)ため、React.lazy() でコード分割して必要になったときだけロードするようにしました。
注意点:@react-pdf/renderer は Web Workers を使って PDF をレンダリングします。Safari では Web Workers の挙動が Chromium と微妙に異なり、Safari でのみ PDF 出力が固まるバグが発生しました。Safari の Web Workers はメインスレッドとのメッセージパッシングに若干の制約があり、大きな ArrayBuffer のやり取りで詰まることがあります。transferable objects を正しく扱うことで解決しました。
モノレポで6ツールを管理する
6ツールを別々のリポジトリで管理すると、共通コードの重複やバージョン管理で辛くなると判断し、モノレポ構成にしました。
共通の hooks・components・lib は apps/shared/ に置き、各アプリから参照します。Vite のエイリアス設定で @shared/hooks/useMediaQuery のようにインポートできます。
CI/CD は GitHub Actions で、各アプリのソースが変更されたときだけそのアプリをビルド・デプロイするように path filter を設定しています。6ツール全部をいつもビルドすると無駄が多いためです。
モノレポで良かった点:クッキー同意バナー(cookie-consent.js)やフッターのスタイル変更を、全ツール一括で適用できます。別リポジトリで管理していたら6回同じ修正を繰り返すところでした。
この設計で作れないもの・向かないもの
サーバーレス・ブラウザ完結設計は万能ではありません。以下のようなサービスには向きません。
- リアルタイムコラボレーション:複数ユーザーが同時編集するツール(Google Docs 相当)はサーバー(WebSocket)が必須
- 外部APIを呼ぶツール:APIキーをブラウザに置くのは危険なため、プロキシサーバーが必要
- 大量データの処理:ブラウザのメモリ・CPUには限界がある
- 通知機能:バックグラウンドで動くサーバーがないとプッシュ通知できない
utily.net のツールがこれらのユースケースを持たないのは、半分は設計の制約から、半分は意図的な機能スコープの絞り込みです。「シンプルで長く使えるツール」を作るには、機能を増やさないことも重要な判断だと思っています。
まとめ:バックエンドなし設計を選ぶべき状況
今回の経験から、サーバーレス・ブラウザ完結が適している状況を整理すると:
- 個人情報・機密データを扱うツール(履歴書、メモ、スニペット)
- 個人開発でランニングコストを最小化したい
- デバイス間同期よりもプライバシーを優先したい
- ユーザー登録・ログインなしに使えるツールを作りたい
- オフラインでも動く必要がある
逆に、チームリアルタイム共有・外部サービス連携・大量データ処理が必要なら、サーバーを持つ設計が正解です。設計はトレードオフの選択であり、何が正しいかはユースケース次第です。
utily.net の6ツールは現時点ではこの設計で十分機能しており、想定していたユースケースはほぼカバーできています。「複数デバイス同期」の要望には今後も向き合い続けますが、サーバーを持たない設計を変える予定は今のところありません。