子ども向けアプリで一番時間を使ったのは、機能ではなく「預かる情報を減らすこと」だった

親子で使うアプリを個人開発しています。作りながら、想定よりはるかに時間を取られたのは、新しい機能ではありませんでした。何を保存しないか、どうすれば消せるか、外部に何を送っているか——この3つを詰める作業です。この記事は、その過程で決めたことと、実際にやった棚卸しの記録です。同じように個人で子ども向けのものを作る人の参考になれば。

「子どもが使う」と決めた瞬間に、設計が変わった

RingiQuest は、家庭のお手伝いとごほうびを管理するアプリです。子どもがタスクをこなしてポイントを貯め、ごほうびが欲しくなったら保護者に「稟議」を出す。使うのは、保護者と子どもです。

この「子どもが使う」という一点が、設計の順番を変えました。最初に決めたのは、機能ではなく「保存しないもの」のリストです。

入力欄を、そもそも作らない

子どもについてデータベースに保存しているのは、実質的にニックネームと、表示に関わる設定(役割、ことばのレベル、テーマ)だけです。

本名、生年月日、学年、学校名、写真、位置情報。これらは「任意入力にする」のでも「暗号化して保存する」のでもなく、入力欄そのものを作りませんでした

この判断には、単純ですが強い理由があります。集めていない情報は、漏れません。誤って第三者に送ることもなく、削除し忘れることもなく、将来の仕様変更で使ってしまうこともない。暗号化やアクセス制御は運用で壊れる余地がありますが、存在しないデータは壊れようがありません。

それでも本名を入れる人はいる

とはいえ、ニックネームの欄に本名を入れることは技術的に可能です。禁止する手段はありません。

そこで、セットアップで子どもの名前を入力する画面に、「ニックネームをおすすめします、本名は避けてください」という趣旨のヒントを添えました。強制はできませんが、何も言わないよりはずっとましです。

これは棚卸しの結果として後から足した部分です。設計上は最小化できていても、利用者の実際の入力までは制御できない——この当たり前のことに、監査するまで気づいていませんでした。

実際にやった個人情報の棚卸し

一般公開の前に、データがどこを通るかを一通り洗いました。項目はこうです。

  • 保存——データベースに何のカラムがあるか
  • 入力——画面上でどんな情報を入れさせているか
  • API 応答——サーバーがクライアントに何を返しているか
  • ログ——エラーログや動作ログに何が出ているか
  • AI 送信——外部の推論サービスに何を送っているか
  • 通知——プッシュ通知の中身に何が入っているか
  • バックアップ——エクスポートに何が含まれるか
  • クライアント保存——ブラウザ側に何を残しているか
  • 第三者——外部に送信しているものがあるか
  • 削除——消したいとき、実際に全部消えるか

このうち、個人開発で見落としやすいのはログAPI 応答だと思います。

ログは、意図せず個人情報の保管場所になる

デバッグのために「どのユーザーで何が起きたか」を出力していると、それはそのまま個人データの複製になります。しかもログは、本体のデータベースと違って削除機能の対象から漏れがちです。

RingiQuest では、ログに子どものデータを出していないことを確認しました。エラーの記録も、失敗の理由だけを出して、中身は出さない方針にしています。

後から追加した不正登録対策(CAPTCHA の一種)でも、同じ方針を適用しました。検証に失敗したときログに残すのは失敗の理由だけで、IP アドレスもトークンも出しません。

API 応答は「返しすぎ」が起きやすい

データベースの行をそのまま JSON にして返すと、必要のない情報まで漏れます。パスワードのハッシュ、内部の識別子、他の家族の存在。

対策として、利用者の情報を返すときは公開してよい項目だけを組み立てる関数を1か所に置いて、必ずそこを通す形にしました。個別の画面ごとに「この項目は返していいか」を判断すると、いつか必ず間違えます。

