Claude Code × Cloudflare|マカロン店主がLINEアプリを12時間で作った話【実装編・2026年最新】
- アプリの心臓部(Webhook受信・Claude API連携・スタッフLINE通知・管理画面UI)の全実装工程
- 各STEPの目的と所要時間(実作業 約8時間の内訳)
- 非エンジニアが踏んだ「実装編の落とし穴5つ」と Claude Code に頼んでの解決方法
- お客さま視点で見る実機の動き(受信→AI下書き→人間チェック→送信のフロー)
こんにちは!マカロン屋4年目・Claude Code歴2か月のいろはです🍰
朝の仕込みの合間や夜の発送作業の合間に、Claude Code(クロード・コード/Anthropic社のAI開発アシスタント)を触っています。
準備編、しっかり読んだよ!
いよいよアプリの中身を作る話だね
はい!今回は「動くアプリ」を作る全工程を、コードを書けない私が Claude Code に頼みながら進めた記録です🙂↕️
⚠️ 「12時間」の前提(準備編からの要約)
この連載で言う「正味12時間」は、Mac の前で実際に作業していた時間(昼食・店舗業務除く)です。Claude Code が7〜8割のコードを書いてくれた前提で、読者の方が同じことを試す場合は 半日〜2日程度(実時間6〜16時間)が現実的な見込みです。
→ 詳しい前提は 準備編 をご覧ください
📍 この記事の立ち位置(読者の方への正直なお伝え)
この記事は「作り方の完全ガイド」というより「マカロン店主が Claude Code を相棒にどう作ったかの実装記」です。とはいえ、読者の方が悩みや不安を解決して、自分でも作れるように、以下の3点を心がけて書きました:
- ① 主要なコードは記事末尾で全文公開(コピペで動かせる起点に)
- ② 各 STEP の目的・所要時間を明示(迷子にならない地図に)
- ③ 落とし穴7つ(準備編2つ+実装編5つ)・運用初日のバグ3つを正直に書く(つまずいても再開できるよう先に共有)
読者の方が同じことを試す場合、調査時間や試行錯誤を含めて 半日〜2日程度(実時間6〜16時間)を見込んで、本記事を「全体の流れと落とし穴を事前に知るための地図」としてご活用ください🍰
▼ 結論|準備編の続き・「動くアプリ」を作る
準備編で整えた土台(Mac の開発ツール・Cloudflare アカウント・LINE Developers・Claude API キー・GitHub Private リポジトリ)の上に、5つの STEP でアプリの中身(心臓部)を作っていくのが実装編です。
マカロン店で例えると、準備編は「お店のオーブン・冷蔵庫・道具を揃えた段階」。実装編はそこから「実際にレシピで生地を仕込んで、新人スタッフ(Claude)に焼いてもらえるところまで作る段階」です🍰
5/8(金)の朝9時から夕方5時まで、実作業約8時間で完成しました。準備編4時間と合わせて合計約12時間。私のような非エンジニアでも、Claude Code を相棒にすればここまで作れる、というのが今回の最大のメッセージです。
🛟 途中で挫折しそうになったら?(逃げ道3つ)
この記事を読みながら実装する中で、「分からない…」「動かない…」「もう無理…」という瞬間は必ず来ます。私もたくさんありました。
そんな時の3つの逃げ道:
- ① 1日寝かせる:その日に解決しなくてOK。翌日 Claude Code に再相談すると意外とスッと進む
- ② STEPを飛ばして先に進む:完璧に動かなくても次のSTEPは試せる。最後にまとめて修正
- ③ 準備編の「困ったときの黄金パターン」を実践(エラー全文コピペ/スクショ/「分からないので教えて」)
大事なこと:挫折してもOK。私も実装中3回ピボット(落とし穴⑥⑦参照)したし、運用初日にバグも出しました(後述)。「完璧に作れること」より「途中で止まっても再開できること」が大事です🍰
では、ここから実装編の本番です。
本当にコード書けない人でも作れたの…?
はい。私の役割は
「お願い文を伝える」
「エラーログをコピペで送る」
「動作テスト」
この3つだけ。コードはほぼ Claude Code が書いてくれました🙂↕️
📅 Claude Code 活用シリーズ・長期スケジュール
| 日付 | 出来事 |
|---|---|
| 2026年3月 | Claude Code Pro 加入(夫婦で1契約) |
| 2026年5月7日 | 連載第1弾「お返事Skill」記事公開 |
| 2026年5月7日〜8日 | Cloudflare アプリ開発(実作業 約12時間) |
| 2026年5月8日 | アプリ運用開始(夫婦2人で実運用) |
| 2026年5月9日 | 連載第2-1弾「準備編」公開 |
| 2026年5月中旬 | ⭐ 連載第2-2弾「実装編」公開(本記事) |
| 2026年8月(予定) | 運用3ヶ月レポート(FAQに追加 or 別記事) |
| 2027年5月(予定) | 運用1年総括(連載完結) |
→ 本記事は連載の中盤。実装の山場を超えれば、あとは「運用しながら微調整」のフェーズに入ります🙂↕️
▼ Claude Codeに頼んだら何が出てきた?
実装編で一番衝撃だったのは、Claude Code が書いてくれたコードの量と質です。私が書いた一文「LINE のメッセージを受け取るエンドポイントを作って、署名検証もして、D1 に保存して、Claude API で返信案を3パターン作って、スタッフ LINE に通知して」だけで、こんなコードが出てきました。
※読まなくてOKです。「Claude Code がこういうものを書いてくれた」という雰囲気だけ感じてください。
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get('x-line-signature') ?? '';
// 署名検証(本物のLINEから来たかチェック)
const isValid = await verifySignature(body, signature, env.LINE_CHANNEL_SECRET);
if (!isValid) return NextResponse.json({error: 'invalid'}, {status: 401});
// ctx.waitUntil で非同期処理(LINE の10秒タイムアウト回避)
for (const event of JSON.parse(body).events) {
ctx.waitUntil(processEvent(event, db, channelToken, anthropicKey));
}
return NextResponse.json({ok: true});
}
え、こんなプログラムを Claude が…?
そうなんです。私の役割は「これをコピペで route.ts ファイルに貼って」と言われた通りに動かすだけ。意味を理解する必要はなくて、Claude Code が書いて、私が動作確認するという分業でした🙂↕️
Claude Code が書いてくれた象徴的な部分を、もう1つだけお見せします。これは「LINE の10秒以内に返事しろ」というルールを回避するための工夫です。
// Claude API は5〜15秒かかるので、ここで待つと LINE がタイムアウト
// → ctx.waitUntil で「裏側で続行」して、LINE には先に200を返す
ctx.waitUntil(
generateReplyDraft(message, anthropicKey)
.then(draft => saveToDb(draft, db))
.then(() => notifyStaff(staffLineToken, message, draft))
.catch(err => console.error('processing failed:', err))
);
return NextResponse.json({ok: true}); // ← LINEに即座に200返す
これ、私には絶対に思いつきません。Claude Code に「LINE がタイムアウトする」と相談したら、5秒で「ctx.waitUntil()(コンテキスト・ウェイトアンティル)を使いましょう」と返ってきました。
マカロン店で例えると、「お客さまには『ご注文承りました』と先に返事して、実際の調理は裏で続ける」みたいな考え方。後から調べたら、これは Cloudflare Workers の典型的な解決パターンらしいです。
「典型的な解決パターン」を一瞬で出してくれるのは強い…!
このあたりが、Claude Code が「友達のエンジニア」として横にいる感覚です。
私は「困ったこと」を伝えるだけでした🙂↕️
▼ 【実装STEP 1〜5】アプリを動かす全工程

