cofounder-tech-stack について
Go API と React SPA プロジェクトのアーキテクチャや開発方法を定めるスキルです。単一バイナリでのデプロイ、公開ページのビルド時プリレンダリング、文書管理、シークレットの扱いを説明します。
主な機能
- 01Go API と React SPA の構成
- 02単一バイナリでのデプロイ指針
- 03公開ルートのビルド時プリレンダリング
- 04プロジェクト文書の規約
- 05ローカルでのシークレット管理
活用シーン
- 01Go と React によるアプリの計画
- 02公開ページのプリレンダリング実装
- 03アーキテクチャ文書の管理
- 04開発用シークレットの設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ locaweb/ cofounder --skill cofounder-tech-stackインストール後、Claude Code で/cofounder-tech-stackを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/google-auth.mdnotify-patterns.mdpg-cron.mdpg-jsonschema.mdpgmq.mdpgroonga.mdpgvector.mdsmtp-gateway.md詳細
技術スタック
単一のバイナリから配信し、1つのコンテナとしてデプロイする、Go の JSON API と React SPA です。検索エンジンや AI エージェントがページを読み取れるよう、公開ページはビルド時に静的 HTML として事前レンダリングされます。
アーキテクチャ
バックエンド: Go 標準ライブラリの net/http ルーター、Postgres 用の pgx/v5、クエリ生成用の sqlc、ロギング用の slog、go:embed による SQL マイグレーションの埋め込み。
フロントエンド: React + TypeScript、shadcn/ui コンポーネント、Tailwind CSS を使用し、React Router のフレームワークモードで ssr: false を設定しています。実行時には純粋な SPA(本番環境では Node サーバーなし)として動作し、Vite を基盤となる開発サーバーおよびバンドラーとして使用します。検索エンジンや AI エージェント向けに、公開ルートは prerender 設定に記載し、実際の HTML として配信します(公開ページ:SEO と AI エージェント向けの事前レンダリングを参照)。フロントエンドコードの作成や変更には、必ず frontend-design スキルを使用してください。レイアウト、スタイル、コンポーネントの美しさ、UI の仕上げに関するデザイン方針が提供されます。
プロジェクト構成
.
├── backend/
│ ├── cmd/server/main.go # Entrypoint
│ ├── internal/
│ │ ├── config/ # Env var parsing
│ │ ├── database/
│ │ │ ├── migrations/*.sql # Embedded, forward-only
│ │ │ ├── queries/*.sql # sqlc source
│ │ │ └── sqlc/ # Generated — do not edit
│ │ └── handler/ # HTTP handlers (JSON API)
│ ├── go.mod
│ ├── go.sum
│ └── sqlc.yaml
├── e2e/ # Visual checks (Playwright screenshots)
│ └── package.json
├── frontend/ # React SPA (React Router framework mode)
│ ├── app/
│ │ ├── routes.ts # Route declarations
│ │ ├── root.tsx # Root layout (head, favicon, global providers)
│ │ ├── routes/ # Route modules
│ │ └── components/ui/ # shadcn/ui (generated, editable)
│ ├── react-router.config.ts # ssr: false (+ prerender list of public routes)
│ └── build/ # Build output (gitignored); static assets in build/client/
└── Dockerfile
プロジェクトの .gitignore に、少なくとも次の内容が含まれていることを確認してください。
backend/server # add the actual output path used by `go build`
frontend/build/
frontend/.react-router/ # generated route types
frontend/node_modules/
e2e/node_modules/
.venv/
.env
backend/cmd/server/server は、go build で生成されるローカルコンパイル済みの Go バイナリです。コミットしてはいけません。
.env には、ローカルでの実行に必要なすべてのサービス接続文字列とアプリケーションシークレットを記載します。絶対にコミットしてはいけません。作成または更新した後は、アクセス権を制限してください。
chmod 0600 .env
記載例:
DATABASE_URL="postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
BASE_URL="http://localhost:5173"
REDIS_URL="redis://localhost:6379"
N8N_WEBHOOK_URL="http://localhost:5678/webhook"
値は必ず二重引用符で囲んでください。一部の値(SMTP アプリパスワードなど)にはスペースが含まれることがあります。
Go バックエンドは os.Getenv() を使用してこれらの値を読み込みます。ローカル開発では、サーバーの起動前に . .env でファイルを読み込んでください(POSIX のドット構文です。すべてのシェルで使えるとは限らない bash の組み込みコマンド source は使用しないでください)。
set -a && . .env && set +a
これにより、ローカル環境とデプロイ環境の差を埋められます。ローカルでは .env が値を提供し、デプロイ環境では Kamal の設定が値を提供します。Go コードは変わりません(os.Getenv("REDIS_URL"))。
ローカルの .env ファイルへのシークレットの入力
チャットでシークレットの値を尋ねてはいけません。
エージェントが生成するシークレット(JWT キー、セッションシークレット、HMAC キーなど)は、生成して直接書き込んでください。
mise x -- python -c "import secrets; print(secrets.token_urlsafe(32))"
ユーザーが提供するシークレット(OAuth 認証情報、SMTP パスワード、API キーなど):
-
ユーザーのシークレットには
REPLACE_WITH_プレースホルダーを使って.envを作成します。例:DATABASE_URL="postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable" JWT_SECRET="<already generated by agent>" GOOGLE_CLIENT_ID="REPLACE_WITH_GOOGLE_CLIENT_ID" GOOGLE_CLIENT_SECRET="REPLACE_WITH_GOOGLE_CLIENT_SECRET" SMTP_PASSWORD="REPLACE_WITH_SMTP_APP_PASSWORD" -
絶対パスを指定して、ユーザーのエディターで
.envを開きます。代わりにチャットへ値を貼り付ける方法を提案してはいけません。ユーザーのプラットフォームに適したコマンドを選んでください。Linux での WSL の検出方法:{ grep -qi microsoft /proc/version 2>/dev/null \ || grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null; } \ && echo WSLプラットフォーム コマンド(エージェントが直接実行) macOS open -a TextEdit /Users/name/project/.envWindows(ネイティブ、git bash) start notepad "C:\Users\name\project\.env"Windows 上の WSL notepad.exe /home/name/project/.env(WSL がパスを自動的にマッピングします)Linux(ネイティブ) ユーザーに、別のターミナルで nano <ABSOLUTE_PATH>/.envを実行するよう依頼します。 -
各認証情報の取得方法を案内します(該当する場合は参照ドキュメント
references/google-auth.md、references/smtp-gateway.mdを使用してください)。値をチャットに貼り付けないよう、明確に伝えてください。保存して確認するよう依頼します。 -
検証します:
.envを読み込み、REPLACE_WITH_のプレフィックスが残っていないことを確認します。実際のシークレット値を出力してはいけません。
mise.toml は Git の対象外にしてはいけません。すべての開発者が同じツールバージョンを使用できるよう、リポジトリにコミットします。
プロジェクトドキュメント
プロジェクトの docs/ ディレクトリで、次の成果物を維持してください。
docs/PRD.md — 製品要件定義書
Web アプリが何を提供するかを記述します。どのように動作するかではありません。技術的な実装とは独立している必要があります。
セクション: 概要、対象ユーザー、主な機能、ユーザーフロー、非機能要件、対象範囲外。
要件を収集する際は、以下を行います。
- 要件が曖昧、不完全、あいまい、または矛盾している場合、ユーザーが空欄を埋めるのを支援します。
- Web アプリのコンテキストに照らして適切で、ユーザーがまだ思いついていない可能性のあるアイデアを提案します。
- 要件の変化に応じて、PRD を常に最新の状態に保ちます。
docs/TASKS.md — 開発タスクトラッカー
PRD から生成され、開発フェーズを反映します。タスクでは、利用可能なスキルの技術的コンテキストを考慮する必要があります。
タスクごとの形式: タスク名 | ステータス(Pending / In Progress / Done / Blocked)| 理由/メモ
作業の進行に合わせて最新の状態に保ちます。
docs/adr/NNN-topic.md — アーキテクチャ決定記録
技術的な決定ごとに、番号付きの Markdown ファイルを作成します。
テンプレート:
# NNN - Title
**Status:** Accepted | Rejected | Superseded by [ADR-NNN]
## Context
[What situation or requirement prompted this decision]
## Decision
[What was decided]
## Rationale
[Why this choice was made]
## Trade-offs
**Pros:**
- ...
**Cons:**
- ...
## Alternatives Considered
- [Alternative 1]: [Why discarded]
- [Alternative 2]: [Why discarded]
選択した理由、検討した内容、採用しなかった内容に焦点を当てます。実装の詳細を掘り下げる必要はありません。コード自体がドキュメントです。
docs/INFRASTRUCTURE.md — サービス一覧
Go バイナリ以外にアプリが依存するすべてのサービスを一覧にします。最初の付属サービスを導入するときに作成します。外部サービスのないアプリでは空のままでもかまいません。
形式:
| 名前 | イメージ | ローカルポート | 環境変数 | 種類 |
|---|---|---|---|---|
| db | supabase/postgres:17.6.1.111 | 5432 | DATABASE_URL | backend |
| redis | redis:7-alpine | 6379 | REDIS_URL | backend |
| n8n | n8nio/n8n:latest | 5678 | — | standalone |
種類: backend = 環境変数を介して Web/ワーカーから利用されます。standalone = ユーザーがブラウザーから直接アクセスします(Go との連携はありません)。
主な決定事項
単一バイナリでのサービング
Go サーバーがすべてを処理します。/api/ 配下の API ルート、フロントエンドの静的ファイル、SPA ルーティングを扱います。開発環境では、Vite の開発サーバーが API 呼び出しを Go バックエンドにプロキシします。
ローカル開発では、Vite の開発サーバーがユーザー向けのエントリーポイントです。ブラウザーのトラフィックはすべてここを経由し、プロキシが /api/* と /auth/* を Go に透過的に転送します。そのため、サードパーティーのコンソール(例:Google Cloud)で設定する OAuth リダイレクト URI など、外部向けの URL には Go バックエンドのポートではなく、Vite のポートを使用する必要があります。.env の BASE_URL には、Vite 開発サーバーのオリジン(例:http://localhost:5173)を設定してください。デプロイ環境では、デプロイ設定を使って BASE_URL にアプリの公開 URL(例:https://myapp.example.com)を設定します。app-deploy スキルを参照してください。いずれの場合も、Go バックエンドは環境変数から BASE_URL を読み取るため、Host ヘッダーから推測する必要はありません。
SPA のキャッチオールは、API 以外のすべてのパスに対して SPA シェルを返してはいけません。そうすると、/assets/ 配下にある .js、.css などのハッシュ付きアセットにも text/html のコンテンツタイプで HTML が返され、ブラウザーが MIME タイプエラーで拒否します。その結果、開発環境では(Vite の開発サーバーがアセットを直接処理するため)動作していても、本番環境ではアプリが完全に壊れているように見えます。必ず以下のパターンを使用してください。これは、実際の失敗から学んだ次の 3 つのルールを反映しています。実ファイルは http.FileServer 経由で配信し(正しい Content-Type になります)、プリレンダリング済みのページ(about/index.html)は直接配信します(ディレクトリを FileServer に渡すと、サイトマップ内のすべての URL が末尾のスラッシュ付きに 301 リダイレクトされるためです)。また、ディレクトリを FileServer に渡してはいけません。渡すとアプリ内部の一覧が閲覧可能になります。http.Dir は指定したディレクトリ内へのアクセスに制限するため、パストラバーサルに対して安全です。
SPA シェルのファイル名は、プリレンダリング設定によって異なります(公開ページ:SEO と AI エージェント向けのプリレンダリングを参照)。prerender リストがないビルドでは、シェルは index.html として出力されます。/ をプリレンダリングするビルド(公開ページがあるアプリすべて)では、プリレンダリングされたホームページが index.html として出力され、シェルは __spa-fallback.html として出力されます。以下のパターンでは起動時に一度だけこれを判定します。また、/ は常に index.html から配信しなければなりません(/ がプリレンダリングされている場合、そのファイルはシェルではなく、ホームページそのものです)。
frontendDistは文字列リテラル"frontend/dist"でなければなりません。"../frontend/dist"や"/frontend/dist"のような絶対パスは使わず、複数のパスを試すフォールバック処理も追加しないでください。本番環境でのバイナリの作業ディレクトリはbackend/ではなく、Dockerfile のWORKDIRです。ローカルのリポジトリ構成を見ると../frontend/distが正しく見えます(Go はbackend/から実行されるため)が、Docker コンテナ内では誤った親ディレクトリに抜けてしまい、404 の原因になります。DEV_MODEが設定されている場合、Vite がフロントエンドを配信し、静的ファイルハンドラーは登録されないため、この誤りはローカル開発では表面化しません。エラーが判明するのはデプロイ後だけです。以下の Dockerfile のセクションに、これに対応するディレクトリ構成を示します。
frontendDist := "frontend/dist"
if _, err := os.Stat(frontendDist); err == nil && !cfg.DevMode {
// SPA shell: __spa-fallback.html when the build prerenders "/", index.html otherwise
spaShell := "index.html"
if _, err := os.Stat(filepath.Join(frontendDist, "__spa-fallback.html")); err == nil {
spaShell = "__spa-fallback.html"
}
fs := http.FileServer(http.Dir(frontendDist))
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/api/") || strings.HasPrefix(r.URL.Path, "/auth/") {
http.NotFound(w, r)
return
}
// "/" always maps to index.html (the prerendered home page in prerendered builds)
if r.URL.Path == "/" {
fs.ServeHTTP(w, r)
return
}
target := filepath.Join(frontendDist, filepath.Clean(r.URL.Path))
if info, err := os.Stat(target); err == nil {
if !info.IsDir() {
fs.ServeHTTP(w, r) // real asset; FileServer sets Content-Type
return
}
// Prerendered page ("/about" → about/index.html). Serve the file directly:
// delegating a directory to FileServer would 301 to "/about/", adding a
// redirect hop to every URL listed in sitemap.xml.
page := filepath.Join(target, "index.html")
if _, err := os.Stat(page); err == nil {
http.ServeFile(w, r, page)
return
}
// Directory without index.html (e.g. /assets/): fall through to the shell —
// FileServer would render a browsable listing of the app's internals.
}
// Unmatched route → SPA shell
http.ServeFile(w, r, filepath.Join(frontendDist, spaShell))
})
} else if !cfg.DevMode {
slog.Warn("frontend dist not found — SPA routes will return 404", "path", frontendDist)
}
SEO および AI エージェント向けの公開ページのプリレンダリング
クライアント側でレンダリングする SPA は、ほとんどのブラウザー以外の閲覧者から実質的に見えません。Googlebot は JavaScript を実行しますが、遅延のある第 2 波のインデックス登録となり、クロールバジェットにもペナルティがあります。Bing の動作には一貫性がありません。また、AI クローラー(GPTBot、ClaudeBot、PerplexityBot、CCBot)やリンクのプレビュー生成ツール(WhatsApp、Slack、LinkedIn)は生の HTML を取得し、JavaScript は一切実行しないため、空の <div id="root"> しか表示されません。これは公開ページに限った話です。認証済みアプリには、サーバーサイドレンダリングを導入してもメリットはありません。
すべてのアプリはすでにフレームワークモードでひな形が作成されています(フロントエンドのひな形作成とクリーンアップを参照)。そのため、設定を 1 か所変更するだけです。prerender に公開ルート(ランディングページ、料金、概要、ブログ、ドキュメント、公開リスティング)をすべて列挙してください。
// frontend/react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
ssr: false, // pure SPA at runtime — no Node server, ever
prerender: ["/", "/about", "/pricing"], // every public route
} satisfies Config;
ログインが必須のアプリ(社内ツール、ダッシュボード)は prerender を省略するだけでよく、それ以外に変更はありません。
ビルド時に React Router は、列挙された各ルートをクライアントビルド内の静的 HTML にレンダリングします。index.html(プリレンダリングされたホームページ。リストには必ず / を含めてください)、about/index.html などに加え、クライアント側のナビゲーションで使用する小さな .data ファイルが各ルートに生成されます。/ がプリレンダリングされるため、SPA シェルは __spa-fallback.html として出力されます。上記の Go キャッチオールがこれを自動的に検出します。ブラウザーでは、プリレンダリングされた HTML が通常の SPA としてハイドレーションされるため、クライアント側のナビゲーションやアプリの使用感は変わりません。ボットとエージェントには完全な HTML が提供されます。
SEO のために SSR フレームワーク(Next.js、Remix)や Node サーバーランタイムを導入しないでください。 プリレンダリングはビルド時にのみ行われるため、単一の Go バイナリーによるデプロイが維持されます。
ssr: false の制約: サーバーの loader/action 関数を使用できるのは、プリレンダリングされたルートのみです(ブログ用のローカル Markdown の読み込みなど、ビルド時に実行されます)。それ以外のルートでは、通常どおり clientLoader/clientAction、または Go API への通常の fetch を使用してください。
ルートごとのメタデータ
すべての公開ルートモジュールで、実際のタイトル、説明、Open Graph タグを含む meta をエクスポートする必要があります。これにより検索結果のスニペットやリンクプレビューが機能します(通常の SPA ではまったく提供できないものです)。
export function meta() {
return [
{ title: "Acme — Effortless invoicing" },
{ name: "description", content: "Send invoices in seconds, get paid faster." },
{ property: "og:title", content: "Acme — Effortless invoicing" },
{ property: "og:description", content: "Send invoices in seconds, get paid faster." },
{ property: "og:image", content: "https://acme.example.com/og.png" },
];
}
動的な公開コンテンツ
ビルド時のプリレンダリングで対象になるのは、ビルド時に判明しているパスのみです。prerender は非同期関数も受け取れるため、リポジトリに保存されているコンテンツ(たとえば Markdown ファイルのブログ記事)はビルド時に列挙できます。実行時に作成される公開ページ(公開リスティング、プロフィール、共有ドキュメント)には、コストに応じて次の方法を選択してください。
- 軽量(デフォルト): コンテンツはクライアント側でレンダリングします。ただし、対象ルートの Go ハンドラーで、配信前に SPA シェルへ適切な
<title>、メタディスクリプション、OG タグ、JSON-LD を挿入してください。レンダリング基盤を導入せずに、インデックス登録とリンクプレビューに必要な情報を提供できます。 - 完全対応: Go の
html/templateを使って、それらのルートをサーバー側でレンダリングし、ビルド済みの Tailwind CSS を再利用します(テンプレートファイルを Tailwind のソースグロブに追加し、そのクラスが含まれるようにしてください)。公開コンテンツページがプロダクトそのものであるアプリ(マーケットプレイス、ディレクトリ、求人掲示板)に限って、この方法を使用してください。
ボット対策チェックリスト
公開ページがある各アプリでは、すべてのURLをBASE_URLから導出し、Goハンドラーで robots.txt、sitemap.xml、llms.txtを配信します。静的ファイルとして記述してはいけません。デプロイされるまで公開オリジンは存在しないため、静的ファイルには必ず架空のドメインが含まれ、本番環境でクローラーによる検出が気付かないうちに妨げられます。合成オリジン(.testドメイン)を2つ使って導出処理をテストします。オリジンが1つだけでは、URLをハードコードしていても偶然テストを通過してしまいます。
robots.txt— AI クローラー(GPTBot、ClaudeBot、PerplexityBot、CCBot)を拒否してはいけません。末尾はSitemap: <BASE_URL>/sitemap.xmlにします。sitemap.xml— すべての公開URLを記載します。llms.txt— LLM エージェント向けに、公開コンテンツをプレーンな Markdown 形式でまとめます。- JSON-LD 構造化データ(これは Go ではなく、ルートの HTML に記述します) — schema.org の型(Product、Article、FAQPage、Organization)に対応するページに追加します。
まずは Postgres
PostgreSQL は主要な外部サービスです。可能な限り Postgres を利用する代替手段を使います。
- キュー:
pgmq— 可視性タイムアウト、アーカイブ、一括操作に対応した軽量メッセージキューです。references/pgmq.mdを参照してください。 - Pub/Sub:
LISTEN/NOTIFY— 拡張機能は不要です。永続性を確保するには、永続化レイヤーと組み合わせます。references/notify-patterns.mdを参照してください。 - キャッシュ: 非ログテーブル
- スケジューリング:
pg_cron+pg_net— データベース内の cron と非同期 HTTP リクエストを組み合わせ、スケジュールに従ってアプリのエンドポイントを呼び出します。コンテナレベルの cron より優先します。 references/pg-cron.mdを参照してください。 - 検索:
pgroonga— CJK を含むすべての言語に対応した全文検索で、ブール検索、ランキング、ハイライト表示、JSONB 検索を利用できます。設定は不要です。アプリに検索可能なコンテンツ(商品、記事、リスティング、メッセージ、ログ)が含まれる場合は、検索機能の追加を積極的に提案します。 単一の十分にサポートされている言語で、よりシンプルな組み込みソリューションで足りる場合に限り、ネイティブのtsvector/tsqueryに切り替えます。references/pgroonga.mdを参照してください。 - ベクトル:
pgvector— HNSW および IVFFlat インデックスによる埋め込みの保存と類似検索に対応します。references/pgvector.mdを参照してください。 - JSON 検証:
pg_jsonschema— CHECK 制約を使って、json/jsonb列を JSON Schema に照らして検証します。references/pg-jsonschema.mdを参照してください。 - 地理空間:
postgis— ジオメトリ型、空間インデックス、地理関数に対応します。https://postgis.net/を参照してください。 - SQL からの HTTP:
pg_net— SQL から非同期の HTTP/HTTPS リクエストを実行します。スケジュールされた呼び出しにはpg_cronと組み合わせるか、Webhook にはトリガーから使用します。 - その他の主な拡張機能:
pgjwt、pg_stat_statements、pgaudit、pg_hashids
supabase/postgresに同梱されている60以上の拡張機能をすべて利用できます。
アプリケーションで必要な機能を Postgres とその拡張機能で提供できない場合(n8n のような既成のツール、Kafka のような特化型システム、または専用サービスが明らかに適しているユースケース)は、付属サービスとして追加します。ローカルでの実行方法はLocal Services、記録方法はdocs/INFRASTRUCTURE.mdを参照してください。デプロイ時には、各付属サービスに専用の VM が割り当てられます(app-deployスキルを参照)。
すべてのクエリに sqlc を使用
すべての SQL はbackend/internal/database/queries/*.sqlに記述します。変更後はcd "$(git rev-parse --show-toplevel)/backend" && mise x -- sqlc generateを実行してください。Go のハンドラーコードに生の SQL 文字列を記述してはいけません。
生成される Go 構造体に小文字の JSON タグ(例:IDをそのままエクスポートする代わりにjson:"id")を含めるため、sqlc.yamlには必ずemit_json_tags: trueを設定してください。これがないと、API はフロントエンドの想定と一致しない PascalCase のフィールド名を返します。
起動時のマイグレーション
埋め込み SQL ファイルは、サーバーがトラフィックを受け付ける前に順番に適用されます。マイグレーションは前進のみで、連番を付けます(001_create_users.sql、002_add_tasks.sql、…)。可能な限り、各マイグレーションは冪等にしてください(CREATE TABLE IF NOT EXISTS、CREATE INDEX IF NOT EXISTS)。
go:embed ディレクティブで指定できるのは、それを宣言するファイルと同じディレクトリ、またはそのサブディレクトリにあるファイルだけです。.. を含むパスはコンパイラーに拒否されます。埋め込みディレクティブは cmd/server/main.go ではなく、migrations/ ディレクトリと同じ場所にある Go ファイル(例:backend/internal/database/migrate.go)に配置してください。
データベース接続の再試行
Go バックエンドは起動時にデータベースへの接続を再試行する必要があります(最大 6 回、指数バックオフ:1 秒、2 秒、4 秒、8 秒、16 秒、32 秒、合計約 63 秒)。これにより、並行起動に対応できます。Preview はすべてのサーバーを同時に起動するため、データベースの準備が整う前にバックエンドが起動する場合があります。また、本番環境へのデプロイでも推奨される方法です。
var pool *pgxpool.Pool
for i := range 6 {
pool, err = pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
if err == nil {
if err = pool.Ping(ctx); err == nil {
break
}
pool.Close()
}
delay := time.Second * (1 << i) // 1s, 2s, 4s, 8s, 16s, 32s
slog.Warn("database not ready, retrying", "attempt", i+1, "delay", delay, "err", err)
time.Sleep(delay)
}
if err != nil {
slog.Error("failed to connect to database", "err", err)
os.Exit(1)
}
SSE によるリアルタイム更新
Go バックエンドは Postgres の NOTIFY イベントをリッスンし、接続中の各クライアントに対して、Content-Type: text/event-stream を指定した標準的な HTTP レスポンスを開いたままにします。React フロントエンドでは、ブラウザー組み込みの EventSource API を使用します。プロキシの設定による悪影響を避けるため、WebSocket よりも SSE を推奨します。
フロントエンドのひな形とクリーンアップ
次のコマンドで、対話なしでフロントエンドのひな形を作成します。
bash -c 'cd "$(git rev-parse --show-toplevel)" && mise x -- npx create-react-router@latest frontend --yes --no-git-init --no-install --no-agent-skills'
テンプレートには Tailwind CSS があらかじめ設定されており、次の npm スクリプトが用意されています。dev(react-router dev — 内部では Vite を使用)、build(react-router build)、typecheck(react-router typegen && tsc)。ルートは app/routes.ts で宣言します。
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("pricing", "routes/pricing.tsx"),
] satisfies RouteConfig;
次に、アプリケーションコードを書く前に、以下のクリーンアップを行います。
-
react-router.config.tsでssr: falseを設定します — テンプレートの既定値はssr: trueで、Node サーバーが必要になります。この構成では Node サーバーを実行しません -
vite.config.tsに 開発用プロキシを追加します。開発サーバーがバックエンドのルートを Go に転送するようにします(既存のpluginsとresolveのエントリーは維持してください)。ターゲットは.envの Go のPORTと一致させる必要があります。server: { proxy: { "/api": "http://localhost:8080", "/auth": "http://localhost:8080", // Served by Go, derived from BASE_URL — without the proxy they 404 only in dev "/robots.txt": "http://localhost:8080", "/sitemap.xml": "http://localhost:8080", "/llms.txt": "http://localhost:8080", }, }, -
frontend/Dockerfile、frontend/.dockerignore、frontend/README.mdを削除します — Dockerfile はプロジェクトのルートで管理します(Dockerfile を参照)。また、テンプレートの README には、この構成で使用しない SSR のセットアップ方法が記載されています -
app/welcome/を削除し、既定のホームルート(app/routes/home.tsx)をアプリケーションの実際のホームルートに置き換えます -
startスクリプトと@react-router/serveを削除します — SPA モードでは Node サーバーを実行しません。@react-router/nodeとisbotは残してください。ビルド時のプリレンダリングでは、サーバーランタイムを解決するためにこれらが引き続き必要です。削除するとreact-router buildとtypegenが失敗します -
public/favicon.icoと、テンプレートのapp/root.tsxにある Google Fonts(Inter)のリンクを削除します — ファビコンとタイポグラフィは frontend-design スキルで処理します -
metaエクスポート(ルートまたはホームルート)で、実際のタイトルと説明を設定します — テンプレートの既定値をそのまま公開しないでください
public/favicon.svg のプレースホルダーは作成しないでください — ファビコンの生成は frontend-design スキルで別途処理します。
デプロイの制約
これらは app-deploy スキルの要件に準拠しています。
- ポートは必ず
PORT環境変数から取得します(デプロイするアプリでは80、ローカル開発では8080に設定します) - ヘルスチェック:
GET /up→ HTTP 200 - データベースには
DATABASE_URL(推奨)または個別のPOSTGRES_*環境変数を使用します — 設定されていない場合は処理を失敗させてください - ファイルストレージには
BLOB_STORAGE_PATH(例:/data/blobs)を使用します - プライマリデータストアには PostgreSQL を使用します(
supabase/postgresに同梱された 60 以上の拡張機能を利用可能)。アプリケーションで Postgres が提供するもの以外のサービスが必要な場合は、デプロイスキルの Kamal レイヤーを通じてアクセサリーを追加できます - ORM、React 以外の JavaScript フレームワーク、CSS プリプロセッサーは使用しません
Dockerfile
マルチステージ構成です。(1)Node でフロントエンドをビルドし、(2)Go バイナリをビルドし、(3)バイナリ、フロントエンドのアセット(frontend/dist/ にコピー)および CA 証明書を含む最小構成の Alpine ランタイムを作成します。Go バイナリにはマイグレーションが埋め込まれます。フロントエンドのアセットはバイナリと同じ階層にコピーします。
Dockerfile の FROM タグには、-alpine サフィックスを付けた mise.toml と同じメジャーバージョンを使用します。たとえば、FROM node:24-alpine や FROM golang:1-alpine です。Docker Hub はこれらの可変タグをビルド時に最新のマイナー/パッチバージョンに解決するため、手動でバージョンを調べなくても Dockerfile は mise.toml と同期されます。Go または Node の新しいマイナー/パッチバージョンがリリースされると、次回のビルドで自動的に取り込まれます。GHA BuildKit キャッシュがマニフェストの変更を検出し、そのレイヤー以降を再ビルドします。
ランタイムステージでは、バイナリとフロントエンドクライアントのビルド成果物(frontend/dist にコピー)を WORKDIR の直下に並べて配置する必要があります。
# Stage 1: Build frontend
FROM node:24-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
# Stage 2: Build backend
FROM golang:1-alpine AS backend-build
WORKDIR /app/backend
COPY backend/go.mod backend/go.sum ./
RUN go mod download
COPY backend/ ./
RUN CGO_ENABLED=0 go build -o /server ./cmd/server
# Stage 3: Runtime — binary and frontend assets as siblings under WORKDIR
FROM alpine:3
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=backend-build /server ./server
COPY --from=frontend-build /app/frontend/build/client ./frontend/dist
EXPOSE 80
ENV PORT=80
CMD ["./server"]
WORKDIR の具体的なパスは問いません。重要なのは、バイナリとフロントエンドのアセットの両方がその直下に配置されていることです。フロントエンドのビルド成果物は frontend/build/client/ に出力されます。Dockerfile では、このディレクトリを Go バイナリが想定するリテラルパスである frontend/dist にコピーします(上記の SPA キャッチオールを参照)。CMD は WORKDIR から実行されるため、両方を WORKDIR にコピーすれば要件を満たします。ローカル開発では Vite がフロントエンドを配信するため、静的ファイル用の Go ハンドラーは DEV_MODE が設定されていない場合にのみ登録されます。
.dockerignore ファイルは作成しないでください。 マルチステージビルドによって最終イメージはすでに小さく保たれています。また、go:embed に必要なファイル(例:migrations/)を誤って除外する .dockerignore を作成すると、作成時に明確なエラーが出ないままビルドが失敗します。
ローカル開発
すべてのツールは、computer-setup でセットアップした mise を介し、mise x コマンドを使って実行します。このコマンドは mise.toml を読み込み、シェルのアクティベーションを必要とせず、指定バージョンのツールを実行します。データベースは computer-setup で同じくセットアップした podman を使い、supabase/postgres コンテナとして実行します。本番イメージと同じ構成です。
コンテナの命名規則: 各プロジェクトのデータベースコンテナには、
<repo_name>-db(例:myapp-db)という名前を付けます。<repo_name>はプロジェクトのルートディレクトリのベース名です。これにより、同じマシン上に複数の共同創業者プロジェクトが共存しても、名前の衝突を防げます。セッション開始時に一度名前を決め、すべてのpodmanコマンドで一貫して使用してください。
重要:
go.modはプロジェクトのルートではなくbackend/にあります。 Go と sqlc のコマンド(mise x -- go run、mise x -- go build、mise x -- go test、mise x -- go mod tidy、mise x -- sqlc generate)はすべて、必ずbackend/ディレクトリから実行してください。bash -cの文字列内には、必ずcd "$(git rev-parse --show-toplevel)/backend" &&を含めてください。複数のシェル呼び出し(bash → go)が連なるコマンドでは、すべてを単一のbash -c文字列に入れ子にするのではなく、小さなヘルパースクリプトを作成することをおすすめします。これにより、ビルド失敗が繰り返される最も一般的な原因を避けられます。
プロジェクトのツールバージョン
初回セットアップ時(プロジェクトのルートに mise.toml がまだない場合)は、手動で作成します。
[tools]
go = "1"
sqlc = "1"
python = "3.14"
node = "24"
jq = "1"
[settings]
python.compile = false
python.compile = false は必須です。Python をソースからコンパイルせず、最新のコンパイル済みパッチバージョンに固定します(ソースからのコンパイルはビルド依存関係がないと失敗します)。
続いて、ツールを信頼してインストールします。
mise trust
mise install
この mise.toml はリポジトリにコミットされ、すべての開発者が同じバージョンを使用できるようにします。mise.toml ではメジャーバージョンのみを指定します(例:go = "1")。mise はインストール時に、これを最新の安定したマイナー/パッチバージョンに解決します。メジャーバージョンを変更するには、mise.toml を編集して mise install を再実行します。
このスキルでは、すべてのツールを mise x コマンド(例:mise x -- go run ./cmd/server)で実行します。シェルのアクティベーションフックを必要とせず、mise.toml で指定されたバージョンのツールを実行します。Claude Code の非対話型シェル、Preview の launch.json、およびあらゆるスクリプトコンテキストで確実に動作します。
ツールのアップグレード
セッションごとに実行(これはサービスの起動には含まれません。このスキルが読み込まれるたびに実行する必要がある独立した手順です)。
-
mise.tomlに上記の[settings]セクションがあることを確認し、なければ追加します。 -
mise 自体を更新します。
# macOS: brew upgrade mise # Linux, WSL, Windows: mise self-update -y -
プロジェクトのツールをアップグレードします。
mise upgrade
mise install ではなく mise upgrade を使用します。install はインストール済みのバージョンをスキップしますが、upgrade はより新しいパッチバージョンを確認します。
1. データベースとローカルサービスを起動する
以下のコマンドでは、Postgres のデフォルトポート 5432 を使用します。ホスト上でそのポートがすでに使用中(別のプロジェクトのコンテナ、または別の Postgres インスタンスによる使用など)でコンテナの起動に失敗する場合は、空いているポートを選び、.env 内の DATABASE_URL をそのポートに変更し、-p フラグも同じポートに合わせて変更してください。env ファイルを正とします。
# Derive the container name from the repo directory
CONTAINER_NAME="$(basename "$(pwd)")-db"
# Start supabase/postgres container (matching production image)
# Important: provide only the POSTGRES_PASSWORD environment variable. The database is started with both user and database name preset to `postgres`.
# Match the port in DATABASE_URL in .env.
podman run -d \
--name "$CONTAINER_NAME" \
-e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \
supabase/postgres:17.11.0.017
# Verify it's ready (uses container exec instead of pg_isready)
podman exec "$CONTAINER_NAME" pg_isready -U postgres
ローカルサービス
アプリケーションで Postgres 以外のサービスが必要な場合は、ローカルで podman コンテナとして実行します。各サービスは、デプロイ時に専用 VM としてプロビジョニングされるアクセサリーに対応します。
命名規則
すべてのプロジェクトコンテナには、<repo_name>-<accessory_name> という名前を付けます。
myapp-db
myapp-redis
myapp-n8n
Postgres には -db という規則がすでにあります。これをすべてのアクセサリーに適用します。これにより、クリーンアップのパターンが使えるようになります(すべてのプロジェクトコンテナを停止するを参照)。
起動または作成のパターン
コンテナはセッション間で保持されます。新しいセッションでは、前のセッションのコンテナがすでに存在している(停止中である)場合があります。単独の podman run ではなく、必ず起動または作成のパターンを使用してください。
podman start myapp-redis 2>/dev/null || \
podman run -d --name myapp-redis -p 6379:6379 redis:7-alpine
コンテナが存在する場合(実行中または停止中)、podman start は何も表示せずに成功します。存在しない場合は、フォールバックの podman run によって作成されます。これにより、セッション再開時の「name already in use」エラーを防げます。
2 種類のアクセサリー
バックエンド接続型(Redis、Kafka など):起動または作成 → 準備状態の確認 → .env に環境変数を追加 → Go クライアントを追加 → docs/INFRASTRUCTURE.md に記録します。
スタンドアロン型(n8n、WordPress など):起動または作成 → 準備状態の確認 → localhost:<port> のリンクをユーザーに提示 → docs/INFRASTRUCTURE.md に記録します。バックエンドからも API を呼び出す場合を除き、Go コードは不要です。
ハイブリッド型(例:webhook を使用する n8n):スタンドアロン型として扱い(ブラウザー用リンク)、バックエンドの環境変数(例:N8N_WEBHOOK_URL)も追加します。種類は standalone のままにし、環境変数の列でバックエンドへの依存関係を示します。
ポート競合の検出
コンテナを起動する前に、podman ps -a --format '{{.Names}} {{.Ports}}' | grep '<port>' を確認します。デフォルトポートがすでに使用中の場合は、空いているポートを選び、.env 内の URL 環境変数(例:REDIS_URL)をそのポートに変更し、コンテナでもそのポートを公開します。.env ファイルを正とします。
よく使う設定例
| アクセサリー | イメージ | ポート | 準備状態の確認 |
|---|---|---|---|
| Redis | redis:7-alpine | 6379 | podman exec <name> redis-cli ping |
| n8n | n8nio/n8n:latest | 5678 | curl -s http://localhost:5678/healthz |
| Meilisearch | getmeili/meilisearch:latest | 7700 | curl -s http://localhost:7700/health |
| WordPress | wordpress:latest | 8080 | curl -s http://localhost:8080 |
| WAHA | devlikeapro/waha:latest | 3000 | curl -s http://localhost:3000/api/health |
これらは出発点です。エージェントは、正しいポートと準備状態確認用エンドポイントについて、イメージのドキュメントを確認してください。
ローカルとデプロイ環境のホスト名
| サービス | ローカル(.env) | デプロイ環境のホスト名 |
|---|---|---|
| Postgres | localhost:5432 | db:5432 |
| Redis | localhost:6379 | redis:6379 |
| n8n | localhost:5678 | n8n:5678 |
ローカルでは、すべてのサービスが localhost 上にあります。デプロイ環境では、各アクセサリーに専用 VM が割り当てられ、CloudStack の内部 DNS によってアクセサリー名と同じホスト名が使われます。サービス間通信にパブリック IP を使用しないでください。.env(ローカル)と Kamal の設定(デプロイ環境)が値を提供し、Go では os.Getenv() を使って同じように読み取ります。
2. Go API を起動する(ターミナル 1)
bash -c 'ROOT="$(git rev-parse --show-toplevel)" && set -a && . "$ROOT/.env" && set +a && cd "$ROOT/backend" && DEV_MODE=1 mise x -- go run ./cmd/server'
3. フロントエンドの開発サーバーを起動する(ターミナル 2)
bash -c 'cd "$(git rev-parse --show-toplevel)/frontend" && mise x -- npm install && mise x -- npm run dev'
Vite のデフォルトポート(5173)だと決めつけないでください。デフォルトポートがすでに別のプロジェクトで使用されている場合、Vite は自動的に次に利用可能なポートを選択します。開発サーバーをバックグラウンドで起動した後、タスクの出力を確認し、Vite の起動バナーにある Local: の行(例:Local: http://localhost:5174/)を探してください。その行に記載された URL を使用し、以降のすべてのアクセスでポートを固定しないでください。また、.env の BASE_URL(およびそこに登録された OAuth リダイレクト URI)が実際のポートと一致していることを確認してください。
タスクの出力を確認できなくなった場合は、OS からポートを検出してください。
lsof -i -P -n -sTCP:LISTEN | grep node | awk '{print $9}'
npm run dev は react-router dev を実行します。これは内部で Vite を使用し、スキャフォールド時に追加された server.proxy ブロックを介して、/api/*、/auth/*、SEO ファイルを Go バックエンドにプロキシします。
すべてのプロジェクトコンテナを停止する
REPO_NAME="$(basename "$(pwd)")"
# Stop and remove all containers for this project
podman ps -a --filter "name=^${REPO_NAME}-" --format '{{.Names}}' | xargs -r podman stop
podman ps -a --filter "name=^${REPO_NAME}-" --format '{{.Names}}' | xargs -r podman rm
これは命名規則(<repo_name>-<accessory_name>)を利用しており、プロジェクトのすべてのコンテナを一度に削除します。
ビジュアルチェック(Playwright のスクリーンショット)
Claude Desktop Preview サーバーは使用しないでください。Go バックエンドと Vite 開発サーバーを手動で起動してください(上記の手順 2~3)。
テストに合格したら、ヘッドレススクリーンショットを撮影して目視で確認してください。すべてのプラットフォームで動作します。
セットアップ
プロジェクトルートの e2e/ には、playwright を含む最小限の package.json があります。初回セットアップ時は、次を実行してください。
bash -c 'cd "$(git rev-parse --show-toplevel)/e2e" && mise x -- npm install && mise x -- npx playwright install chromium'
e2e/ ディレクトリは frontend/ とは完全に分離されています。Dockerfile がここに触れることはないため、本番イメージには影響しません。
スクリーンショットの撮影
まず、実際の Vite ポートを確認してください(5173 が使用中の場合は自動的に次のポートになります)。
lsof -i -P -n -sTCP:LISTEN | grep node | awk '{print $9}'
bash -c 'cd "$(git rev-parse --show-toplevel)/e2e" && mise x -- npx playwright screenshot --viewport-size="1280,720" --full-page http://localhost:5173 /tmp/homepage.png'
モバイルの場合(PRD で必要とされている場合):--viewport-size="375,812"。認証が必要なルートの場合:e2e/ に POST /api/dev/login を呼び出して Cookie を取得し、ページに移動してスクリーンショットを撮るスクリプトを作成してください。Read ツールでスクリーンショットを読み取ってください。
ビジュアルチェックの範囲
- 主要なルート(ホーム、ダッシュボード、主な機能)。
- 認証が必要なルート(開発用ログイン経由)。
- 今回の作業で影響を受けるページ。
- 見た目 — 配置、パディング、間隔、読みやすさ、コントラスト。
違和感のある箇所を修正し、コミット前にスクリーンショットを撮り直してください。
ローカル開発のフィードバックループ
コードとテストを作成 → テストを実行 → ビジュアルチェック → 修正して繰り返す → ドキュメントを更新 → コミットしてプッシュ → 作業をまとめる。
Write/Edit Code + Tests
↓
Start services (podman, go run, npm run dev)
↓
Run tests (Layer 1: Go, Layer 2: Vitest + npm run typecheck)
↓
All pass? ──No──► Fix & repeat
↓ Yes
Visual check (screenshots)
↓
Looks right? ──No──► Fix & repeat
↓ Yes
Update docs (TASKS, PRD, ADRs, INFRASTRUCTURE)
↓
Commit & push → Wrap up session
コミット後、cofounder エージェントで定義されている形式で、セッションのまとめを提示してください。
sqlc のワークフロー
必ず次の順序に従い、手順 3 を省略しないでください。
backend/internal/database/queries/*.sqlの SQL クエリを作成または更新します。- sqlc generate を実行します。
bash -c 'cd "$(git rev-parse --show-toplevel)/backend" && mise x -- sqlc generate' - ハンドラーを書く前に、
backend/internal/database/sqlc/に生成された.goファイルを読み、正確な構造体名とフィールド名を確認します。sqlc が付ける名前は予測できません。
- 型キャスト付きの位置パラメーター(例:
$2::boolean)は、列名ではなくColumn2、Column3などになります。 - 部分的な
RETURNING句を持つクエリは、完全なモデル(User)とは別の行型(例:UpsertUserRow)を生成します。 - 出力を読まずにフィールド名を推測すると、型の不一致エラーが発生し、何度も修正を繰り返すことになります。
- 生成されたファイルの正確な名前を使って Go ハンドラーを書きます。Go ファイル内に SQL を手書きしないでください。
規約
- シンプルなハンドラー: リクエストを解析 → データベースを呼び出す → JSON を返します。ハンドラーにビジネスロジックを含めてはいけません。
- ロギング:
slogのみを使用します。fmt.Printlnやlog.Printlnは使用しないでください。 - バリデーション: すべての入力をサーバー側で検証します。クライアント側のバリデーションだけを信頼してはいけません。
- 認可: ミドルウェアだけでなく、すべてのハンドラーでチェックします。
- フロントエンドコンポーネント:
bash -c 'cd "$(git rev-parse --show-toplevel)/frontend" && mise x -- npx shadcn@latest add <component>' - ORM は使用しません。 SQL には sqlc のみを使用します。
- CSS プリプロセッサーは使用しません。 Tailwind CSS のみを使用します。
- 追加の JavaScript フレームワークは使用しません。 React + React Router のみを使用します。
- SSR フレームワークや Node サーバーランタイムは使用しません。 SEO のニーズはビルド時のプリレンダリングで満たします(公開ページ:SEO と AI エージェント向けのプリレンダリングを参照してください)。Next.js やランタイムレンダリングサーバーを導入してはいけません。
認可のベストプラクティス
アプリケーションにユーザーログイン領域がある場合、アプリをすばやくプロトタイプするためにユーザー名とパスワードによるセルフサインインを使用しても構いませんが、本番環境では使用しないでください。この方法のセキュリティは脆弱です。
推奨される方法: プロトタイピングではセルフサインイン(ユーザー名 + パスワード)から始め、その後できるだけ早く次のいずれか、または両方に移行します。
- マジックリンク付きメール — 実用的で、暗記する必要がありません。SMTP ゲートウェイの設定が必要です。references/smtp-gateway.mdを参照してください。
- Google Auth — 実用的で、暗記する必要がありません。Google のセキュリティメカニズムを利用します。Google Auth の設定が必要です(無料)。references/google-auth.mdを参照してください。
テスト用の開発ログイン
ローカル開発中、エージェントは認証が必要なページをテストする必要があります(ビジュアルチェックの Playwright スクリーンショットを使用)。マジックリンクと Google Auth のフローは自動テストで完了できないため、バックエンドには実際の認証フローをバイパスする開発専用のログインエンドポイントを公開する必要があります。
仕組み
バックエンドは、DEV_MODE=1 が設定されている場合にのみ POST /api/dev/login ルートを登録します。ガードはルート登録時(ミドルウェアではなく)に設ける必要があります。これにより、このフラグがない場合はエンドポイント自体が存在しません。
このエンドポイントはユーザー識別子(例:メールアドレス)を受け取り、ユーザーを検索して、実際の認証フローとまったく同じ仕組みでセッションを作成します。Cookie 名、トークン形式、セッションストアも同一です。唯一の違いは、外部プロバイダー(マジックリンクメールまたは Google OAuth)をスキップすることです。
テストユーザー
開発ログインハンドラーは、ユーザーがまだ存在しない場合、その場で作成する必要があります(INSERT ... ON CONFLICT DO NOTHING)。これにより、すべての環境(本番環境を含む)で実行されるマイグレーションにテストユーザーを含めずに済みます。
セキュリティ
DEV_MODEが設定されていない場合、エンドポイントを絶対に登録してはいけません。これはルート登録時のガードで強制します。- 本番環境へのデプロイでは、
DEV_MODEを絶対に設定してはいけません。デプロイスキルには含まれていません。
その他の参考資料
- references/smtp-gateway.md -- SMTP ゲートウェイの設定:メール(リマインダー、認証リンク、通知など)の送信に使用する Gmail(プロトタイピング)と Locaweb(本番環境)
- references/google-auth.md -- Google Auth OAuth の設定:Google Cloud Console の設定、同意画面、認証情報
Postgres 拡張機能のリファレンス
- references/pgroonga.md -- PGroonga 全文検索:演算子、ランキング、ハイライト表示、CJK サポート
- references/pgmq.md -- pgmq メッセージキュー:送信、読み取り、アーカイブ、削除の SQL 例
- references/pg-cron.md -- pg_cron + pg_net:スケジュール済みジョブ、HTTP トリガー、一般的なパターン
- references/pgvector.md -- pgvector 類似検索:距離演算子、HNSW/IVFFlat インデックス、チューニング
- references/pg-jsonschema.md -- pg_jsonschema バリデーション:CHECK 制約のパターン、主要な関数
- references/notify-patterns.md -- LISTEN/NOTIFY + 永続化:ジョブキューには pgmq、データ更新には通常のテーブル、ポーリングによるフォールバック