┌─────────────────────────────────────┐
│ Next.js (全て) │
├─────────────────────────────────────┤
│ • Frontend (React) │
│ • Server Components │
│ • Server Actions (ビジネスロジック) │
│ • API Routes │
│ • 認証処理 │
└─────────────────────────────────────┘
↓
Supabase (DB/Auth/ファイル)
特徴: すべてNext.jsで実装
┌─────────────────────────┐ ┌──────────────────────┐
│ Next.js Frontend │ │ Node/Hono Backend │
│ • React Components │◄─────►│ • API Routes │
│ • Server Components │ │ • ビジネスロジック │
│ • Client Actions │ │ • 認証 & 暗号化 │
└─────────────────────────┘ └──────────────────────┘
↓
┌──────────────┐
│ PostgreSQL │
│ + Supabase │
└──────────────┘
特徴: Frontend と Backend が完全に独立
メリット:
• Backend を独立して スケールアウト可能
• Frontend は静的配信(CDN)で高速
• 重い処理は Backend に集約
例:
10万アクセス → Frontend: 1台 / Backend: 10台
隠蔽:
• API Key, Secret キーを Backend のみに持たせる
• Client 側は秘密鍵を知らない
• Supabase の service_role キーを Backend でのみ使用
図:
Client Backend Supabase
↓ ↓ ↓
[Request]→[認証]→[Keys]→[DB操作]
↑
服務ロールキーはここだけ
できること:
• Frontend: デザイナー・UI 開発者
• Backend: サーバー・DB 最適化の専門家
例:
Frontend: Next.js (TypeScript)
Backend: Hono / Express / FastAPI / Go
各層で最適な技術選択が可能
モノリシック時の課題:
• Server Components でデータ取得 → 毎回 DB hit
• キャッシュ戦略が複雑
分離時:
• Frontend: 静的 HTML → CDN キャッシュ ✅
• Backend: REST API → Edge Cache ✅
- Redis キャッシュ層を追加可能
デメリット:
• デプロイ先が複数(Vercel + Railway など)
• 環境管理が2倍
• デバッグが難しい
- Frontend error vs Backend error の切り分け
解決策:
• ローカル開発で両方同時起動 (docker-compose)
• Logging 戦略の統一
モノリシック:
Request → Server Action → DB → 20ms
分離型:
Request → Frontend → HTTP → Backend → DB → 50ms
差: +30ms の遅延が許容できるか
課題:
Frontend も Backend も同じDB に接続
→ 整合性管理が大変
例: ユーザー削除時に両方で考慮必須
モノリシック:
$ git push → Vercel が自動デプロイ
分離型:
Frontend: git push → Vercel deploy
Backend: git push → Railway deploy
→ 両方のデプロイ待つ必要あり
構成:
Frontend: Next.js
Backend: Supabase (BaaS)
DB: PostgreSQL (Supabase)
利点:
• シンプル、保守しやすい
• デプロイ1つで完結
• RLS で権限管理
欠点:
• 秘密鍵を frontend にも必要
• スケーリング限界が来やすい
• API キーの管理が面倒
推奨: 個人プロジェクト、MVP、規模が小さい場合
構成:
Frontend: Next.js (Vercel)
Backend: Hono / Express (Railway / Fly.io)
DB: PostgreSQL (Supabase)
利点:
• API層でセキュリティ集約
• Backend スケーリング可能
• 言語自由度
欠点:
• 運用複雑(2つのサーバ)
• 通信遅延 +30ms 程度
実装例:
【Frontend】
export async function getArticles() {
const res = await fetch('https://api.example.com/articles');
return res.json();
}
【Backend (Hono)】
import { Hono } from 'hono';
import supabase from './supabase';
const app = new Hono();
app.get('/articles', async (c) => {
const { data } = await supabase
.from('articles')
.select('*')
.eq('status', 'published');
return c.json({ data });
});
推奨: 成長中のスタートアップ、チーム規模多い場合
構成:
Frontend: Next.js pages/
API Gateway: Next.js api/routes/
Backend: 別プロセス (Node/Python)
DB: PostgreSQL
利点:
• Gateway でリクエスト変換可能
• 柔軟な構成
欠点:
• 層数が多い(複雑)
• デプロイ難易度UP
推奨: 大規模エンタープライズ
構成:
Frontend: Next.js (Vercel)
BFF: Hono (Railway)
Backend API: REST API (別チーム)
DB: 複数
利点:
• GraphQL / REST を BFF で変換可能
• Multiple Backends 対応
欠点:
• 非常に複雑
• 運用コスト大
図:
Client → BFF (Hono) → Backend1 (User API)
→ Backend2 (Post API)
→ Backend3 (Analytics)
推奨: マイクロサービス企業
// .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ... ← これが client に露出!
// client component で使える状態
const supabase = createClient();
// ブラウザから直接 DB アクセス可能
Frontend (.env.local)
NEXT_PUBLIC_API_URL=https://api.example.com
Backend (.env)
SUPABASE_URL=https://xxxx.supabase.co
SUPABASE_KEY=eyJ... ← Server Only, ブラウザに見えない
SERVICE_ROLE_KEY=xxx ← Backend のみ
【Login Flow】
1. Client: POST /api/login { email, password }
2. Backend: Supabase で認証 → JWT 生成
3. Backend: Set-Cookie: auth_token=jwt_token (HttpOnly)
4. Client: 以降の Request で Cookie 自動送信
メリット:
• XSS 対策 (HttpOnly Cookie)
• CSRF トークンも対策可能
【Hono例】
import { getCookie, setCookie } from 'hono/cookie';
app.post('/login', async (c) => {
const { email, password } = await c.req.json();
const { data, error } = await supabase.auth.signInWithPassword({
email, password
});
if (error) return c.json({ error: error.message }, 401);
// ✅ HttpOnly Cookie に設定
setCookie(c, 'auth_token', data.session.access_token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'Strict',
});
return c.json({ success: true });
});
【API Request】
GET /api/articles
Authorization: Bearer eyJhbGc...
メリット:
• RESTful
• Mobile 対応
デメリット:
• XSS 対策が面倒
• LocalStorage に保存すると XSS で盗まれる
対策:
→ In-Memory 保持 + refresh token rotation
// Hono CORS
import { cors } from 'hono/cors';
app.use(cors({
origin: 'https://example.com', // Frontend URL のみ許可
credentials: true, // Cookie 送信許可
}));
// ❌ NG
app.use(cors({
origin: '*', // 全てのドメインからアクセス可能 = セキュリティリスク
}));
import { Hono } from 'hono';
const app = new Hono();
const rateLimitMap = new Map();
function checkRateLimit(ip: string): boolean {
const now = Date.now();
const limit = rateLimitMap.get(ip) || [];
const recent = limit.filter(t => now - t < 60000); // 60秒以内
if (recent.length >= 100) return false; // 100回/分を超えたら拒否
recent.push(now);
rateLimitMap.set(ip, recent);
return true;
}
app.use(async (c, next) => {
const ip = c.req.header('cf-connecting-ip') || 'unknown';
if (!checkRateLimit(ip)) {
return c.json({ error: 'Too Many Requests' }, 429);
}
await next();
});
import { z } from 'zod';
const createArticleSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
tags: z.array(z.string()).max(10),
});
app.post('/articles', async (c) => {
const body = await c.req.json();
// ✅ Validate
const result = createArticleSchema.safeParse(body);
if (!result.success) {
return c.json({ error: result.error.flatten() }, 400);
}
const { title, content, tags } = result.data;
// DB 保存
// ...
});