あわせて、すべてのデータを家族単位で区切っています。他の家族のデータを指す ID を渡しても、見えません。このとき返すのは「権限がない」ではなく「存在しない」という応答にしました。「権限がない」と返すと、そのデータが存在すること自体が分かってしまうからです。

消せることまでが「預かる」の責任

データを預かるというのは、貯めることではありません。返せることと、消せることまでが含まれます。

持ち出せる

保護者の操作で、ご家族の記録をまとめてダウンロードできます。クエスト、稟議、ポイントの履歴。

ここで意識的に除外しているものがあります。パスワードや暗証番号のハッシュ、端末のトークン、通知の鍵、セッション。これらは「その家族のデータ」ではありますが、手元のファイルに書き出す意味がなく、書き出すと危険なだけです。エクスポートは全部入りがいい、という発想を一度疑う必要がありました。

消せる

保護者の操作で、家族のアカウントごと完全に削除できます。記録も、プロフィールも、登録した端末も、通知の購読も、家族のレコード自体も消えます。

実装で気をつけたのは取りこぼしです。テーブルが増えるたびに、削除処理に足し忘れる余地が生まれます。特に、家族の ID を直接持っていないテーブル(たとえば「誰がいつクエストを完了したか」の記録)は、間接的にたどらないと消せません。

この種の処理は、後から足すほど漏れます。最初から入れておくべきものの筆頭です。

期限を決めて、自動で消す

もうひとつ、後から気づいて直した部分があります。

このアプリは、子どもに朝と夜、応援メッセージを送ります。その本文には子どものニックネームと、その日の活動が含まれます。そして初期の実装では、これが無期限に溜まり続けていました

機能としては何も壊れていません。ただ、目的を果たした後まで持ち続ける理由がない。そこで保存期間を30日に決めて、自動で消すようにしました。

ここに、ひとつだけ例外を作っています。各利用者の最新の1件だけは、何日経っても消しません。ホーム画面に最新の応援メッセージを表示しているので、単純な期限切れ削除にすると、定期実行が長く止まったときにその表示が空になってしまうからです。

こういう例外は、書いておかないと必ず忘れます。プライバシーポリシーの保存期間の項にも、この例外まで含めて明記しました。日数を変えるときは両方直す必要がある、ということも含めて。

AI に何を送っているのか、を説明できる状態にする

このアプリには AI を使った機能がいくつかあります。応援メッセージ、クエストの提案、対価ポイントの提案、保護者向けの週次レポート。

AI を使うということは、外部の推論サービスにデータを送るということです。ここは説明できる状態にしておく必要がありました。

送っているものを、最小限に絞る

送っているのは、子どものニックネームと、活動の要約(今日のクエスト、達成の件数など)です。本名も生年月日も、そもそも保存していないので送りようがありません。集めない設計が、ここで効いてきます。

学習に使われないことを、どう確認したか

利用しているサービスの提供元が公開しているデータ利用の方針を確認し、いつ時点のどの版を見たかを記録に残しました

ここで重要なのは、これは契約書ではなくドキュメントであるという点です。予告なく改訂される可能性があります。だから「一度確認したから大丈夫」ではなく、定期的に見直す作業として運用に組み込みました。記述が変わったら、プライバシーポリシーのほうを直す必要があります。

外部サービスを使うということは、その事業者の方針の変化を追い続ける義務を引き受けるということでもあります。個人開発では、ここを軽く見がちです。

AI は提案まで、決めるのは保護者

設計上の線引きとして、AI に決めさせないことにしました。

ポイントの金額も、承認するかどうかも、レートの変更も、確定するのは必ず保護者の操作です。週次レポートも、達成件数や獲得ポイントといった数字はアプリ側が集計し、AI が書くのは文章の部分だけです。

これはプライバシーの話とは少しずれますが、根は同じだと思っています。説明できないものを、家庭の決定に混ぜないということです。

アプリ本体に第三者トラッカーを入れない、という判断

アクセス解析すら入れていない

