ケーススタディ
VetMiMi
シドニーのアートセラピストのために、予約と非公開のビデオ通話、かんたんな投稿ができるサイトを作りました。
シドニーで一人で活動するアートセラピスト、Daw Miさんのためのクライアント案件です。個人ブログだったサイトを、英語とミャンマー語のサイトに作り直しました。クライアントは予約と非公開のビデオ通話ができ、Daw Miさんは1つの文章をサイト・Facebook・Instagram・LinkedInに投稿できます。
- 担当
- リード開発者(API、サイト、管理画面、サーバー)
- 期間
- 2026年8月〜10月 · 企画からローンチまで
- ステータス
- AWSで稼働中
- 種別
- クライアント案件
- APIの機能。すべて1つの共通ファイルで定義
- 89
- Goのテスト。本物のPostgreSQLで実行
- 454
- 1つの下書きから投稿できる場所(Web・Facebook・Instagram・LinkedIn)
- 4
- 言語:英語とミャンマー語
- 2
01
依頼の内容
Daw Miさんの前のサイトは、個人ブログのように見えました。彼女が求めていたのは、英語とミャンマー語で使える仕事用のサイトです。事務の手間も減らしたいと考えていました。
- メールのやり取りなしで、セッションを予約できること。
- 一般向けのビデオアプリを使わずに、非公開のオンラインセッションができること。
- 同じ文章を4回書き直さずに、サイトとSNSで文章を発信できること。
彼女は一人で仕事をしていて、予約は月に数十件です。だから、運用コストはほぼゼロにする必要がありました。また、これはセラピーです。予約フォームでは、健康のことは聞きません。緊急時の対応をするとも思わせません。予約のリンクも非公開にします。
コードを書く前に、提案書を書き、似た仕事の事例を9つ調べました。ページや予約のルールも、彼女と一緒に考えました。そのあと、デザインの試作品を、仲間と一緒にNext.jsのアプリにしました。
02
全体の仕組み
コードの置き場所(リポジトリ)は2つで、ルールは1つです。ブラウザが話す相手は、Next.jsのサイトだけです。
- Next.js(Webサイトを作るためのフレームワーク)が、公開サイトと管理画面を動かします。1つのアプリにまとめたので、ログインもデザインも1つで済みます。
- Go APIが中心の仕事をします。1つのプログラムが2つのモードで動きます。1つはリクエストに答えて、ビデオ通話を動かします。もう1つは裏で作業をします。サイトのホストはどちらもできないので、いつも動いているサーバーが必要でした。Goは軽く、小さくて安いサーバーに収まります。
- PostgreSQLがメインのデータベースです。Redisは作業の待ち行列(キュー)を持ちます。Redisのデータが消えても、作業はPostgreSQLから作り直せます。
- Resendがメールを送ります。S3(AWSのファイル置き場)は画像と、Web用に小さくしたコピーを保存します。
03
1つの共通ルール
APIの89の機能は、すべてopenapi.yamlという1つのファイルに書いています。Goのサーバーのコードも、サイトのTypeScriptの型も、このファイルから作られます。つまり、サイトとAPIは1つの約束ごとを共有しています。
もし両方が合わなくなったら、ビルドが失敗します。考え方はシンプルです。項目の名前が変わったら、壊れるべきなのは私のビルドです。サイトに来た人の予約ではありません。
セキュリティも同じファイルで決めます。どの機能にも「誰が使えるか」を書きます。書いていない機能があると、サーバーは起動しません。
04
二重予約が起きない予約
空いている時間は、Go APIが計算します。もとにするのは、週ごとの受付時間、休みの日、すでにある予約です。時間はいつもシドニー時間です。サマータイムには専用のテストがあります。時計が進んだり戻ったりする日も、正しく扱えます。
最後の守りはデータベースです。有効な予約どうしが重なると、PostgreSQL自体がそれを受け付けません。たくさんのリクエストが同時に来ても同じです。その時間がうまっていたら、ほかの空き時間を最大5つ見せます。
ほかにも、安全のための工夫があります。
- 送信ボタンを2回押しても、 同じ答えが返ります。予約が2つできることはありません。
- 見えないスパム対策でボットを見つけます。3秒以内に送られたフォームは受け付けません。
- 予約用の非公開リンクは、元に戻せない形(ハッシュ)でだけ保存します。データベースがもれても、使えるリンクはありません。
05
予約をこわさないメール
予約の途中でメールを送ると、メールサービスが遅いだけで予約がこわれるかもしれません。そこで、予約の変更と一緒に、メールもデータベースに保存します。そのあと、裏で動くワーカーがResendでメールを送ります。失敗したら最大5回までやり直し、クラッシュで抜けたメールも拾います。同じメールを2回送ることはありません。
リマインダーは、送る直前に予約をもう一度確認します。だから、日時が変わった予約に古いリマインダーは届きません。メールが失敗しても、予約は取り消されません。
メールは12種類あり、それぞれ2つの言語があります。どれもテストで確認しています。
06
非公開の1対1ビデオ通話
一般向けのビデオアプリにはプライバシーの約束がなく、有料のビデオサービスは分単位でお金がかかります。クライアント1人とセラピスト1人なら、WebRTCがぴったりです。2つのブラウザが直接ビデオを送り合い、間には誰も入りません。
- 通話に入る時: Go APIが、2つのブラウザがつながる手助けをします。入るには、5分だけ使える通行証が必要です。部屋はセッションの15分前に開き、1時間後に閉じます。サーバーは中継したメッセージを保存しません。
- つながりにくいネットワーク: スマホや会社のネットワークには、直接の通話を止めるものがあります。その時は、中継サーバー(TURN)がビデオを運びます。中継サーバーから、サーバーの内部には入れません。
- ブラウザの中: 先にカメラとマイクを試せます。通話が切れたら自動でつなぎ直し、回線が弱い時はお知らせが出ます。
通話は一切録画しません。
07
一度書けば、どこにでも投稿
最初は、本格的なCMS(記事を管理するシステム)を作る予定でした。でも、本当に必要なものがわかったので、もっと小さな投稿ポータルを作りました。一度書いて、それをシェアするためのものです。
1つの投稿は、チャンネルごとに1つの版を持ちます。下書き → レビュー → 承認 → 予約投稿 → 公開、と進みます。承認できるのはサイトの管理者だけです。承認の時に、各チャンネルのルールをチェックします。たとえばInstagramは、画像が1〜10枚、文字は2,200字までです。
- Webサイト: 直した記事がレビューを通るまで、訪問者には前に公開した版が見えます。
- Facebook・Instagram・LinkedIn: それぞれ別の作業として、裏で送ります。投稿が出たかわからない時は「結果不明」にします。自動で送り直すことはありません。同じ投稿が2回出るのは、誰も望みません。
- つながっていない時: 文章をコピーして自分で投稿し、「投稿済み」にできます。
AIの手伝い: Claude Haiku 4.5が、SNS用の文章や翻訳の下書きを作ります。彼女の話し方を守り、作り話をしないように指示しています。下書きが勝手に保存されることはありません。いつも彼女が直して、承認します。
08
セキュリティとプライバシー
- 管理者のログイン: パスワードは安全な形(argon2id)で保存します。管理者のログインは、認証アプリのコードにも対応しています。15分間に10回失敗すると、アカウントがロックされます。
- SNSの鍵(トークン) は、暗号化して保存します。
- ログ には、名前、メールアドレス、メモ、非公開リンクを残しません。
- データは少なく: あえて保存しないものを、データ設計に書いています。毎日の作業で、古い記録を消します。
09
テスト
- 454件のGoテストは、本物のPostgreSQLで動きます。 偽物ではありません。テストごとに、新しいデータベースを用意します。
- 競合テストで、2つのことが同時に起きた時を確かめます。たとえば、2人が同じ時間を予約しても、取れるのは1人だけです。
- 安全チェックが本当に効くかを確かめます。 チェックを消してテストが失敗するのを見てから、元に戻しました。
- Webサイトには179件のユニットテストがあります。9件のPlaywrightテストで、アクセシビリティ、キーボード操作、レイアウトを確かめます。
10
AWSで動かす
すべてが、AWSの小さなEC2サーバー1台で動いています。設定には、サーバーをコードで書けるTerraformを使いました。API、ワーカー、サイト、データベース、Redis、ビデオの中継サーバーが同じサーバーにあり、それぞれにメモリの上限があります。
- 自動デプロイ: GitHubでテストが通ると、GitHub Actionsが新しい版をサーバーに載せます。そのあと、ヘルスチェック用のページを確認します。新しい版が動かなければ、自動で前の版に戻ります。
- バックアップ: 毎晩、データベースを非公開のS3バケットにバックアップし、30日間残します。サーバーはバックアップを追加できますが、消すことはできません。
- アラーム: CloudWatchがサーバーを見ています。予算のアラームは別にしてあるので、サーバーを消しても残ります。
- 大きな構成: ECS FargateとRDS(アプリとデータベースをAWSが動かしてくれるサービス)で、もっと大きな構成も作りました。でも、そのあと止めました。月に数十件の予約なら、サーバー1台で十分です。ビデオの中継サーバーに必要なポートも、安く開けられませんでした。仕事が大きくなったら、また使えます。
11
今の状況とこれから
VetMiMiはAWSで稼働中です。4つの段階(土台、予約と問い合わせ、ビデオ、投稿ポータル)はすべて終わりました。
まだ残っていることは、Daw Miさんによるメールの確認、ネイティブの人によるミャンマー語の確認、法律関係のページ、全体を通したテスト、バックアップから元に戻す練習です。
いくつかのトレードオフは、わざと選んで書き残しています。サーバーが1台なので、そこが止まると全部止まります。ビデオは1対1だけです。SNSとの接続は、60〜90日ごとにつなぎ直す必要があります。どれも一人で運営するセラピーには合っていて、仕事が大きくなった時の次の一手も決めてあります。
技術スタック
- Go
- chi
- sqlc
- OpenAPI 3.1
- Next.js 16
- TypeScript
- PostgreSQL
- Redis
- asynq
- WebRTC
- coturn
- Resend
- Claude API
- Docker
- Terraform
- AWS EC2
- Amazon ECR
- CloudWatch
- GitHub Actions (OIDC)
- Playwright
最終更新: