人とAIのタスク管理サービス「tone」の技術スタックとAI活用を紹介
toneについて
最近、「tone(トーン)」というタスク管理サービスを開発しました。toneは人とAIのためのシンプルで軽量なタスク管理サービスです。GitHub issueやJIRAでは、エンジニアにフォーカスされすぎて職種横断で使いづらかったり、機能が多すぎてハードルを感じたりします。toneは職種横断で使えるようなわかりやすいUIや機能をメインとしつつ、AIユーザーやMCPサーバの提供といった、人とAIの協働を目指して開発を続けています。
無料で使えるのでぜひ登録してみてください。
今回はtoneの技術スタックや工夫点を紹介します。
バックエンド
インフラ構成
インフラ構成はざっくり以下のような感じです。
- Cloudflare
- ドメイン管理とWAF(セキュリティルール)を使用しています
- Cloud Run
- WebAPIやイベント駆動のバッチ処理、Webhook受信などサービスに関わるすべての処理をCloud Runで実行しています
- WebAPIはGo + Connect を使用しています
- ネットワーク的にはインターネットに公開せず、「Internal and Cloud Load Balancing」としています
- mainブランチに変更があったらCloud Buildでビルドし、自動デプロイされます
- Cloud SQL
- MySQL
- レイテンシを少しでも減らすために採用しました
- Stripe
- Stripe Checkoutによる決済機能を使用しています
- StripeからのWebhookはCloud Runで受信します
- Firebase
- 認証のためにAuthのみ使用しています
- Resend
- 招待メールなどメールの送信に使用しています
- Supabase Realtime
- タスク一覧で誰がどのタスクを閲覧中か、どのタスクが更新されたかなどをリアルタイムに表示するために使用しています
Go
Goは元々好きではありましたが、静的型付け言語であること、高速なこと、文法がシンプルで誰が書いても一定以上のリーダブルコード(人もAIも)になること、エコシステムが整っていることなどが採用理由です。
コード設計は以下のような感じです。後述の自動テストではローカル上のMySQLと通信するミディアムテストがほとんどなのでDIなどはせず、かなり素朴に記述しています。
(usecase, entityなど本来の意味や使い方とは違うのでご注意ください。)
- controller
- エンドポイントの定義
- リクエストから必要な情報を取得し、usecaseを呼び出す
- usecaseから得た情報をprotoに変換する
- presentationモジュール
- エラーハンドリング
- ユーザーのためのエラーハンドリング
- ログやアラートのためのエラーハンドリング
- usecase
- controllerから呼び出され、必要な処理をすべて実行し、controllerに結果を返す
- ビジネスロジックやDBへのクエリ、外部APIの呼び出しをそのまま記述します
- 自動テストはローカルのMySQLと実際に通信するミディアムテストがほとんどです。DIする必要がほぼないのでrepository層などはあえて作っていません(必要になったらAIに作ってもらいます)
- 定義する関数はすべてトップレベル関数としており、usecase層から別のusecase層の関数を呼び出すこともOK
- このほうが厳密に作るより、共通化やリファクタリングも簡単で、AIにも理解しやすい気がしているのでこのまま突き進んでいます
- entity
- GORMのテーブル定義、ValueObjectなどプロジェクト全体で横断的に使う重要なオブジェクトを定義しています
- その他横断的関心ごとを記述する細かなサブモジュール
- notification
- observability
- ...
GORM, GORM gen
ORMとしてGORMを採用していますが、GORMではあまり型の恩恵を受けることができません。そのため拡張ライブラリであるGORM genを採用しています。これによってテーブル定義から型付きのクエリが自動生成され、型の恩恵を受けながらクエリを書くことができます。人もAIも理解しやすく、間違えにくくなります。
認可
認可のフレームワークとしてCasbinを採用しました。
認可はビジネスロジックも複雑になりがちで、DBへの問い合わせも多くなります。toneでは、「権限、ロール、ユーザーグループ」といったRBACも提供している(例えば、請求関連の権限だけを持つユーザーを作るなどが可能)関係で、コードベースで頑張るのはすぐに破綻しそうだったので採用しました。学習・導入コストは高めですが、これによってRBACの処理に一貫性が生まれるのと、高速化に寄与しています。
他にもSubject(誰が)、Resource(どのリソースに対して)、Action(何ができるか)の形式で表現できる認可はなるべくCasbinで実装しています。例えば、プライベートリストも「ユーザID(Subject)」「Resource(リストID)」「Action(閲覧、編集、削除など)」の形式に落とし込めるのでCasbinで実装しています。
Connect
ConnectはgRPCの課題を解決して、使いやすくしてくれる軽量でシンプルなライブラリです。Webフロントエンド(ブラウザ)との通信にEnvoyなどのプロキシーサーバーを用意する必要がなく、protobufでAPIスキーマを共有しつつ、型安全な開発ができます。
またConnectで構築したAPIはgRPC・HTTPの両方で呼び出すことができます。toneでは、機能追加などで変更の多いWebフロントエンドからは型安全にgRPCとして、MCPサーバーからは技術的な制約もあるので素朴にHTTPで呼び出すといったふうに使い分けています。
様々な言語のライブラリが用意されています。さらにprotobufに対してバリデーションを書くためのプラグインなど用意されています。
toneでは、大まかにフロントエンドとバックエンドの2名 + 複数のAIで開発しています。スキーマ駆動開発をすることでコミュニケーションコストが大幅に削減されたと感じています。
Cloud SQL
料金もそこそこかかるので悩みましたが、数十ミリ秒のレイテンシでも1回のAPI呼び出しで何度もクエリすることがほとんどなので合計すると結構な時間になってしまいます。フロントエンドでの楽観的キャッシュも頑張っていますが、キャッシュの効かない部分ではDBのレスポンス速度が体感に直結します。軽量でサクサク動くタスク管理はブレたくなかったので採用することにしました。
自動テスト
ごく一部の明確なロジックのみスモールテストを実装し、残りはdevcontainer上に起動したMySQLと疎通するミディアムテストを実装しています。
AIパワーもあるので、あまり深く考えずにエンドポイントごとにレスポンスをassertしたり、内部のDBの変化をassertしたり、更新系であれば、更新後に別のエンドポイントのレスポンスが正しく変更されているかをassertするようなテストをたくさん書いています。自動テストの目的はほとんどデグレ対策と捉えているので今の段階ではこのようなテストで十分だと考えています。
ローカル上で動くのでテスト実行速度は速いです。
フロントエンド
フロントエンドの技術スタックは以下のようになっています。
- Next.js
- TanStackQuery
- APIレスポンスのキャッシュ
- サクサク動くサービスにするためフロントエンド側で楽観的更新を頑張っている
- TanStackTable
- タスク一覧の実装に使っている
- Supabse Realtime
- タスク一覧で誰がどのタスクを閲覧、編集中かをリアルタイムに表示するために採用
- ReactFlow
- オートメーション機能のGUIを実装するために採用
- Mantine, TailwindCSS
- UIの実装はMantineを主軸としつつ、TailwindCSSで細かい部分の微調整を行う
- Firebase Auth + NextAuth.js
- ログイン、認証の実装で使用
- Playwright
- E2Eテスト
その他
開発環境など
- チーム
- フロントエンドメインとバックエンドメインの2名
- Claude Code、Cursorなど複数名
- エディタ
- Cursor
- 開発環境
- 余計なことに気を使いたくないのでdevcontainerで環境を共通化しています
- よく使うコマンド群はGoのタスクランナーであるTaskでわかりやすくしています
- AIの恩恵を受けやすくするため、フロントエンド、バックエンド、LP、ブログなどモノレポにしています
AI
APIの開発は、Claude CodeでSonnet 4でも8,9割程度はやってくれる感覚です。自分がポイントかもと思うことを列挙します。
- Goの文法が非常にシンプルで、AIの書いたコードがちょっと変だなとか、自分たちのコーディングスタイルとはちょっと違うなと思うことがほぼない
- TypeScriptやSwiftなど型も強力で自由度も高いですが、書き方や好みが多様にあってどうしてもバラけてしまう
- コンテキストファイルやlintで制御を頑張りますが、本質的な時間ではない
- TypeScriptやSwiftなど型も強力で自由度も高いですが、書き方や好みが多様にあってどうしてもバラけてしまう
- 認可にCasbinを採用しているので重要な部分の書き方のパターンがほぼ決まっている
- usecase層は素朴になんでも書いているので、本質的な処理の内容に集中しやすい(のではと思っている)
- DIやモジュール分割のことを考えなくていい
- usecase層はトップレベル関数の集合体にしているので、共有化やリファクタリング、部分的なDIもしやすい
- 自動テストは実際のDBを利用したミディアムテストで、モックなどもほぼ不要となっているため、テストが書きやすい
- 「実装 -> 自動テスト -> 修正」のサイクルもAIが回せるようにコマンドやコンテキストファイルを整備している
- コードにコメントを残す
- とにかく簡単にコーディングする
- 上から読めばわかる
- = コードジャンプを多用しなくても処理が理解できる
- 上から読めばわかる
- とにかく自動テストを頑張る
- フィードバックが早いことが重要なのでスモールテスト、ミディアムテストを増やし、ラージテストは極力減らす
その他
- ClaudeによるコードレビューをCIに組み込む
- Playwight MCP + ClaudeでPRから差分を読み取り、探索的テストを行う
- SQL MCPでDBの内容を分析し、「昨日の登録者」「活発なワークスペース」「どのような使われ方をしているか」などをSlackで報告するスラッシュコマンドを実装
Discussion