アプリ本体(app.ringiquest.com)には、アクセス解析も、広告のタグも、外部のエラー収集サービスも入っていません。ひとつもです。

個人開発でこれをやると、失うものがはっきりあります。どの画面が使われているのか、どこで離脱しているのかが、まったく分かりません。改善の手がかりが、利用者からの声だけになります。

それでもこの選択をしたのは、子どもが使う画面だからです。解析ツールを入れるということは、子どもの操作を第三者のサーバーに送るということです。それをやってまで得たい情報ではありませんでした。

ただし「何も記録されていない」わけではありません

ここは正確に書いておきたいところです。ページに解析用のプログラムを入れていないことと、通信が誰にも見えていないことは、同じではありません

このアプリは Cloudflare という会社のネットワークを通って配信されています。通信がそこを通る以上、リクエストの数や、アクセス元の国、ブラウザの種類といった情報は、配信の過程で必ず扱われます。管理画面を開けば、1日に何件のリクエストがあったか、どの国からか、遮断した攻撃が何件かを、私は見ることができます。JavaScript を1行も入れていなくても、これは発生します。

これは配信と攻撃対策のために生じるもので、止める方法はありません(止めるとしたら、自前でサーバーを建てて自分でログを取ることになり、むしろ扱う情報は増えます)。そして重要なのは、ここで見えるのは通信の統計であって、お子さんのニックネームや、何のクエストをやったかとは結び付いていないということです。どの画面がよく使われているか、どこで離脱したかも、これでは分かりません。

「解析ツールを入れていません」という言い方は、埋め込みタグの話としては正確です。ただ、それを「何ひとつ記録されていない」と読まれるなら、それは言い過ぎになります。この区別を書かずに済ませることもできましたが、それでは次の節に書くことと矛盾してしまいます。

そして実際に、線を引いておいてよかった日が来た

この記事を最初に書いた時点では、紹介サイトのほうにも解析タグを一切入れていませんでした。そして「何も送っていません」と、条件なしで書いていました。

その後、いまお読みいただいているこの紹介サイト(ringiquest.com)に、Google AdSense の広告を掲載することにしました。広告は Cookie を使い、閲覧の情報を Google に送ります。つまり、この記事を表示しているこのページは、もう「何も送っていない」ページではありません。

だからこの節も書き換えました。掲げた原則が変わったなら、原則を掲げた文章のほうを直すのが筋だからです。古い記述を残したまま広告だけ足せば、それは単に嘘になります。

重要なのは、アプリ本体の側は何も変わっていないことです。お子さんが操作する画面には、広告も解析ツールも入っていません。そして今後も入れません——AdSense のプログラムポリシー自体が、ソフトウェアの内部やプライベートな通信が主要素の画面への広告掲載を禁じているので、規約の面からも入れられません。

この「アプリは無傷のまま、紹介サイトだけを変えられる」という状態は、偶然ではありません。次の節に書いたとおり、最初から両者を別のプログラムとして分けておいたからこうなりました。

もうひとつの例外——新規登録の不正対策

ひとつだけ、外部にデータが渡る箇所があります。新規登録時の不正対策です。

誰でも家族を作れるようにした結果、スクリプトで大量にアカウントを作れる状態になりました。これを塞ぐために CAPTCHA の一種を導入したのですが、この仕組みは性質上、IP アドレスやブラウザの特徴を検証サービスに送ります

アプリ本体で外部にデータが渡るのは、いまのところこの1か所だけです。プライバシーポリシーに、新しいデータフローとして追記しました。掲げた原則に例外があるなら、その例外こそ書く必要があります。

ランディングページを、別のサービスとして分けた

関連する構造上の判断をひとつ。このブログを含む紹介用のページは、アプリ本体とは完全に別のプログラムとして動いています。動かしているサーバーの設定ファイルも、デプロイの経路も、別々です。