ここからが本番。5つの STEP でアプリの心臓部を作っていきます。各STEP に「目的」を1行入れているので、迷子にならずに進めます。すべて Claude Code への「お願い文」だけで実装できました。
STEP 1|Cloudflare D1でデータベースを作る(約30分)
🎯 目的:お客さまメッセージを保管する「受注ノート」を用意する
まずはメッセージを記録する場所を作ります。Cloudflare D1 という小さなデータベースで、マカロン店で例えるなら「お客さまから来た注文をすべて書き写す受注ノート」です。
Claude Code に送ったお願い文:
Cloudflare D1 でデータベースを作って、
以下の4つのテーブルを用意してください:
1. conversations(お客さまごとのやり取り)
2. messages(個別メッセージ)
3. drafts(Claude が生成した返信案)
4. staff_actions(スタッフが何をしたかの記録)
スキーマファイルも一緒に作って、wrangler でデプロイ
する手順を教えてください。
Claude Code が migrations/0001_initial_schema.sql ファイルを書いてくれて、私はターミナルで2行のコマンドを打つだけでした。
テーブルってなに…?
受注ノートを「お客さま一覧」「メッセージ一覧」「返信案一覧」「スタッフ操作履歴」の4ページに分けたイメージです。Claude Code が分けてくれました🙂↕️
👤 あなたがやること
下のお願い文をコピペで Claude Code に送る → ターミナルで指示されたコマンド(wrangler d1 create iroha-reply-db など)を順にコピペ実行
🤖 Claude Codeへのお願い文(コピペ用)
「Cloudflare D1 でデータベースを作って、4つのテーブル(会話・メッセージ・返信案・スタッフ操作ログ)を用意してください。スキーマファイルも書いて、wrangler でデプロイする手順を教えてください」
STEP 2|LINE Webhook 受信+署名検証(約60分)
🎯 目的:LINE から届くメッセージを「本物」とだけ受け取れるようにする
次は「LINE からの新着お知らせ(Webhook)を受け取る入口」を作ります。ここで一番大事なのが 署名検証。「本物の LINE から来たメッセージだけ」を受け取って、悪意ある第三者からの偽メッセージは弾く仕組みです。
マカロン店で例えるなら、「お客さまが本物のうちのお店に注文を入れているか」を、お店だけが知ってる暗号で確認するイメージ。Claude Code が HMAC-SHA256(エイチマック・エスエイチエー256)という暗号技術で、秘密のチャネルシークレットと送信内容を照合する仕組みを書いてくれました。これがあると「LINE の名前を装った偽メッセージ」を弾けるので、セキュリティの最低ラインです。
HMAC…?難しい言葉が出てきた…
これもClaude Code が書いてくれた部分なので、私は意味を理解しなくてOKです。「セキュリティ的に必須」と言われたので、そのまま採用しました🙂↕️
👤 あなたがやること
準備編で取得した LINE Channel Secret を wrangler secret put LINE_CHANNEL_SECRET でCloudflare Secret に登録 → 下のお願い文をコピペで Claude Code に送る → 生成されたコードをそのまま保存
🤖 Claude Codeへのお願い文(コピペ用)
「LINE Webhook を受け取るエンドポイントを書いてください。HMAC-SHA256 で署名検証して、本物の LINE からのメッセージだけ受け取るようにしてください。Cloudflare Workers で動くコードでお願いします」
STEP 3|Claude APIで返信案を3パターン自動生成(約60分)
🎯 目的:受信メッセージを「研修済みの新人さん(Claude)」に渡して下書きを作ってもらう
ここがアプリの中核。Claude API に「いろは口調」で返信案を3パターン作ってもらう仕組みです。連載第1弾で作った「お返事Skill」のシステムプロンプトをほぼそのまま使いました。
Claude Code が書いた処理の流れはこんな感じ:
- 受信したお客さまメッセージを Claude API に送る
- システムプロンプトに「いろは口調」「商品ラインナップ」「営業時間」などを埋め込む
- Claude が A 短め・B 標準・C 丁寧 の3パターンを生成
- 3パターンを D1 に保存
初めて動いた瞬間、Claude Code が『おはようございます☀』と書き出した瞬間、本当に新人スタッフが入ってきた感覚でした。私の口調・絵文字の使い方・締めのフレーズまで再現していて、これはすごい、と素直に思いました。
口調まで再現できるの…!?
はい。第1弾の Skill 作成で「文体ルール」を細かく書いていたので、その資産がそのまま API でも活きました。Skillを作っておいて本当に良かった🍰
👤 あなたがやること
準備編で取得した Claude API キーを Cloudflare Secret に登録 → 連載第1弾で作った SKILL.md の中身を Claude Code に共有(テキストでコピペ) → 下のお願い文を送る
🤖 Claude Codeへのお願い文(コピペ用)
「上で共有した SKILL.md をシステムプロンプトとして使って、Claude API(Opus)で返信案A短め・B標準・C丁寧の3パターンを JSON で受け取る関数を書いてください。時間帯であいさつを切り替える処理も含めて」
STEP 4|スタッフLINEに新着通知を送る(約30分)
🎯 目的:「下書きできたよ」を私たちのスマホ(LINE)に届ける
返信案ができたら、準備編で作った「店舗運営用LINE」に通知を送ります。これで私たち2人は、キッチンで仕込みをしていてもスマホがブブッと鳴って気付けるようになりました。
通知の中身は工夫して、返信案A・Bのプレビュー込みにしました。スマホでパッと見て「Aで送ろうかな」と判断できる粒度です。
スマホで返信案がプレビューで見られるの便利すぎる…!
外出先や仕込み中でも、スマホ1つで「これでOK」「ちょっと修正したい」を判断できます。管理画面を開くのは「修正したい」時だけ🙂↕️
👤 あなたがやること
準備編で作った 店舗運営用LINEのChannel Access Token を Cloudflare Secret に登録 → スタッフ全員の LINE User ID を Webhook ログから取得 → 下のお願い文を送る
🤖 Claude Codeへのお願い文(コピペ用)
「店舗運営用LINEに『📩新着メッセージ + 返信案A・Bプレビュー』を Push する関数を書いてください。複数のスタッフ User ID にループで送る形で」
STEP 5|管理画面(一覧・詳細・編集・送信)を作る(約120分)
🎯 目的:返信案を確認・編集して、お客さまに送るための「司令室」を作る
最後に作ったのが管理画面(Web UI)。ここが**「人間が必ずチェックして送信する」**場所で、アプリの安全装置の中核です。
機能はシンプルに4つだけ:
- 一覧画面
/admin:受信したメッセージを「未対応 / 返信済み / 完了」のタブで切り替え表示 - 詳細画面
/admin/conversations/[id]:会話履歴 + Claude が生成した返信案A・B・C を表示 - 編集機能:返信案を選んで微修正できるテキストエリア
- 送信ボタン:1タップで LINE Push API 経由でお客さまに返信
Claude Code に「Next.js の App Router で /admin と /admin/conversations/[id] を作って、Tailwind CSS で見やすくして」と頼んだら、2時間で動く管理画面が完成しました。


