個人開発SaaSのAI開発が完成するまで
ScrapostApp というアプリを作りました。
テキスト、画像を地名付きで投稿し、最終的にはダウンロード又はにGoogleフォト・ドキュメント連携して永続保存するというアプリです。
データは3日でアクセスできなくなり、10日で削除されます。
課金することで10日アクセスできるようになります。
自転車やバイクツーリングが趣味で出かけた先を地名付きで記録したいと、文書やフォト連携で残したいと思ったので自分用ツールとして作っていましたが
規模が大きくなって割とちゃんとしたアプリになったので一般公開することにしました。
作るにあたってのAI開発の手法だったり、アプリ公開までにやったこととかを書いていきます。
全体仕様をなんとなく決める
ID+パスワード認証が煩わしいので、クエリパラメータにUUIDを組み合わせた文字列を設定して、それだけで認証できるようにする
緯度経度はブラウザの位置情報機能をそのまま使う
位置情報は国土交通省の住所データを使う(地名変換サービスは高いので使わない)
地名は1kmメッシュとして検索高速化 & プライバシーへの配慮とする
画像は長辺1920pxに縮小して保存、API時間を減らす、S3コストを減らす
高画質保存モードを付けて、エクスポート時に高画質をダウンロードできるようにしておくことで高画質も保存したいユーザーに応える
データは10日で削除、個人情報を抱えたくないのでこれ以上の保持は考えない
認証できるのは作成から3日まで
課金した場合は認証が作成から10日まで延長される
AIに指示を出す準備
AWSに実装することを前提に指示を出していきます。
バックエンドのコードテンプレートを作成、DBデザインをYAMLで作成、あらかじめ基礎となるコードは自力で作成して準備します。
UIはデザイン済みのサンプルHTMLを準備しておきます。
機能責務分け
ゴールとアクションという分け方をする。
アカウント作成の手順が次のような手順であった場合を考える
- メールアドレス入力
- 検証トークンマジックリンクを受信(ユーザー操作)
- 検証トークンによる認証
- 初期設定画面~アカウント作成
次のようにゴールとアクションを決めていく
- G0010-アカウント作成
- A0010-検証トークン発行、メール送信
- A0020-マジックリンククリック、認証画面遷移
- A0030-検証トークンの確認、初期設定、アカウント作成
4桁目の数字はアクションを挿入したい時のための予備番号として利用する
伴う実装は次のようになる
- A0010: メールアドレス入力HTML、検証トークン発行・メール送信のLambda
- A0020: ユーザー操作の仕様書のみ存在、実装は無し
- A0030: 遷移後のHTML各種、検証トークン確認処理、アカウント作成・完了メールLambda
バックエンド指示
Lambda API の指示例
main.py に機能を全て集約、flask等のフレームワークに依存しない純粋変数での呼び出しで機能するように作ること
次のような呼び出しを想定している。
lambda_function.py -> handler.py -> main.py
local_httpd.py -> handler.py -> main.py
console.py -> main.py
基本構成と実装方式は ./src/sample/ 以下を参照
aws samコマンドで src/* をデプロイする前提で構成すること
テーブル構成は contracts/designs/*.yaml を参照
過度なオブジェクト指向を禁止
仮想関数の使用を禁止
再利用しない機能を関数化しない、ベタ書きで構わない
指示が無い限りmain.pyのみで完結すること
sam コマンドの出力は _build/{dev|prod} にすること
sam コマンドは Docker を使用し環境依存しないこと、Windowsホストdevcontainer, macホストdevcontainer での動作を想定
./src/sample/ には動作確認済みの自力実装の lambda_function.py, handler.py, main.py が実装済みです。
UI指示
UIについての指示例
start.html, start.js として HTML、JS をペアのファイル構成とし、CSSはHTMLに埋め込む
オブジェクト指向の禁止、再利用しない機能を関数化しない、ベタ書きで構わない
動的要素を極力作らないようにする
次のような隠し属性であらかじめフォーム作成しておく
管理者権限UIは <div id="adminPostTemplate" style="display: none;"> ~管理権限投稿フォーム~ </div>
投稿権限UIは <div id="postPostTemplate" style="display: none;"> ~投稿者限投稿フォーム~ </div>
閲覧権限UIは <div id="readPostTemplate" style="display: none;"> ~閲覧者限投稿フォーム~ </div>
利用時は clone して実体化させる、基本的にはこの仕組みで要素を変更すること。
デザインについては post_sample.html を参照
文中の post_sample.html は chatGPT+自力で作ったデザインHTMLです。
オブジェクト指向、仮想関数の禁止
コードを自力で追いかける際のコストを減らすため、次のようなルールを毎回指示します。
- ファイル分けを基本的に禁止(機能が多い場合は機能別ファイル分けくらいはするが、それは開発者の任意の指示で行う)
- オブジェクト指向禁止
- 仮想関数禁止
オブジェクト指向するとネストが増え1機能追うだけでテキストエディタがいくつも開いたりして煩わしいです
仮想関数は検索阻害にしかならないのでベタ書きにして長い関数名を作らせて検索フィルタしやすいようにします。
このようにして追跡コストを減らすために一般的な開発とは異なるポリシーで作らせています。
共通部のサブディレクトリ化
ある程度まで機能を作り込むと、共通機能、外出ししたい機能が出てきます
それらをリポジトリに分けて、サブディレクトリとして取り込むようにします。
gitリポジトリ一覧
- scrapost
- アプリ本体
- scrapost_contracts
- 契約情報(DB設計等)
- scrapost_integrations
- 外部サービス連携
- scrapost_g001_a002
- ゴールG001、アクションA002 公開機能を分離する
- 汎用的な機能ではないが他の機能で全く同じ事をする状況があったため分離した
- scrapost_stats_queue
- 統計情報キューの管理
例えば
usecases/uca001_ucb001 というアカウント作成機能は
usecases/uca001_ucb001/src/aws_lambda/libs/contracts
usecases/uca001_ucb001/src/aws_lambda/libs/integrations
の2フォルダをgitサブディレクトリとして利用します。
指示書の書き方
次のようなYAMLファイルで処理の流れを正順で記します
分岐や例外は言葉で書くのみとします。
Googleフォト連携の一部抜粋、実際はこの3倍以上のテキスト量があります。
- category: "UI"
implements: google_doc_start.html
process: |
画面中央表示
`Google ドキュメント 連携を行います`
`scrapost認証中...`
`ドキュメントタイトル: {1行INPUT グレーダウン、利用不可能状態}`
thread_idをキーにlocalStorageからaccess_tokenを取得する
required: |
query.thread_id
output: |
localStorage.{thread_id}.access_token
/api/google_document/auth で認証実行
event: |
取得失敗時は google_doc_error.html へジャンプ
- category: "API"
implements: /api/google_document/auth
process: |
access_token で認証、該当Accountsレコード取得
required: |
access_token
output: |
認証可否, Accounts.title
event: |
取得失敗時は google_doc_error.html へジャンプ
- category: "UI"
implements: google_doc_start.html
process: |
画面中央表示
`Google ドキュメント 連携を行います`
`scrapost認証中...`
`ドキュメントタイトル: {Accounts.title} 編集可能状態`
`<button>Google認証開始</button>`
Google認証開始操作を受けて /api/google_document/initialize を呼び出す
required: |
thread_id, access_token
output: |
POST /api/google_document/initialize (access_token, title, client_instance_id)
event: |
初期化失敗時は google_doc_error.html へジャンプ
implements に html, pyファイルと指定して、1ファイルで複数処理があるなら継続して同じファイル名を記載したアイテムを追加し処理を記載
implements のファイル名を見てAIがバックエンド実装かフロントエンド実装かを判断、遷移タイミング、必要な引数等も判断してくれます
コード等価の指示書を書くよりも低コストに指示が出せていると思いますが、もっと合理的な指示の出し方があるんじゃないかとも思っています
検証
AIがpytestまで全部やってしまうのでコードレベル検証はほぼしていません。
Playwrightを使った総合試験のみ実施しています。
Googleフォト連携、Googleドキュメント連携だけは手作業で試験しています。
ただし、実装は一通り自分の目で確認しています。
DynamoDB で scan する実装されたことあったので、コードは必ず確認しています。
AI実装してみての感想
基本的にはちゃんと作ってくれます、とても頼もしいです、自分より優秀ですね。
全体コードが大きくなるとトークンコストが跳ね上がるので、機能を分けて開発するほうが多分トークンコストが安いです。
という事に気がついた時には分離する準備ができておらず、結局高コストのまま作り上げましたが、次なんかやる時は最初から小分けにするプロジェクト設計にしようかなと思いました。
オリジナリティのあるロジックではAIがうまく対応できないこともあります。その場合は人間が補完する必要があります。
実際あったのは次の例で、投稿一覧を docx に変換するロジックで、
次のようなテキストセットは ScrapostApp においては改ページしないで欲しいのですが、
2026/09/04 00:21:30 宮城県仙台市泉区泉中央 移動なし 55分
ほげほげ
2026/09/04 00:21:30 宮城県仙台市泉区泉中央 移動なし 55分 ほげほげ
のような文章は改ページしないようにdocxを作成してください
と指示を出しても上手くいかず、結局自分でデータ構造を提案し
「投稿グループ」という概念を作成し
高さ計算、改ページ判断は「投稿グループ」単位で行ってください
と指示を出し、更にロジックを仮想コードで提案し、やっとちゃんと動くものが出てきました。
アプリ作成で面倒だったとこ
メール送信
BREVOの無料枠でメールを送信しています。
AWS SES は敷居が高く審査通すことができませんでした、なにが悪いかなど一切教えてもらえず「説明不足なので許可は出せません、要件を満たしていません」とだけ回答されるだけなので AWS SES はあきらめました。
BREVOで1日300通のメール送信できるので、開発中のテストや少人数向けの運用には十分でしたのでBREVOで良いと思います。
公式サポートメールの準備
さくらのWEBメールで support@scrapost.app アドレスを作成し運用することにしました。
最初はメールアドレス作る気なかったんですが、ドメインが一致していないと迷惑メールにひっかかりやすくなるとか、なんか色々あったので取得したドメインでメール送受信できるような環境を作りました。
DNS設定、契約
AWS Route 53 を利用して DNS 設定を行いました。
サイトにドメイン名アクセスさせるだけなら簡単なんですが、BREVOの設定を取り込んだり、さくらのWEBメールの設定を行ったりする必要があるため、少し面倒でした。
chatGPTに聞きながら1日かけて設定していました、なんとか全て設定できて正常に動いています。
動いていますが、DNSはよくわかりませんでした。
Stripe課金設定
クレジットカード等の決済を Stripe を使って実装しています。
色々面倒でした(小並感)。
大変だったけどなにも覚えてないです。
総務省への通信事業者届出
ScrapostApp はURL共有により複数人でのクローズドチャット機能として使う事が可能です。
そのため、総務省への通信事業者届出が必要となります。
地域毎に総務省の窓口があるので、私の場合は東北通信総合局に電話して色々聞きました。
https://www.soumu.go.jp/soutsu/tohoku/application.html
この工程でハマったのは、chatGPT、Gemini に届け出はどうすれば良いか訪ねたところ、
chatGPT、Gemini共に「特定大規模特定電気通信役務提供者」あたりを元に話をしてくるので困りました。
監査ログや通報機能の完備など、長文で厳格な要件を次々と挙げてくるため、そんなに大変なの??と。
他の小規模事業者を例に出したり色々と質問を重ね、やっと「通信事業者届出」という簡単な手続きだけで良いという結果にたどり着きました。
AIはその後も様々な厳格な要件を挙げてきましたが
実際に総務省窓口に電話して聞いたところ相当緩いことが分かり、無事届け出の手続きは完了しました。
おそらく、大規模事業者や犯罪に使われるなどの目立った事が無い限りは最低限の本人確認さえできれば良いのでしょうね。
5chとかyahoo掲示板みたいなパブリックなサービスは届け出不要みたいです。
注意点は、DM機能という提供でなくてもDM機能として使えるならそれは「クローズドチャット」と判断される点です、個人開発でSaaS公開する時には注意しましょう。
Googleフォト権限の許可もらう
ScrapostApp は Googleフォト連携機能があります。
Google Cloud Platform の OAuth 2.0 スコープ photoslibrary.appendonly が必要です。
この利用許可をもらうために、Googleに対して どういうアプリで、どういう使い方をするのか、操作動画、デモンストレーションページを提示する必要があり、そこが結構面倒でした。
それらを知らなかったため、申請してからデモンストレーション用機能を作り込んだりしたので結構工数もかかりましたし、動画撮影なんかもしたので想像以上に手間でした。
ちなみに Google Drive への書き込み(docxの変換)は申請しただけで許可が出ました、機密性が低いという判断なんでしょうかね?
古いiPhone使ってる人がGPS機能使えなかった
iPhone12, 13 を使用してる人に試験してもらった際に、GPSが使えないとの報告があり原因調査してたんですが、
結果としては 「iPhone 12,13 は GPS 許可を出すメニューが深いとこにあった」 という結果でした
最近のiPhoneだったり、Android はブラウザから許可を出すとOS許可までたどり着くためGPS許可が出しやすいのですが、古いiPhoneだと、ちゃんとOS設定からGPS許可を出す必要がありました。
AI代金
copilot pro+ 39$(5900円/月)
chatGPT plus 20$(3200円/月)
copilot 追加料金 6万円くらい
期間2ヶ月なので、合計で
copilot pro+ 39$(5900円/月) × 2 = 78$(11800円)
chatGPT plus 20$(3200円/月) × 2 = 40$(6400円)
copilot 追加料金 6万円くらい
合計で約8万円くらいかかりました。
その他の料金
ドメイン代 dev、prodのドメインで合わせて年間3000円
桜のWEBメールが年間2000円くらい
合わせて1年で5000円くらいなのでこちらは大したことないですね。
以上です
AIがコーディングしてくれるから楽なはずなんですが、逆に疲れる感じがします。
思考疲れというものなんでしょうか?
思考に対して休む暇を与えてくれない感じですね。
時間あたりの密度が濃いというか
手でプログラミングしてる時間って実は休憩時間だったのではと思いました。