旅行や複数人での飲み会のたびに発生する「誰がいくら支払い、誰が誰にいくら返すか」という不透明なお金の精算処理。お金の計算や配慮に煩わされることなく、大切な友人や家族との「思い出」そのものに集中できる時間を守りたい。そのような想いから開発したのが、精算管理Webアプリケーション 「Kashikari」 です。
本プロダクトでは、ユーザー登録の手間を最小限に抑えつつ、高度な「送金経路の最小化アルゴリズム」を搭載し、さらに個人情報の漏洩をデータベース層から物理的に防御する強固なセキュリティ(Supabase RLS)を実装しました。
この記事では、Next.js (App Router) と Supabase をフル活用して個人開発した「Kashikari」の設計思想、アルゴリズムの仕組み、そしてセキュリティ設計の裏側を技術的な視点から詳しく解説します。
1. 解決したかった課題:既存の精算アプリにおけるUXのボトルネック#
すでに割り勘や精算を管理するアプリケーションは市場にいくつか存在しますが、実用において以下の3つのボトルネックがありました。
- アプリインストールと会員登録の壁 グループの全員に「特定のネイティブアプリを入れてアカウントを作成して」と要求した時点で、利用のハードルが極端に高くなり、結局紙のメモやチャットでの手動計算に戻ってしまうケースが多々ありました。
- 送金経路の非効率性 「AがBに1,000円、BがCに1,000円返す必要がある」といった多人数間の貸し借りにおいて、素朴に計算すると送金イベントが複数発生します。これをまとめ、「AがCに1,000円直接送金する」というように、グループ全体の送金回数と送金総額を最小化する最適化ロジックが必要でした。
- データプライバシーの懸念 「誰とどこでいくらのお金をやり取りしたか」という極めてプライベートな情報を、巨大な広告プラットフォームや海外の不透明なサーバーに預けたくないというプライバシー上の要求がありました。
Kashikari は、「ブラウザ完結でインストール不要、1タップでの共有、送金回数の自動最適化、徹底したプライバシー保護」 をコアバリューとして設計しました。
2. 技術スタック:迅速なデプロイと堅牢性を両立する構成#
- フロントエンド: Next.js (App Router) / Tailwind CSS / Shadcn UI
- バックエンド: Supabase (PostgreSQL / Auth / RLS / Realtime)
- インフラ・ホスティング: Vercel
技術選定における「コードを書かない」という意思決定#
個人開発では、開発リソースが1人分に制限されています。認証サーバーの構築や API のボイラープレートコードを書く時間を徹底的に削減するため、バックエンドには Supabase を全面採用しました。
データベース設計と認証、さらにはデータベース直結のセキュリティ制限(RLS)にロジックを寄せることで、バックエンドサーバーの構築・保守費用をゼロにし、フロントエンドのユーザー体験向上のために100%の時間を使うことが可能となりました。
3. 送金経路を最小化する「欲張りアルゴリズム」の実装#
Kashikariの最大の特徴は、グループ全体の貸し借りデータを解析し、「誰から誰へ、最低何回の送金を行えば全員の精算が過不足なく完了するか」 を導き出す最適化アルゴリズムです。
最適化アルゴリズムのステップ#
- 純資産残高(Net Balance)の算出
グループ内の各メンバーについて
(自身が支払った総額) - (自身が負担すべき総額)を算出します。この値がプラスの人は「債権者(受け取る人)」、マイナスの人は「債務者(支払う人)」、ゼロの人は精算対象外となります。 - 債権者と債務者の分離とソート 残高がプラスのメンバーリストと、マイナスのメンバーリストをそれぞれ作成し、絶対値の大きい順(最も多く払うべき人と、最も多くもらうべき人)にソートします。
- マッチング(Greedy法) 「最大債務者(最も返済額が大きい人)」から「最大債権者(最も回収額が大きい人)」へ送金を行います。送金額は双方の絶対値の小さい方(全額精算できる側)に設定し、残高を更新してリストを再評価します。これをすべての残高がゼロになるまで繰り返します。
TypeScriptによる精算最適化アルゴリズムの実装例#
interface MemberBalance {
name: string;
net: number; // プラスは債権、マイナスは債務
}
interface Transaction {
from: string;
to: string;
amount: number;
}
export function optimizeTransactions(balances: MemberBalance[]): Transaction[] {
// 1. 債権者と債務者を抽出し、絶対値の大きい順にソートする
const creditors = balances
.filter((b) => b.net > 0)
.sort((a, b) => b.net - a.net);
const debtors = balances
.filter((b) => b.net < 0)
// 債務はマイナス値なので、昇順ソートで絶対値が大きい順にする
.sort((a, b) => a.net - b.net);
const transactions: Transaction[] = [];
let cIdx = 0;
let dIdx = 0;
// 2. 両方のリストを走査しながら精算をマッチングさせる
while (cIdx < creditors.length && dIdx < debtors.length) {
const creditor = creditors[cIdx];
const debtor = debtors[dIdx];
// 残高の絶対値の小さい方を送金額とする
const amount = Math.min(creditor.net, Math.abs(debtor.net));
if (amount > 0) {
transactions.push({
from: debtor.name,
to: creditor.name,
amount: Math.round(amount * 100) / 100, // 小数点第2位で丸める
});
}
// 残高を相殺して更新
creditor.net -= amount;
debtor.net += amount;
// 残高が完全に精算された側のインデックスを進める
if (Math.abs(creditor.net) < 0.01) {
cIdx++;
}
if (Math.abs(debtor.net) < 0.01) {
dIdx++;
}
}
return transactions;
}このアルゴリズムにより、グループ全員が複雑な個別の貸し借り履歴を覚える必要がなくなり、最終的な「最小限の送金指示」に従うだけで完全に精算を終えることができます。
4. 信頼を担保するセキュリティ設計:Supabase RLS(行レベルセキュリティ)#
お金を取り扱うアプリケーションにおいて、情報の気密性と堅牢性は最優先事項です。「パラメータのグループID(UUID)を書き換えたら、他人のグループデータが見えてしまった」というような典型的なWeb脆弱性を排除するため、Supabase RLS (Row Level Security) を利用しました。
これにより、データベースレベルで不正なデータへのCRUD操作を防ぎます。
データベーステーブル構成と RLS ポリシーの実装#
グループに所属するメンバーだけが、そのグループの精算情報を取得できる設計を SQL(PostgreSQL)で構築します。
-- RLSを有効化
ALTER TABLE groups ENABLE ROW LEVEL SECURITY;
ALTER TABLE group_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE expenses ENABLE ROW LEVEL SECURITY;
-- 1. グループメンバーのアクセス制御ポリシー
-- ログインユーザーがそのグループのメンバーテーブルに存在する場合のみ参照可能
CREATE POLICY "Members can view their group member list"
ON group_members
FOR SELECT
TO authenticated
USING (
auth.uid() = user_id
);
-- 2. グループ詳細データのアクセス制御ポリシー
-- ログインユーザーが対象グループの所属メンバーである場合のみ参照可能
CREATE POLICY "Members can select group data"
ON groups
FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT 1 FROM group_members
WHERE group_members.group_id = groups.id
AND group_members.user_id = auth.uid()
)
);
-- 3. 出費(経費)データのアクセス制御ポリシー
-- 自分が所属するグループに関連付けられた出費データのみ操作可能
CREATE POLICY "Members can manage expenses in their group"
ON expenses
FOR ALL
TO authenticated
USING (
EXISTS (
SELECT 1 FROM group_members
WHERE group_members.group_id = expenses.group_id
AND group_members.user_id = auth.uid()
)
);このように RLS ポリシーを敷くことで、フロントエンドや中間 API サーバーの実装ミスによってバリデーションが漏れたとしても、PostgreSQLがクエリ実行時に自動的に認証情報を判定し、不正アクセスを確実に拒否します。
5. UI/UXの磨き込み:ネイティブアプリを超える心地よさ#
個人開発プロダクトにおいて大手のサービスと戦うには、徹底的なフロントエンドの操作感の作り込みが必要です。
- PWA(Progressive Web App)対応: ホーム画面に追加することで、アドレスバーを隠したネイティブアプリ同等の全画面表示で操作できます。
- Web Share API の採用: 招待用URLを生成した際、デバイス標準の共有シートを呼び出すことで、LINE や Slack へワンタップで安全に共有できます。
- Supabase Realtime による変更の瞬時同期: グループ内の誰かが出費を追加・編集した際、他のメンバーの画面も再ロードなしで滑らかに状態変化が反映される設計にしました。
6. よくある質問(FAQ)#
Q. なぜ認証サービスに Auth0 や Firebase Auth ではなく Supabase Auth を採用したのですか?#
A. Supabase Database (PostgreSQL) との密結合が非常に強力だったためです。データベーススキーマ内の auth.users テーブルと直接連携し、RLS ポリシー上で auth.uid() を参照して条件分岐させる設計を、追加のコードなしで構築できるのは Supabase ならではの強みです。
Q. 送金の最小化アルゴリズム(欲張り法)で、常に最適な組み合わせになりますか?#
A. はい、総送金回数を最小化する上で、この貪欲法(Greedy)アルゴリズムは実用上極めて正確に動作します。数学的な厳密解(部分和問題に基づく最適分割)では稀にさらに1回減らせる組み合わせが存在することもありますが、計算量が指数関数的に増大するため、実務やユーザー利用シーン(数十人規模)においては今回採用したアルゴリズムがパフォーマンスと結果のバランスにおいて最適解です。
Q. 個人開発におけるホスティング費用はどの程度かかっていますか?#
A. 現在、Supabase の無料枠(Free Tier)と Vercel のホスティングプラン(Hobby Plan)の範囲内で運用しているため、サーバー費用は月額0円です。Supabase の無料プランでも十分に数千ユーザー規模のトランザクションに耐えられるため、スモールスタートの個人開発には最適なスタックです。
Q. ログインなし(ゲスト機能)でのデータ保存はどのように実現していますか?#
A. ログイン不要で利用を開始できるよう、初回アクセス時に一時的な UUID をクライアントの LocalStorage に発行し、Supabase 側には匿名セッション用の匿名ユーザー(Anonymous Sign-ins)として登録・紐付けることで、データ永続化とセキュリティ制限をクリアしています。