↑実際の管理画面の一覧です。新着が来ると「未対応」タブに表示され、対応済みになると「返信済み」に自動で移動します。
本当にちゃんとした管理画面ができてる…!
私もできたとき「これ自分で作ったの…?」と二度見しました(笑)デザインも全部 Claude Code が考えてくれて、私は色味だけ少し調整しました🍰
👤 あなたがやること
「こんな画面が欲しい」イメージを Claude Code に伝える(一覧画面・詳細画面・編集→送信の流れ) → 生成された複数のファイル(page.tsx等)をそのまま保存 → ブラウザで localhost:3000/admin にアクセスして動作確認
🤖 Claude Codeへのお願い文(コピペ用)
「Next.js App Router で /admin と /admin/conversations/[id] を作ってください。一覧画面(未対応/返信済み/完了タブ)・詳細画面(返信案A・B・C表示と編集・送信ボタン)。Tailwind CSS で、スマホでも崩れないレスポンシブにしてください」
⏱ 実装STEP 1〜5 の所要時間まとめ
| STEP | 目的 | 所要 |
|---|---|---|
| ① D1 データベース | 受注ノートを用意 | 30分 |
| ② Webhook + 署名検証 | 本物だけ受け取る | 60分 |
| ③ Claude API 連携 | 新人さんに下書き依頼 | 60分 |
| ④ スタッフLINE通知 | スマホに届ける | 30分 |
| ⑤ 管理画面UI | 司令室を作る | 120分 |
合計:実作業 約5時間(休憩・本業の業務時間は除く・準備編4時間と合わせて合計約12時間でアプリ完成)
▼ 非エンジニアの私が踏んだ「実装の落とし穴」5つ(正直に)

