ケーススタディ

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用に小さくしたコピーを保存します。
ClientbrowserTherapistadmin consoleNext.js 16site + /adminGo APIchi · OpenAPIGo workerasynqexternalAnthropicClaude Haiku 4.5Resendemail · English + BurmeseMeta GraphFacebook · InstagramLinkedInREST postsPostgreSQL 17EXCLUDE · sqlcRedis 7queues · limitsS3mediaservice key / sessionrevalidatesuggestionsemail · publishSQLqueueuploads
図 1. ブラウザはNext.jsとだけ話します。Next.jsがGo APIを呼びます。ワーカーがメールを送り、投稿を公開し、サイトに更新を伝えます。

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)がビデオを運びます。中継サーバーから、サーバーの内部には入れません。
  • ブラウザの中: 先にカメラとマイクを試せます。通話が切れたら自動でつなぎ直し、回線が弱い時はお知らせが出ます。

通話は一切録画しません。

Client browserrole: clientTherapist browserrole: practitionerGo signalling hubWebSocket · 5-min ticketGoogle STUNfind public addresscoturnTURN relay · HMAC credsoffer · answer · ICEoffer · answer · ICEmedia, peer to peerSTUNrelay when direct fails
図 2. つなぐための連絡はGo APIを通ります。ビデオと音声は、ブラウザどうしで直接送ります。ネットワークがそれを止める時は、中継サーバーを通ります。

07

一度書けば、どこにでも投稿

最初は、本格的なCMS(記事を管理するシステム)を作る予定でした。でも、本当に必要なものがわかったので、もっと小さな投稿ポータルを作りました。一度書いて、それをシェアするためのものです。

1つの投稿は、チャンネルごとに1つの版を持ちます。下書き → レビュー → 承認 → 予約投稿 → 公開、と進みます。承認できるのはサイトの管理者だけです。承認の時に、各チャンネルのルールをチェックします。たとえばInstagramは、画像が1〜10枚、文字は2,200字までです。

  • Webサイト: 直した記事がレビューを通るまで、訪問者には前に公開した版が見えます。
  • Facebook・Instagram・LinkedIn: それぞれ別の作業として、裏で送ります。投稿が出たかわからない時は「結果不明」にします。自動で送り直すことはありません。同じ投稿が2回出るのは、誰も望みません。
  • つながっていない時: 文章をコピーして自分で投稿し、「投稿済み」にできます。

AIの手伝い: Claude Haiku 4.5が、SNS用の文章や翻訳の下書きを作ります。彼女の話し方を守り、作り話をしないように指示しています。下書きが勝手に保存されることはありません。いつも彼女が直して、承認します。

Draftone per channelIn reviewApprovedsite_admin onlyPublishingnow or scheduledAI suggestionsClaude Haiku 4.5 · never savedWebsitesnapshot + revalidateFacebook Pagephotos → feedInstagramcontainer → publishLinkedInimage → postshe edits and approves
図 3. 1つの下書きが、チャンネルごとの4つの版になります。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台で十分です。ビデオの中継サーバーに必要なポートも、安く開けられませんでした。仕事が大きくなったら、また使えます。
GitHub ActionsCI → releaseAmazon ECRarm64 imagesEC2 t4g.small · Docker ComposeCaddyTLSNext.jswebsiteGo APIapi modeGo workerworker modePostgreSQL 17Redis 7noevictioncoturnUDP relayAWSS3backups · mediaCloudWatchalarmsAWS Budgetsseparate statepush via OIDCdeploypullnightlymetrics
図 4. EC2サーバー1台ですべてを動かします。GitHub Actionsがデプロイし、ヘルスチェックに失敗したら前の版に戻します。バックアップはS3へ送り、CloudWatchがサーバーを見守ります。

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

最終更新:

次のケーススタディAU-Van同じ席を二度売らない、LINEで使えるバン予約ケーススタディを読む

もっと知りたい方へ

コードのこと、なぜそう決めたか、次にやりたいことまで、よろこんでお話しします。気軽にメッセージをください。

お問い合わせ