かりんだよ!

5周年記念ライブツアーに向けてアプリ、いや、Webサイトなんだけどアプリ風に!
ファンのみなさんがセットリストを投稿したり、スタンプを集めたりできるWebアプリを作っています。

本日はデバッグ配信!



このブログは配信前に書いてますので
配信がどうなったか分かりませんが

まあまあの結果だったからこのブログが上がっているということでしょう。


アプリの運営は私1人。
自分がステージに立つ日に、アプリ側で困ったことが起きる可能性も大いにあります。。


なので、機能の多さだけでなく、参加しやすさやトラブル時の操作も大事だと思い設計しています。


今回も2本に分けてブログを書きます!
1本目はCloudflare WorkersとD1を使った仕組みと、その設計にした理由。


2本目では、AIとどう作業を進めたかを書きます。

2026年10月1日時点の開発記録です。


全8会場15公演のツアー初日は10月17日なので、会場で運用した結果の報告ではありません!!


下心をお話すると
これを見てぜひ、12/1(火)のZeppでのライブに来て欲しい!!!
という気持ちもありまして、、
気になってくださった方はぜひ遊びに来てね


ファンのみんながセットリストを作るアプリ


佳林党員(宮本ファンのみんなの総称)だけではなく、ハロプロを知ってるみなさんが楽しめるものを作りたい!


そこから宮本佳林がツアーをしてることを知って、足を運んでいただきたい!という作戦です。


中心は、自分の考えたセットリストを作って投稿する機能。


会社の方にお願いして、サブスク公開されてる楽曲約3,200曲分のデータを頂きました。


曲を選び、MCやアンコールを挟んで並べられます。


他の人の投稿やランキングを見たり、いいねを押したりもできます。

Xに作ったセトリの画像をポストもできます。


DBの書き込みが増えすぎると利用上限や追加料金に影響するかも!!ということで、いいねは1日3回、セトリ投稿は1週間に1度にしました。


さらに、RPGのような遊びも加えました。


勇者・戦士・僧侶・魔法使いからジョブを選び、称号を集めたり、宮本の衣装を着せ替えたり、ペットを連れたり。物販カードのQRコードで貯めるスタンプや、公演の演出を決めるルート投票もあります。


ライブを見に来るまでの時間も、帰ったあとの時間も、ツアーの一部として楽しんでもらいたいと思い制作しました。


ここからはAIと書いていきたいと思います。


Next.jsをCloudflare Workersで動かす


現在の主な構成は次のとおりです。

役割使用しているもの
画面とアプリケーションNext.js 15 App Router・React・TypeScript
スタイルTailwind CSS v4
実行環境Cloudflare Workers・@opennextjs/cloudflare
データベースCloudflare D1
bot対策Cloudflare Turnstile
入力値の検証Zod
QR読み取りの補助jsQR


Next.jsをOpenNextのCloudflare向けアダプターでWorkersにデプロイしています。

投稿、冒険者の情報、スタンプなどの保存先はD1です。データベースの変更はSQLのマイグレーションファイルとして管理しています。

テスト用のstagingと本番用のproductionは、Worker、D1、シークレットを分けました。
配信でみんなでデバッグもしたい!と思ったし、DBにテストデータがある状態で本番公開したくない。直す時もテストで試してから本番に反映したいな、、と思ったからです!


登録の手順を減らして、引き継ぎコードを用意する


メールアドレスやパスワードの登録は求めていません。初回アクセス時にUUIDの識別子を作り、

ブラウザのlocalStorageに保存します。

以前アンケートサイトで使った手法ですね!


実装ではこれをdevice_idと呼んでいます。

今回は利用者ごとのデータも管理するので、ジョブと5桁の番号を登録するとアプリ内の冒険者になり、その組み合わせがオリジナル番号になるようにしました。端末やブラウザを変えるときには、12桁の引き継ぎコードを使います。


会場で初めて知った人にも、なるべく少ない手順で参加してもらいたい。メールアドレスや電話番号などの個人情報もなるべく持ちたくない。そう考えて選んだ方式です。


ただし、device_idは物理的な端末や人間を厳密に識別するものではありません。

保存データを消したり、別のブラウザで開いたりすると、同じ人でも別の利用者として扱われる場合があります。1人1アカウントも、この仕組みだけでは保証できません。


きっと、ハロプロファンは優しいから何個も作ったりしないよな、、
4つ目以降は「1人1アカウントでお願いします」と表示もつけたしな、、
と思わせてくれるハロプロファンのみんなが大好きです。


端末にデータが見つからなくなっても、サーバー上の記録が消えたとは限りません。その記録へ戻るための引き継ぎコードは、登録直後に表示し、アプリ内でも再確認できるようにしました。


手順を軽くする分、復元方法をきちんと伝えることも必要でした。


日付の判定と、テスト用の時計


スタンプの取得期間、投票の締切、お知らせの公開などの判定はサーバー側で行い、公演日の扱いは日本時間にそろえています。スマホの時計を基準にすると、端末の設定で結果が変わる可能性があるので。


そのうえで、公演日を待たずにテストできるよう、用途ごとに時計を分けました。


時刻取得用途
realNow()作成日時、監査ログ、レート制限などの実時刻
getAppNow()公演日や締切など、アプリの状態を決める時刻

普段は同じ時刻です。

stagingやlocalでテスト日時を有効にしたときだけ、getAppNow()が仮想時刻を返します。 


この時計は固定せず、設定した瞬間から進み続けます。
仮想時刻 = 設定した日時 + 設定してからの実時間の経過23時59分に設定すれば、画面を開いたまま0時をまたぐ動きも確認できます。


ただ、セトリ投稿の7日間のクールダウンは、実時刻で記録した作成日時が基準です。