準備編で2つ、実装編で5つ、計7つの落とし穴を踏みました。ここからは実装編で踏んだ5つを正直に書きます。「Claude Code に頼んでも一発でうまくいかない」という現実を、隠さずお伝えします。
落とし穴③|デプロイ成功なのに 500 Internal Server Error 事件
本番にデプロイ完了したのに、アクセスすると 500 エラー。ターミナルログを見ると:
(error) ⨯ TypeError: Cannot read properties of undefined (reading 'default')
原因は export const runtime = 'edge' という1行を route.ts に書いていたこと。OpenNext for Cloudflare(オープンネクスト・Next.js を Cloudflare Workers で動かすための変換ツール)と競合してエラーが出ていたんです。
マカロン店で例えると、「いつもの調理器具(Cloudflare Workers)にレシピ(Next.js)を持ち込んで作る」のに、レシピの最初に『プロ用オーブン使ってね』と書いてあって、お店のオーブンと噛み合わなかったようなイメージ。
解決策はシンプルで、その1行を削除するだけ。Claude Code にエラーログを貼ったら、5秒で「OpenNext と edge 宣言は非互換なので外してください」と返ってきました。
その1行で500エラー…?こわい…
こういう「1行のミスで動かない」が、非エンジニアには本当に怖いんです。でもエラーログをそのまま Claude Code に貼れば、原因はほぼ即特定してくれます🙂↕️
落とし穴④|LINE Webhook タイムアウト事件(同じメッセージが4件保存される)
テストメッセージを1通送ったのに、データベースに同じメッセージが4件保存されている謎現象。ログを見ると:
POST /api/webhooks/line - Canceled
原因は LINE の「Webhook は10秒以内に応答しろ」というルール。アプリは Claude API を呼んでいて 5〜15秒かかる → タイムアウト → LINE が「失敗した」と判断して何度もリトライ → DB に同じメッセージが4件挿入される、という連鎖でした。
解決策は ctx.waitUntil() で「LINE には即座に200を返して、Claude生成は裏側で続行」する方式に変更。これも Claude Code が「Cloudflare Workers の典型的な解決パターン」として一発で書いてくれました。
こんなの、自分じゃ絶対見抜けない…!
エラーログとデータベースの状態を Claude Code に伝えただけで、原因と解決策をセットで提示してくれました。「外部API + Webhook タイムアウト」は AI 連携アプリ典型の落とし穴だそうです🙂↕️
落とし穴⑤|LINE アクセストークン漏洩→再発行事件(一番ヒヤッとした話)
これが一番ヒヤッとしました。店主の私が「メモのつもりで」LINE Channel Access Token を Claude Code のチャット欄に貼ってしまったんです。
気づいた Claude Code が即座に「トークンをチャットに貼らないでください」と制止してくれました。これだけで救われた気がしましたが、その後本番デプロイしても LINE 通知が届かない不具合が発生。
status=401
body={"message":"Authentication failed. Confirm that the access token..."}
原因の推測は、チャットに貼ったトークンがコピペで一部欠損していたか、その後の Cloudflare Secret 登録時に不完全だったか。どちらにしても「漏洩疑いがある時点でアウト」と判断して、迷わず再発行しました。
- LINE Developers でトークンを「再発行」クリック
- 古いトークンは即座に無効化される
- 新しいトークンを Cloudflare Secret に登録
- 動作確認 → 成功
「再発行」できるの知らなかった…!
これ知らなかったら、本気で泣いてました。今回学んだのは「トークンは絶対チャットに貼らない」「漏洩疑いがあれば即再発行」「セキュリティはコードで守るだけでなく運用で守る」の3つです🙇♀️
落とし穴⑥|Cloudflare Access が workers.dev URL を守ってくれない事件
これは差別化最強の落とし穴。セキュリティ要件で「管理画面は特定メアドだけがアクセス可」にしたくて、Cloudflare Access という公式機能を設定したのですが、/admin を開いても認証画面が出ず、誰でも開けてしまう状態だったんです。
原因を調べたら、Cloudflare Access は「Cloudflare で管理しているドメイン」にしか自動でエッジ介入できない仕様。私が使っていた iroha-reply-app.iroha-no-oto.workers.dev という URL は、Cloudflare 上で動いてはいるけれど「Access の介入対象外」という落とし穴でした。
⚠️ Cloudflare Access の選択肢3つ(私が検討したこと)
A. カスタムドメイン(reply.iroha-no-oto.com 等)を新たに設定する → DNS まわりが大変
B. Access の発行する JWT を Worker のコードで自分で検証する → 技術的に複雑
C. HTTP Basic 認証で代替(環境変数1つ・5分で実装)← これを選択
「Cloudflare Access」って公式機能でも落とし穴があるの…!?
あったんです、これが。日本語記事にもほぼ書かれていない情報で、結構詰まりました💦 解決策は次の落とし穴⑦をご覧ください🙂↕️
落とし穴⑦|Basic 認証で代替したら LINE 内蔵ブラウザでログインできない事件
落とし穴⑥の代替案として HTTP Basic 認証(環境変数で ID/PW を Cloudflare Secrets に登録、ミドルウェアで検証)を採用しました。これで /admin アクセス時にダイアログが出てログインできるはず…と思ったら:
iPhone Safari と PC ブラウザでは正常にダイアログが出るのですが、LINE のスタッフ通知から URL をタップしてLINE 内蔵ブラウザで開くと、Android では Basic認証ダイアログが表示されない仕様でした。
急遽、フォーム認証(HTML フォームで ID/PW 入力)に再ピボット。Claude Code に「Basic 認証から HTML フォーム認証に変えて」と頼んだら、30分で書き換えてくれました。
3回もピボット…!
そうなんです(笑)Cloudflare Access → Basic認証 → フォーム認証と、3回方針転換しました。でも毎回 Claude Code が代わりにコードを書き換えてくれるので、私の作業は「お願い文」を伝えるだけ🙂↕️
💡 落とし穴5つから学んだこと
- エラーログは「そのまま全文」Claude Code に貼るのが最速の解決法
- セキュリティはコードで守るだけでなく「運用」で守る(トークン即再発行など)
- 「公式機能」でも環境次第で落とし穴がある(Cloudflare Access × workers.dev)
- 完璧主義より「2人で運用するMVP」に合わせた選択を(フォーム認証で十分)
- 方針転換は恥ずかしくない(3回ピボットでも、Claude Code がコード書き換えしてくれる)
▼ 運用初日に発見した「3つのバグ」(落とし穴とは別カテゴリ)
実装が完成して、いざ運用開始。その初日にバグを3つ見つけました(前章の「落とし穴」は実装過程で踏むもの、ここで紹介する「バグ」は運用してみないと見えない問題として分けて整理しました)。アプリは「作って終わり」じゃない、という実例として正直にシェアします。
作ったあとも問題出るんだ…!?
はい。むしろ「運用してみないと気づかないバグ」こそ、本当の勝負どころでした🙂↕️
⚠️ 運用初日に見つかった3つのバグ
- バグ①|編集テキストが「消えている」ように見える事件
- バグ②|スマホ表示で文字レイアウトがはみ出る事件
- バグ③|挨拶しか送られてないのに「オーダーケーキ」と決めつける事件
バグ①|編集テキストが「消えている」ように見える事件
管理画面で返信案を選択すると、本来は編集テキストエリアに返信案がそのまま入っているはずなのに、画面を見るとテキストが薄く灰色で表示されて「消えてる」ように見える状態でした。

え、これ本当にちゃんと入ってるの…?って不安になる見た目
原因を Claude Code に相談したら、答えは 「placeholder(プレースホルダ)属性が値より優先されている表示バグ」。テキストエリアに値を入れたつもりが、実は「入力ヒント(プレースホルダ)」として表示されていただけで、データが反映されていなかったんです。
💡 解決策
テキストエリアの placeholder 属性ではなく、React の value プロパティに直接値を渡すように Claude Code が書き直してくれました。修正後はちゃんと黒い文字で表示されるように。
→ 「placeholder と value を混同する」のは非エンジニアが React 触ると最初にやるあるあるミスらしいです。
バグ②|スマホ表示で文字レイアウトがはみ出る事件
スマホで管理画面を開くと、3つの返信案カード(A短め・B標準・C丁寧)が縦長に1列でしか並ばない+長文(C丁寧 366字)が画面の枠からはみ出す問題が発生。

