Back to Portfolio

Case Study · 04 · OSS · MIT

SubCutter

無駄なサブスクや固定費を スパッと断ち切る、アカウント不要のローカルファースト PWA

Problem

固定費を見たいだけなのに、家計簿アプリは重すぎる。

サブスクリプション型サービスの増加で、毎月の固定費は気づかないうちに膨らんでいく。ところが既存の家計簿アプリは 銀行連携・口座登録が前提 で、プライバシーへの不安と初期設定のコストが最初のハードルになる。

「月額いくら払っているか」だけを知りたいライトユーザーにとって、家計簿アプリはそもそもオーバースペック。必要なのは、入力は最小 / 気づきは最大 の、もっと素朴な道具だった。

同時に、個人的には サーバー維持費ゼロ・アカウント不要・データは自分のもの というローカルファースト Web アプリの実装パターンを、実アプリで検証したかった。

Approach

ブラウザ内で完結する、SQL データベース付き PWA。

バックエンドを持たない前提で設計を組み直した。永続化には公式 @sqlite.org/sqlite-wasm を採用し、SQLite WASM を OPFS に書き込む ブラウザ内データベースを本番運用。OPFS 非対応の環境 (Safari 等) には sqlite3.capi.sqlite3_vfs_find('opfs') で機能検出してから localStorage へ透過的にフォールバック する抽象化層を置いている。

ローカルファーストは「機能」ではなく運用ルールとして徹底。fetch / XMLHttpRequest / WebSocket を使うコードを内部ルールで明文化して禁止し、PR レビューでブロッカー扱いにしている。サーバーを持たないと決めた瞬間に、認証・同期・バックアップの責務の位置が根本から変わった。

状態管理は外部ライブラリを入れず React Context + useReducer のみ。サブスクは多くて数十件というドメインの規模感なら、Redux / Zustand を入れる複雑性のほうがコスト過多と判断した。スタイリングも Tailwind を入れず、CSS Variables だけで薄いデザインシステムを組む。

ダッシュボード

月額 / 年額の合計、カテゴリ別ドーナツチャート、クイック追加 FAB を 1 スクリーンに集約。

サブスク管理

追加・編集・スワイプ削除。30 以上のサービス名からカテゴリを自動推定してゼロコンフィグ化。

分析 & 節約シミュレーション

カテゴリ別ランキング + 「もし解約したら年間いくら浮くか」を 1 タップで試算。

バックアップ / 復元

JSON エクスポート・インポート。サイズ (5MB) / 件数 (1000) / スキーマ / 型 / 値域を全検証してから取り込む。

PWA & ダークモード

Workbox ベースの Service Worker でオフライン動作。OS 設定追従のテーマ切替と、ホーム画面追加に対応。

ローカル完結のセキュリティ

SQL は全クエリでパラメータバインド必須。インポート JSON はスキーマ・型・値域を検証してから取り込む。

SubCutter のホームダッシュボード。月額合計 ¥12,469 とカテゴリ別ドーナツチャートを表示。
Dashboard月額 / 年額を一画面で把握
サービス追加モーダル。サービス名入力時に Netflix、Amazon Prime、Disney+、U-NEXT、DAZN が自動補完として表示される。
Auto-completeサービス名からカテゴリを自動判定
分析レポート画面。月額合計、年額合計、利用中件数、6ヶ月推移のラインチャート、カテゴリ別支出を表示。
Analyticsカテゴリ別ランキングで解約候補を見つける
Language
TypeScript 5.9
Framework
React 19 + react-router-dom v7
Build
Vite 7
Database
@sqlite.org/sqlite-wasm (OPFS) + localStorage fallback
State
React Context + useReducer (no external store)
Charts
Chart.js + react-chartjs-2
PWA
vite-plugin-pwa (Workbox)
Styling
Vanilla CSS + CSS Variables
Testing
Vitest + Testing Library + Playwright (E2E)
Quality
ESLint (Flat Config) + Prettier + tsc --noEmit
CI
GitHub Actions (typecheck / lint / test / build / E2E)

Result

v1.0.0 を MIT で公開。
運用フェーズの心理負荷はほぼゼロ。

2026-04-04 に v1.0.0 をリリースし、ソースコードを GitHub で MIT ライセンス公開 している。外部 API・バックエンドを一切持たない完全クライアント完結の構成で、守るべき境界が最小化された結果、運用フェーズの精神的負荷はほぼゼロ。

ブラウザ単体 + SQLite WASM + OPFS という組み合わせでも、実用に耐える本格アプリ を作れることを実証できたのがこのプロジェクトの成果のひとつ。

Learnings

作ってみて分かったこと。

ローカルファーストは "機能" ではなく "設計ルール"

「データを外に出さない」を原則ではなく コードに書けないルール として明文化したのが効いた。fetch / XMLHttpRequest / WebSocket を PR レビューでブロックする運用にしてから、認証・同期・バックアップの設計が一気にシンプルになった。

OPFS の COOP/COEP 要件は配信側で片付ける

SharedArrayBuffer を使うため、配信側で Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp を設定する必要がある。Vite 開発サーバーと本番配信でヘッダーを統一してしまえば、アプリ側のコードに侵入せずに解決できた。

Safari 対応は "機能検出 + 透過的フォールバック"

OPFS 非対応環境では sqlite3_vfs_find('opfs') が偽を返すので、そこを分岐点に localStorage 同期へ落とす。上位層から見ると同じインターフェースで書き続けられるよう、ストレージ抽象化は "薄すぎず、厚すぎず" の粒度に調整した。

サブスク規模なら外部状態管理ライブラリは要らない

数十件程度のドメインに Redux / Zustand を入れるのは、解決する問題より導入する複雑性のほうが大きい。React Context + useReducer で足りる領域を見極める練習になった。

テストのコスト配分は "DB × E2E の二段" で足りる

DB 層・hook・util は Vitest で単体検証し、Playwright は主要フロー (CRUD・バックアップ・テーマ切替) のみ E2E でカバー。個人開発でも回せる量に抑えつつ、壊れたら困る境界だけは確実に守れる構成になった。

"サーバーを持たない" 前提の精神的平穏

デプロイ後に障害・流出・課金のどれも気にしなくていい状態は想像以上に楽だった。守るべき境界が小さくなることの副次効果として、個人開発を継続するハードルが劇的に下がる。