分けた理由は、将来の判断に備えるためでした。当時こう書いていました——「紹介ページに何らかの計測タグを入れる判断をする日が来るかもしれません。そのとき、両者が同居しているとアプリ本体の『トラッカー皆無』という性質まで巻き込んで汚れます」。

そして、実際にその日が来ました。この紹介サイトには広告が入り、そのために外部スクリプトの読み込みを許可する必要が生じ、結果として厳しく締めていたセキュリティ設定(Content Security Policy)も緩めることになりました。

もし両者が1つのプログラムに同居していたら、この緩和はアプリ本体にもそのまま及んでいました。子どもが使う画面のセキュリティ設定が、広告の都合で緩む。それは受け入れがたい。分けてあったおかげで、緩んだのは紹介サイト側だけで済みました。

設計判断の効果というのは、たいてい後になってしか確かめられません。これは、境界を引いておいたことが実際に効いた数少ない実例です。いつか変える可能性のあるものと、絶対に変えたくないものは、最初から別の入れ物に入れておくといい。

個人開発者がやるべき最低限のチェックリスト

一通りやってみて、これだけは外せないと思ったものを挙げます。

1. 集めない

  • その項目は、機能のために本当に必要か。「あとで使うかも」で列を作らない
  • 必要なら、識別子で足りないか。本名でなくニックネームで動くか
  • 入力欄を作らないのが最強。任意入力にすると、必ず誰かが入れる

2. 出せる・消せる

  • エクスポートと完全削除は、最初から入れる。後付けは必ず漏れる
  • エクスポートに含めてはいけないもの(認証情報など)を意識的に除外する
  • 間接的にしかたどれないテーブルを、削除処理から落としていないか確認する
  • ログや通知の履歴など、本体以外に溜まる場所を忘れない

3. 外に出るものを把握する

  • 外部サービスに何を送っているか、項目レベルで言えるようにする
  • 委託先の方針は、確認した日付と版を記録する。一度でなく定期的に
  • 掲げた原則に例外があるなら、その例外こそポリシーに書く

4. 認証まわりの基本を落とさない

  • パスワードや暗証番号のハッシュ化は、推奨されている強度に合わせる。古い設定のまま放置しない
  • メールで送る確認リンクなどのトークンは、平文で保存しない(ハッシュだけ持つ)。単回使用にして、期限を切る
  • 存在を漏らさない。「そのアドレスは登録されていません」と返すと、登録の有無が分かってしまう

この記事は法的助言ではありません ここに書いたのは、あくまで個人開発者としてやった実装上の判断です。法令の要求水準や、事業としての義務については、必要に応じて専門家に相談してください。実際、このアプリのプライバシーポリシーも、雛形として作ったうえで見直しを前提にしています。

まとめ——信頼は、機能では買えない

作っている最中、何度か「ここまでやる必要があるだろうか」と思いました。家族数人が使うアプリに、完全削除機能もエクスポートも過剰ではないか、と。

結論としては、必要でした。理由は2つあります。

ひとつは、後から入れるのは本当に大変だということ。テーブルが増えてから削除処理を書くと、必ずどこかを落とします。最初に入れておけば、新しいテーブルを足すたびに削除処理も更新する習慣がつきます。

もうひとつは、これが説明できる状態を作る作業だったということです。「子どもの何を保存していますか」と聞かれて即答できること。「アプリに解析ツールは入っていますか」に「ひとつも」と答えられること。そして、方針が変わったときに、どこを直せばいいかが分かること——この記事に広告のことを書き足せたのも、最初にデータの通り道を全部洗い出してあったからです。個人が作ったアプリを家庭で使ってもらうために必要なのは、機能の多さではなく、この種の説明可能性だと思っています。

機能は後から足せます。一度預かってしまった情報は、後から集めなかったことにはできません。

RingiQuest を試してみる この記事で書いた方針で作っている、親子のためのタスク&ごほうび管理アプリです。完全無料・アプリ内に広告なし。ご家族の名前と、あいことばを決めるだけで始められます。

RingiQuest を無料ではじめる