スマホでチェックする運用なのに、これは致命的…!
原因は Tailwind CSS のレスポンシブ指定が抜けていたこと。Claude Code が最初に書いてくれた管理画面は PC前提のレイアウトで、スマホ画面サイズに合わせる指定(sm: や md: プレフィックス)が部分的に抜けていました。
💡 解決策
Claude Code に 「スマホで管理画面を開いたら3つのカードが縦に積み上がって、長文がはみ出してる」とスクショ付きで送ったら、CSS を即書き直してくれました。修正後は3カード横並び・長文も折り返しで表示されるように。
→ 「スマホで運用するならスマホでテストする」が鉄則。PCで動いてもスマホで崩れるパターンは多い。
バグ③|挨拶しか送られてないのに「オーダーケーキ」と決めつける事件
もっと深刻だったのが、3つ目。上のバグ①②と同じスクショに既に映っているのですが、テスト用にお客さま役の自分から店アカウントへ 「こんにちは!」 とだけ送ったら、Claude Opus が生成した B標準(101字)はこうでした:
こんばんは🌙 メッセージありがとうございます
オーダーケーキについて何かご不明な点は
ございますでしょうか?
ご希望のサイズやデザイン
お渡し日など お決まりでしたら
お知らせいただけたらと思います✨
え、「こんにちは!」だけ送ったのに、いきなりオーダーケーキの話…?
そうなんです。お客さまは「こんにちは!」と挨拶しかしていないのに、AIが 「オーダーケーキについて何かご不明な点は…」 と勝手に決めつけて返事してしまった点が問題でした🤔
原因を Claude Code に相談したら、こう教えてくれました:
⚠️ なぜ「オーダーケーキ」と決めつけてしまったか
SKILL.md に 「種別Aオーダーケーキ予約 / 種別B通常マカロン予約 / 種別C営業日問い合わせ…」と書いていたため、AI は「メッセージが短すぎて種別不明な時は、最初に書いてあるオーダーケーキの可能性が高い」と判断したらしいです。
つまり「文脈不足のときの安全側フォールバック」が、オーダーケーキ案内になっていた、という構造的なバグでした。
💡 解決策|SKILL.md に1行追加するだけで解決
SKILL.md に 「メッセージが挨拶のみの場合は、用件を質問する短い返信に留める」ルールを1行追加。
# メッセージ種別の判定と対応
## H:挨拶のみ・用件不明(追加)
- 「こんにちは」「こんばんは」のみで終わる短い挨拶
- 商品名・予約・営業日・アレルギー・催事に関する単語なし
→ 用件を尋ねる短い返信のみ
例:「お気軽にお問い合わせください😉」
修正後、もう一度「こんにちは!」を送ると、A短め(45字)が 「お気軽にお問い合わせ下さいませ😉」 と返してくれるようになりました。
SKILL.md って後から育てていけるんだね
そう、ここが Skill の最大の強み。「失敗パターン」を1行追加するだけで AI の判断が改善します。第1弾の「実例」を増やす運用 がそのまま活きました🍰
💡 「初めてのバグ3つ」から学んだこと
- 運用してみないと気づかないバグがある(見た目バグ・スマホ崩れ・文脈バグの3種類)
- スマホで運用するならスマホでテストする(PCだけのチェックでは不十分)
- AI は文脈を100%読めるわけではない(SKILL.md の構造に引きずられる)
- SKILL.md と CSS は1行ずつ育てて改善する(運用で育つアプリ)
▼ お客さま視点で見る|実機の動き
ここまで実装の話でしたが、「お客さま側からはどう見えるのか」を実機で見せます。お客さまは「アプリを使っている感覚はゼロ」。普通に LINE で問い合わせて、普通に返事が届くだけです。
- お客さまが LINE で「こんにちは!」と送る
- 5〜10秒後、店舗運営用 LINE のスマホに「📩新着あり」通知が届く(返信案A・Bプレビュー込み)
- 夫婦のどちらかがスマホで通知を確認。すぐ返せそうなら管理画面を開く
- 管理画面で返信案A・B・Cから選んで微修正 → 「送信」ボタンタップ
- お客さまの LINE に普通の返信が届く(自動返信感はゼロ)
お客さま側から見たら全く違和感ないんだね
そう、これが大事なポイント。「AIに自動返信されている」と感じさせないようにしました。お客さまにとっては、いつもの「いろは」さんが少し早く返してくれるだけ🙂↕️
▼ 実際に作りたい方への3ステップ・ロードマップ
「記事を読んだあと、自分でも作りたい」と思った方へ。いきなり実装編を読んで詰まらないように、ステップを3段階に分けた進め方をおすすめします。
🗺 自分のお店でも作りたい方への3ステップ
STEP A|まず連載第1弾「お返事Skill」を作る(半日)
いきなり Cloudflare アプリは重い。まず 第1弾の「お返事Skill」 を半日で作って Claude Code に慣れるのがおすすめ。SKILL.md 1枚から始まります。
STEP B|Cloudflare アカウントで Hello World を動かす(15分・無料)
準備編に進む前に、「Cloudflare で何か簡単なものを動かす」体験をおすすめ。Cloudflare Workers 公式チュートリアル で15分でHello Worldが動きます。「自分でもできた」体感が、その先の自信になります。
STEP C|本記事+準備編を見ながら自分のアプリを作る(半日〜2日)
準備編で土台を整えたら、本記事の主要コード(記事末尾参照)をベースに自分の店向けにカスタマイズ。つまずいたら逃げ道3つ(1日寝かせる/飛ばして先へ/黄金パターン)を使ってください。
大事なこと:「最初から完璧」を目指さない。私も3回ピボット、運用初日に3つのバグを出しました。「動くものを少しずつ育てる」のが、非エンジニアが完走するコツです🍰
▼ 主要コード全公開(コピペで動かせる起点)
「記事を読んだだけじゃ作れない・コードが欲しい」という読者のために、主要なコードを抜粋公開します。GitHub リポジトリは Private のままですが、コアの構造はこれで全部把握できます。
各コードの目的と置き場所を1行で添えています。コピペして自分用にカスタマイズすればOK。エラーが出たら準備編の「黄金パターン」(エラー全文をClaude Code に送る)でほぼ解決します🍰
⚠️ 公開時の注意:トークン・パスワード・チャネルシークレットは絶対にコードに直書きしない。すべて env 経由で Cloudflare Secrets から取得します(.dev.vars.example 参照)。
① D1 データベース・スキーマ全文(migrations/0001_initial_schema.sql)
受注ノート(4テーブル:会話・メッセージ・返信案・スタッフ操作ログ)を作るSQL。wrangler d1 execute iroha-reply-db --remote --file=./migrations/0001_initial_schema.sql で本番反映。
-- iroha-reply-app 初期スキーマ(4テーブル)
-- conversations: お客さま1人ごとの会話まとまり
CREATE TABLE conversations (
id TEXT PRIMARY KEY,
platform TEXT NOT NULL CHECK (platform IN ('line', 'instagram')),
customer_external_id TEXT NOT NULL,
customer_display_name TEXT,
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'replied', 'archived')),
last_message_at INTEGER NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
UNIQUE(platform, customer_external_id)
);
-- messages: お客さまから来たメッセージ・送信した返信
CREATE TABLE messages (
id TEXT PRIMARY KEY,
conversation_id TEXT NOT NULL,
direction TEXT NOT NULL CHECK (direction IN ('inbound', 'outbound')),
message_type TEXT NOT NULL DEFAULT 'text',
content TEXT NOT NULL,
external_message_id TEXT,
sent_by_staff_email TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
);
-- drafts: Claude が生成した返信下書き3パターン
CREATE TABLE drafts (
id TEXT PRIMARY KEY,
message_id TEXT NOT NULL,
draft_a TEXT NOT NULL,
draft_b TEXT NOT NULL,
draft_c TEXT NOT NULL,
selected_draft TEXT CHECK (selected_draft IN ('a', 'b', 'c', 'custom')),
edited_content TEXT,
generated_at INTEGER NOT NULL DEFAULT (unixepoch()),
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
);
-- staff_actions: スタッフの操作ログ(誰がいつ何をしたか)
CREATE TABLE staff_actions (
id TEXT PRIMARY KEY,
staff_email TEXT NOT NULL,
action_type TEXT NOT NULL CHECK (action_type IN ('view', 'edit', 'send', 'archive')),
conversation_id TEXT,
message_id TEXT,
details TEXT,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
);
② Cloudflare Workers 設定(wrangler.jsonc)
Cloudflare Workers にデプロイするための設定ファイル。preview_urls: false が一番大事(Cloudflare Access の認証バイパス防止)。
{
"name": "iroha-reply-app",
"main": ".open-next/worker.js",
"compatibility_date": "2026-05-07",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
"binding": "ASSETS",
"directory": ".open-next/assets"
},
"services": [
{
"binding": "WORKER_SELF_REFERENCE",
"service": "iroha-reply-app"
}
],
"observability": { "enabled": true },
"upload_source_maps": true,
"preview_urls": false, // ← ここ重要:Access認証バイパス防止
"d1_databases": [
{
"binding": "iroha_reply_db",
"database_name": "iroha-reply-db",
"database_id": "your-d1-database-id" // ← Cloudflareから払い出される
}
]
}
③ 環境変数テンプレ(.dev.vars.example)
ローカル開発用の環境変数テンプレ。本番は wrangler secret put で Cloudflare Secrets に登録。このファイル自体は .gitignore で実値を含むやつ(.dev.vars)を除外。
# ローカル開発用の環境変数(.gitignoreで除外・本番はCloudflare Secrets使用)
LINE_CHANNEL_SECRET=your-line-channel-secret
LINE_CHANNEL_ACCESS_TOKEN=your-line-channel-access-token
LINE_STAFF_NOTIFY_USER_IDS=Uxxxxxxx,Uyyyyyyy
ANTHROPIC_API_KEY=sk-ant-xxx
ADMIN_USER=staff
ADMIN_PASS=your-secure-password
④ HMAC-SHA256 署名検証(src/lib/line.ts 抜粋)
LINE からの Webhook が本物かチェックするコード。Web Crypto API を使うので Node.js の crypto は不要(Cloudflare Workers でも動く)。
// LINE Webhook の署名検証(HMAC-SHA256・Web Crypto API使用)
export async function verifyLineSignature(
body: string,
signatureHeader: string | null,
channelSecret: string
): Promise<boolean> {
if (!signatureHeader) return false;
const encoder = new TextEncoder();
const keyData = encoder.encode(channelSecret);
const messageData = encoder.encode(body);
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signatureBuffer = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
const signatureBytes = new Uint8Array(signatureBuffer);
const expectedSignature = btoa(String.fromCharCode(...signatureBytes));
return timingSafeEqual(expectedSignature, signatureHeader);
}
// タイミング攻撃対策の文字列比較(同じ長さでも比較時間が一定になる)
function timingSafeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
⑤ LINE Webhook 受信エンドポイント(src/app/api/webhooks/line/route.ts 抜粋)
このアプリの**心臓部**。署名検証→DB保存→Claude API→スタッフLINE通知を一気に行います。ctx.waitUntil() で「LINE には即座に200を返して、Claude生成は裏側で続行」する仕組みが肝。
// LINE Webhook受信(POST /api/webhooks/line)
import { getCloudflareContext } from '@opennextjs/cloudflare';
import { NextResponse } from 'next/server';
import { generateDrafts, saveDrafts } from '@/lib/claude';
import { findOrCreateConversation, getRecentMessages, insertInboundMessage } from '@/lib/db';
import { getLineUserProfile, pushLineMessage, verifyLineSignature } from '@/lib/line';
export async function POST(request: Request) {
const { env, ctx } = getCloudflareContext();
const channelSecret = env.LINE_CHANNEL_SECRET;
const channelAccessToken = env.LINE_CHANNEL_ACCESS_TOKEN;
const anthropicKey = env.ANTHROPIC_API_KEY;
const staffNotifyUserIds = (env.LINE_STAFF_NOTIFY_USER_IDS ?? '')
.split(',').map((s) => s.trim()).filter(Boolean);
// ① 署名検証(本物のLINEから来たかチェック)
const rawBody = await request.text();
const signature = request.headers.get('x-line-signature');
const verified = await verifyLineSignature(rawBody, signature, channelSecret);
if (!verified) {
return NextResponse.json({ error: 'invalid signature' }, { status: 401 });
}
const body = JSON.parse(rawBody);
const db = env.iroha_reply_db;
// ② 各イベントを「裏側」で処理(LINEには即座に200を返す)
for (const event of body.events) {
ctx.waitUntil(
processEvent(event, db, channelAccessToken, anthropicKey, staffNotifyUserIds)
.catch((err) => console.error('processEvent failed:', err))
);
}
return NextResponse.json({ ok: true }); // ← ここで即座に200返す
}
// 個別イベント処理(DB保存 + Claude API + LINE通知)
async function processEvent(event, db, channelAccessToken, anthropicKey, staffNotifyUserIds) {
if (event.type !== 'message' || !event.source.userId) return;
// お客さま情報取得
const profile = await getLineUserProfile(event.source.userId, channelAccessToken);
const { id: conversationId } = await findOrCreateConversation(db, {
platform: 'line',
customerExternalId: event.source.userId,
customerDisplayName: profile?.displayName ?? null,
timestamp: Math.floor(event.timestamp / 1000),
});
// メッセージをDBに保存
const messageId = await insertInboundMessage(db, {
conversationId,
content: event.message.text,
messageType: 'text',
externalMessageId: event.message.id,
timestamp: Math.floor(event.timestamp / 1000),
});
// Claude API で返信案3パターン生成
const history = await getRecentMessages(db, conversationId, 8);
const drafts = await generateDrafts(anthropicKey, event.message.text, history.slice(0, -1), 'line');
await saveDrafts(db, messageId, drafts);
// スタッフLINEに通知(返信案A・Bプレビュー込み)
const customerName = profile?.displayName ?? 'お客さま';
const notifyText = `📩 ${customerName}さんから新着メッセージ\n\n${event.message.text.slice(0, 200)}\n\n💡 返信案A:\n${drafts.draft_a}\n\n💡 返信案B:\n${drafts.draft_b}`;
for (const staffId of staffNotifyUserIds) {
await pushLineMessage(staffId, notifyText, channelAccessToken);
}
}
⑥ Claude API 呼び出し(src/lib/claude.ts 抜粋)
これは「Claude API に送る指示書」のコード。SKILL.md(連載第1弾)のシステムプロンプトをほぼそのまま「Claude にこういう振る舞いをして」と渡して、Claude Opus に返信案3パターンを生成してもらう部分。JST時間帯判定でこんにちは/おはよう/こんばんは を切替。
// Claude API で返信案3パターンを生成(src/lib/claude.ts)
import Anthropic from '@anthropic-ai/sdk';
// SKILL.md由来のシステムプロンプト(実際は150行以上・ここでは構造のみ)
const SYSTEM_PROMPT = `あなたは棒付きマカロン専門店「いろはのおと」の店主「いろは」さんです。
お客さまから公式LINE/Instagram DMに届いたメッセージへの返信下書きを3パターン作成します。
## 出力ルール(厳守)
JSON形式のみで返答:
{"category":"A〜G","draft_a":"短め30〜80字","draft_b":"標準80〜200字","draft_c":"丁寧200〜400字","hint":"スタッフ向け補足"}
## いろは流の返信ルール
### 冒頭あいさつ(時間帯別・JST判定)
- 朝(5:00〜10:59):「おはようございます☀」
- 昼(11:00〜17:59):「こんにちは」
- 夜(18:00〜4:59):「こんばんは🌙」
### 文体
- ですます調・敬語ベース・親しみやすい
- 絵文字は2〜3個まで
- 命令形を避ける
## メッセージ種別の判定(A〜G)
- A:オーダーケーキ予約
- B:通常マカロン予約
- C:営業日問い合わせ
- D〜G:その他
## NG事項
- 確定価格を断言しない(必ず幅で)
- 在庫を断言しない
- 絵文字4個以上禁止
## 種別判定の最重要原則
- 必ず「今回のお客さまメッセージ」を見て種別を決める
- 過去履歴は口調の参考のみ
- 短い挨拶・お礼には用件を聞き返す軽い返信を作る
`;
// ↑ 実際のSKILL.mdは150行以上で、商品ラインナップ・価格構造・実例集を全部含む
export async function generateDrafts(apiKey, customerMessage, recentHistory, platform) {
const client = new Anthropic({ apiKey });
// JST 時間帯コンテキスト
const now = new Date();
const jstHour = (now.getUTCHours() + 9) % 24;
const timeContext = jstHour >= 5 && jstHour < 11 ? '朝' : jstHour >= 11 && jstHour < 18 ? '昼' : '夜';
const userPrompt = `## 媒体
${platform === 'line' ? 'LINE 公式アカウント' : 'Instagram DM'}
## 現在時刻(JST)
${timeContext}(${jstHour}時台)
## 直近のやり取り
${formatHistory(recentHistory)}
## 今回のお客さまメッセージ ← この内容だけで種別を判定
${customerMessage}`;
const response = await client.messages.create({
model: 'claude-opus-4-7',
max_tokens: 2048,
system: SYSTEM_PROMPT,
messages: [{ role: 'user', content: userPrompt }],
});
// JSON 抽出
const textBlock = response.content.find((b) => b.type === 'text');
const cleaned = textBlock.text.trim().replace(/^```(?:json)?/i, '').replace(/```$/i, '').trim();
return JSON.parse(cleaned); // { category, draft_a, draft_b, draft_c, hint }
}
⑦ 管理画面の送信API(src/app/api/admin/send/route.ts)
管理画面で「送信」ボタンを押した時の処理。① LINE 送信 → ② DB記録 → ③ 返信案選択保存 → ④ 会話ステータス更新 → ⑤ 監査ログの5ステップ。「誰がいつ何を送ったか」を全部記録するのがポイント。
// 管理画面で「送信」ボタンを押した時の処理(POST /api/admin/send)
import { getCloudflareContext } from '@opennextjs/cloudflare';
import { NextResponse } from 'next/server';
import {
getConversation, insertOutboundMessage, logStaffAction,
markConversationReplied, markDraftSelected,
} from '@/lib/db';
import { pushLineMessage } from '@/lib/line';
export async function POST(request: Request) {
const { env } = getCloudflareContext();
const channelAccessToken = env.LINE_CHANNEL_ACCESS_TOKEN;
const body = await request.json();
const { conversationId, draftId, selected, content } = body;
// 入力チェック
if (!conversationId || !draftId || !selected || !content?.trim()) {
return NextResponse.json({ error: 'missing required fields' }, { status: 400 });
}
const db = env.iroha_reply_db;
const conversation = await getConversation(db, conversationId);
if (!conversation) {
return NextResponse.json({ error: 'conversation not found' }, { status: 404 });
}
// ① LINE で実際に送信
const ok = await pushLineMessage(
conversation.customer_external_id,
content,
channelAccessToken
);
if (!ok) {
return NextResponse.json({ error: 'LINE push failed' }, { status: 502 });
}
// ② DBに送信記録を残す
const messageId = await insertOutboundMessage(db, {
conversationId, content,
sentByStaffEmail: request.headers.get('x-staff-user') ?? 'unknown',
});
// ③ どの返信案を選んだか記録
await markDraftSelected(db, draftId, selected, selected === 'custom' ? content : null);
// ④ 会話ステータスを「返信済み」に更新
await markConversationReplied(db, conversationId);
// ⑤ 監査ログに記録(誰がいつ送ったか)
await logStaffAction(db, {
staffEmail: request.headers.get('x-staff-user') ?? 'unknown',
actionType: 'send',
conversationId, messageId,
details: `selected=${selected} length=${content.length}`,
});
return NextResponse.json({ ok: true, messageId });
}
💡 コードを写経するとき覚えておきたい3つ
- ① 「自分のお店向け」にカスタマイズする箇所はシステムプロンプト(SKILL.md)の店名・商品・価格情報のみ
- ② セキュリティ最重要はWebhook署名検証(コード④)と Cloudflare Secrets(コード③)
- ③ 動かなくなったら Claude Code にエラーログを全文コピペで送る・スクショで送る・「分からない」と聞く
▼ まとめ|本番運用前のセキュリティチェック・FAQ