テスト時計だけ進めても2回目を投稿できないぞ!ということで、stagingでは環境変数でクールダウンを短くしています。


どの判定にどちらの時計を使うかまで整理する必要がありました。たいへんだね、、

本番ではスタンプだけを救済する

本番でアプリ全体の時計を変えると、スタンプだけでなく投票やお知らせにも影響します。


そのため、本番ではテスト日時の設定を無視し、設定APIも404を返すようにしました。 


スタンプの通常の取得期間は、公演日の0時から翌日の1時まで。


終演後に物販へ寄って、帰りの電車でQRを読むころには日付が変わっているかもしれないからです。


さらに、管理画面から特定の公演のスタンプを、その日だけ追加で有効にする救済機能を用意しました。対象は1公演と1日。


日付が変われば失効し、救済中は管理画面にバナーも出ます。


対応する範囲を絞るほうが、私1人でも影響を把握しやすいと考えました。この案に決まるまでのAIとのやり取りは、2本目で書きます。


会場で同じIPアドレスを使う人を止めない


レート制限は、短時間のリクエスト数を制限する仕組みです。ただ、会場Wi-Fiや携帯回線の仕組みによって、多くの人が同じ送信元IPアドレスになる場合があります。


IPだけで厳しく制限すると、普通に操作している別々のファンをまとめて止めてしまうかもしれません。


そこでdevice_id単位の制限を中心にし、IP単位の制限は大きな連打への補助として緩く併用する方針にしました。一斉投票のような、正常なアクセス集中も想定しています。


ただし、現在のWorkers内のインメモリカウンタは、全リクエストで共有される厳密なカウンタではありません。同じ実行インスタンスに届く保証がないため、事故的な連打を減らす補助として扱っています。(Cloudflare Workersの実行の仕組み)

厳密なカウントには、状態を一元的に調整する仕組みが必要です。Durable Objectsなどが検討対象になりますが、結果整合性のあるWorkers KVへ保存するだけでは、同時更新に対応した厳密なカウンタにはなりません。(Workers KVの整合性、同時書き込みについて)
ここは説明できるほど理解できてない気もしますが、制限が厳しすぎても動かないトラブルが起きるんだな、というのはAIくんたちとお勉強しました。


QRコードの役割と、アプリ内で読む理由


QRコードは2種類あります。

種類配布や設置取得できるもの
常設QR会場に設置称号
スタンプQR物販カードに印刷対象公演のスタンプ

スタンプQRには公演ごとの識別子を使い、stagingとproductionでも値を分けています。


ただし、購入履歴との照合はしていません。


QR画像が共有されれば現地にいない人が取得できる可能性もあり、厳密な来場証明や購入証明としては扱っていません。


ここも、みんなを信頼しての実装。

厳しくしすぎて楽しいものが作れないより、みんなを信頼して楽しい機能が作りたいなと思いました!


読み取りでは、ホーム画面に追加したWebアプリを使っていた人が、標準カメラ経由でSafariを開くと、別の利用者として扱われるケースがありました。

同じスマホでも、同じ保存情報で開くとは限りません。
そこでスタンプ画面とメニューにQRスキャナを設け、普段使っているアプリの中で読み取ってもらう導線にしました。

「写真から読み取る」入力も、確認したiOS Safariではcapture="environment"があるとカメラが起動し、保存済みの画像を選べませんでした。属性を外して選べるようにしています。

BarcodeDetectorが使えない環境にはjsQRをフォールバックとして用意。解析は150ミリ秒間隔で、画像を長辺1,024ピクセルまで縮小してから処理します。BarcodeDetectorはすべての主要ブラウザで使えるAPIではありません。(BarcodeDetectorの対応状況)
QRまわりは仕様を何回も直したり話し合ったりしたところです、、!


過去の投稿をその時点の記録として残す


楽曲や投稿を画面から外すときは、

物理削除ではなくフラグで非表示にする方針です。

公開状態を変える操作と、記録を消す操作を分けています。

投稿時のキャラクターと称号も保存するので、あとで着せ替えても過去の投稿の見た目は変わりません。
この衣装で!このセトリ!とかもできるかなー!と思ったりしました。


公開用の設定と秘密の設定を分ける


Turnstileでは、公開用のsite keyをWorkersの実行時の環境変数に置いてもブラウザへ届かず、書き込みが403で失敗したことがありました。


Next.jsのNEXT_PUBLIC_*は通常、ビルド時にブラウザ向けJavaScriptへ埋め込まれます。


デプロイ先の実行時変数だけを変えても、作成済みのコードの値は変わりません。(Next.jsの公開用環境変数)


今回はnext.config.mjsで環境に応じた公開用の値を解決し、ビルド時に埋め込むようにしました。


site keyは公開する値ですが、秘密鍵や管理用の値を同じ扱いにしないことも必要です。


エラーのあとも操作を続けられるようにする


通信が一時的に失敗したのか、QRが無効なのか、登録情報がないのかで、次にしてほしい操作は変わります。原因を区別し、再試行できる場合は理由と読み直すボタンを出すようにしました。


DBの内部エラーをそのまま見せるのではなく、利用者が対処できる案内にする方針です。点検で見つかった具体的な不具合は、2本目にまとめます。


ツアー前に考えていること


ライブは夜に終わる。

同じ場所から多くの人がアクセスする。

そして運営者の宮本はステージに立っています。
その現場を想像することが、登録の手順や時間の扱い、管理画面から対応できる範囲を決めることにつながりました。

まだ会場での運用結果はありません。

実際の使われ方を見ながら、想像がどこまで合っていたかを確かめていきたいですね!
上手くいかなかったら笑ってやってください。

全力で失敗から学びます。

成功しますように!