使用技術
Errata Hub を構成する技術と、実際の組み方をまとめています。
技術スタック
- フレームワーク
- Next.js 16(App Router)/ React 19
- 言語
- TypeScript
- スタイリング
- Tailwind CSS v4
- 認証
- Supabase Auth(PKCE)/ ソーシャルログイン(Google・GitHub)
- データベース
- PostgreSQL(Supabase)/ Prisma v7
- ファイル保存
- Supabase Storage
- バリデーション
- Zod
- 外部 API
- OpenBD / Google Books
- テスト
- Vitest / Testing Library / Playwright
- CI
- GitHub Actions / CodeQL / Dependabot
- ホスティング
- Vercel
ディレクトリ構成
機能ごとに縦に切り(features/)、利用者の操作(Server Action)も 1つ1ファイルで、その機能の actions/ に置いています。 複数の機能にまたがる操作だけは、画面と同じく app/ で組み立てます。 DB を読み書きしてよいのは各機能の db/ と、機能をまたぐ処理(services/)だけで、画面や操作のコードから DB の クライアントを読み込もうとすると lint エラーで落ちます。
src/ ├── app/ 画面と HTTP リクエストの受け付け口(URL がそのままフォルダ)。 │ 複数の機能にまたがる操作も、使う画面の隣に置く │ ├── (site)/ 公開ページ │ ├── admin/ 管理画面 │ └── api/ Route Handler(HTTP で直接受ける必要がある4本だけ) ├── features/ 機能ごとのまとまり(下は「投稿」の例) │ └── report/ │ ├── actions/ この機能だけで完結する操作。1つ1ファイルの Server Action(フォームの送信先をサーバー上の関数に直接向ける仕組み) │ ├── db/ DB を読み書きする。ここと services 以外からは DB を操作しない │ ├── components/ この機能でしか使わない画面部品 │ ├── constants/ ラベル・文字数上限 │ ├── utils/ DB にも画面にも依存しない関数 │ └── schema.ts 入力の検査(Zod) ├── components/ 機能に属さない画面部品(ui = 部品 / layout = 枠) ├── services/ 機能をまたぐ処理(認可・操作ログ・レート制限) ├── lib/ 外部サービスへ接続するクライアントを作る(Prisma・Supabase) ├── utils/ constants/ どの機能にも依存しない純粋な関数・定数 └── proxy.ts 静的アセットを除く全リクエストが最初に通る(CSP の nonce 発行・認証の確認)
実装の要点
OpenBD / Google Books API
ISBN で本を引くときは OpenBD、タイトルで探すときと書影(サムネイル)が要るときは Google Books を使う。どちらもブラウザからは呼ばず、サーバー側の /api/books/openbd と /api/books/search を通す。Google Books の API キーをブラウザに置かないためと、閲覧者の IP を外部サービスへ渡さないため。
Vitest / Playwright
種別やステータスの分岐は単体テストで押さえ、ログイン・投稿・管理操作の一連は実ブラウザで通している。文字のコントラスト比や CSP 違反の有無も e2e で測っていて、見た目の性質を目視に任せていない。
セキュリティと運用
CSP(nonce + strict-dynamic)
ページを返すたびに1回限りの識別子(nonce)を発行し、その識別子が付いたスクリプトだけをブラウザに実行させる。外部から差し込まれたスクリプトは識別子を持たないため、HTML の中にあっても実行されない。引き換えに、ページを事前に生成して配信することはできない(識別子がリクエストごとに変わるため)。
レート制限(Postgres)
投稿・追記・画像アップロード・書籍検索・出版社の回答・賛同には、利用者ごとに一定時間あたりの回数制限をかけている。回数の記録に専用のサービスは足さず、すでに使っている PostgreSQL の1テーブルで数える。加算と上限の判定を1つの SQL 文で行うので、同時に複数のリクエストが来ても二重に数えられない。
認可はサーバー側で判定
誰が何をしてよいか(認可)は、管理操作を実行するたびにサーバーでロールを確認して決めている。画面にボタンを出すかどうかは補助でしかなく、権限を保証していない。データベース側でも、ブラウザから直接つなぐ経路は全て拒否している(アプリのサーバーを通らない読み書きを成立させないため)。
操作ログ(監査ログ)
投稿・権限への変更と退会は、削除のような取り消せないものだけでなく、更新も含めて記録している(表示名とリンクの変更は記録していない)。変更前と変更後の内容を添えるので、誰が何をどう変えたかを後から辿れる。記録と操作は1つのトランザクションにまとめてあり、どちらか片方だけが残ることはない(外部サービスをまたぐ退会だけは例外で、記録に失敗しても退会は成立させる)。
システム構成
ブラウザからのリクエストは Vercel 上の Next.js が受け、サーバーコンポーネントや Server Action が Supabase(認証・データ永続化・画像の保存)と書誌 API に問い合わせます。 認証は PKCE の code フロー、外部 API キーはサーバー側に隠蔽しています。 書誌 API へはブラウザから直接アクセスせず、サーバーが代理で取得します(閲覧者の IP を外部へ渡さないため)。
データモデル
図は横にスクロールできます。
主要な6つのテーブルだけを描いています。矢印は1対多の関連で、矢の先が「多」側です。 投稿は利用者と書籍のどちらにも必ず紐づき(どちらも必須の参照)、書籍には出版社が任意で付きます。 出版社の回答が投稿と出版社の両方を参照しているのは、書籍の出版社を管理者が後から直せるためです。 書籍から辿る形にすると、過去の発言の帰属が後から変わってしまいます。 追記と回答はどちらも作った時点で不変で、2回目が1回目を書き換えることはありません。
画像・賛同・操作ログ・レート制限のテーブルは、関連の形を見るのに要らないので省きました。 全11テーブルの図(自動生成)と、型・制約・その判断をした理由はリポジトリにあります。schema.prismaER 図(全テーブル)