これで連載第2弾「Cloudflareアプリ完全自動化編」は完結です。準備編 + 実装編の合計約12時間で、個人事業主の小さなお店でも本格的な「LINE 自動返信下書きアプリ」が作れる時代になりました。
🛡 本番運用前のセキュリティチェック(重要)
本記事はマカロン店主の個人の運用体験であり、皆さまの環境で同じ動作・同じ安全性を保証するものではありません。お客さまの個人情報を扱うアプリですので、本番運用直前に以下3点はエンジニアの方にレビュー依頼を強く推奨します。
- Webhook 署名検証(HMAC-SHA256 が正しく実装されているか)
- データの保持期間設定(D1 にお客さまメッセージが残り続けないように、自動削除ルール)
- 認証設定の漏れ(管理画面が誤って公開状態にならないか)
→ 私もこれから、知り合いのエンジニアに最終レビューを依頼する予定です。
FAQ|よくある質問
Q1|本当に12時間で本格的なアプリが作れますか?
私の場合は2日間(実作業約12時間)で本番デプロイまで完了しました。ただしこの12時間は正味作業時間(昼食・店舗業務除く)で、Claude Code が7〜8割のコードを書いてくれた前提です。読者の方が同じことを試す場合は 半日〜2日程度(実時間6〜16時間)を目安にしてください。
Q2|InstagramのDMも対応できますか?
本記事で作ったのはLINE 公式アカウント版のみです。Instagram Graph API も同じアーキテクチャで対応可能ですが、Meta の審査があり個人事業主には少しハードルが高いです。私自身も Instagram 対応はまだ未着手で、運用安定後に検討する予定です。
Q3|運用開始後のメンテナンスは大変ですか?
運用開始から数日経った時点での体感は「思ったよりラク」です。Cloudflare Workers が勝手に動き続けてくれるので、私たちは管理画面を開いて返信案を確認するだけ。ただし「返信案の精度を上げるための SKILL.md 微調整」は定期的に必要です。
Q4|お客さま情報の取り扱いは大丈夫ですか?
本記事の構成では、お客さまメッセージはCloudflare D1(暗号化されたデータベース)に保存されます。アクセスできるのは Cloudflare アカウント所有者と認証通過したスタッフのみ。個人情報を本気で扱うなら、本番運用前にエンジニアレビュー必須です(前述)。
Q5|失敗したらどうすればいいですか?
準備編でお伝えした「困ったときの黄金パターン」3つを実践してください。
① エラーログをそのままコピペで Claude Code に送る
② 画面のスクショを撮って渡す
③「分からないので教えて」と素直に聞く。
9割の詰まりはこれで解決します🍰
🎯 今日から始める3つのアクション
- ① 連載第1弾「お返事Skill」をまだ作っていない方は、まずSkillから(半日)
- ② 準備編を読みながら、Cloudflare アカウントを作る(10分)
- ③ 本記事をブックマークして、週末にアプリ実装にチャレンジ(半日〜2日)
非エンジニアの個人事業主でもこんなアプリ作れる時代なんだね…!
はい、本当にそういう時代です。Claude Code Pro 月$22 + Claude API 月$1〜3(合計 月額約3,500円)で、これだけのアプリが作れる。Claude Code Pro 月$22 は元が取れるか という疑問は、もう完全に「YES」です🍰
📚 あわせて読みたい
※ 本記事にはアフィリエイトリンクを含みますが、紹介する判断軸・体感はすべて4年間の個人事業主としての本音です。広告収入とは無関係に、自分が納得した選択肢だけを書いています。
ご質問・ご相談はお気軽にどうぞ
お問い合わせはこちら





