リアルタイムでうんこ状況を共有し、相互応援できるWebアプリケーション。匿名制で気軽に参加でき、定型文による安全な応援システムを提供。
- うんこ開始: ボタン押下でうんこセッション開始
- うんこ終了: ボタン押下でうんこセッション終了
- 応援受信: リアルタイムで応援メッセージを受信・表示
- セッション管理: 重複開始防止、異常終了時の自動クリーンアップ
- 現在うんこ中リスト: リアルタイムでうんこ中ユーザー一覧表示
- 開始通知: 「○○がうんこ開始しました」リアルタイム通知
- 応援送信: 定型文メッセージでうんこ中ユーザーを応援
- 応援履歴: 送信した応援の記録表示
- 統計表示: 累計うんこ数、ランキング、個人記録
- 匿名システム: アカウント登録不要、自動生成ニックネーム
- リアルタイム同期: Server-Sent Eventsによる即座な状態共有
- アクティビティ履歴: 最近のうんこ活動一覧
1. 💪 がんばれ〜!
2. 🎉 ナイスうんこ!
3. ⚡ スッキリして〜!
4. 🌟 お疲れさま!
5. 🔥 いいぞ〜!
6. ✨ 素晴らしい!
7. 🎊 やったね!
8. 💝 愛してる!
[ユーザー] → [Vercel Edge Network] → [Next.js] → [Turso]
↓
[Server-Sent Events]
フロントエンド:
├── Next.js 14 (App Router)
├── React 18 + TypeScript
├── Tailwind CSS
├── EventSource API (SSE)
└── PWA設定
バックエンド:
├── Next.js API Routes
├── Server-Sent Events
├── @libsql/client (Turso SDK)
└── UUID生成
インフラ:
├── Vercel (ホスティング + Edge Network)
├── Turso (グローバル分散SQLite)
└── GitHub (ソースコード管理)
GitHub → Vercel (自動デプロイ + グローバル配信)
→ Turso (データベース)
-- ユーザーテーブル
CREATE TABLE users (
id TEXT PRIMARY KEY,
nickname TEXT NOT NULL,
total_unko_count INTEGER DEFAULT 0,
created_at INTEGER DEFAULT (unixepoch()),
last_unko_at INTEGER,
last_active_at INTEGER DEFAULT (unixepoch())
);
-- うんこセッションテーブル
CREATE TABLE unko_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
status TEXT CHECK(status IN ('in_progress', 'completed', 'abandoned')) NOT NULL,
start_time INTEGER NOT NULL,
end_time INTEGER,
duration_seconds INTEGER,
total_cheers INTEGER DEFAULT 0,
created_at INTEGER DEFAULT (unixepoch()),
FOREIGN KEY (user_id) REFERENCES users (id)
);
-- 応援テーブル
CREATE TABLE cheers (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
from_user_id TEXT NOT NULL,
cheer_type_id INTEGER NOT NULL CHECK(cheer_type_id BETWEEN 1 AND 8),
created_at INTEGER DEFAULT (unixepoch()),
FOREIGN KEY (session_id) REFERENCES unko_sessions (id),
FOREIGN KEY (from_user_id) REFERENCES users (id)
);
-- アクティビティログテーブル
CREATE TABLE activity_logs (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
action_type TEXT NOT NULL, -- 'unko_start', 'unko_end', 'cheer_sent'
target_session_id TEXT,
metadata TEXT, -- JSON形式の追加データ
created_at INTEGER DEFAULT (unixepoch()),
FOREIGN KEY (user_id) REFERENCES users (id)
);
-- インデックス
CREATE INDEX idx_unko_sessions_status ON unko_sessions(status);
CREATE INDEX idx_unko_sessions_user_id ON unko_sessions(user_id);
CREATE INDEX idx_unko_sessions_start_time ON unko_sessions(start_time);
CREATE INDEX idx_cheers_session_id ON cheers(session_id);
CREATE INDEX idx_cheers_from_user_id ON cheers(from_user_id);
CREATE INDEX idx_activity_logs_user_id ON activity_logs(user_id);
CREATE INDEX idx_activity_logs_created_at ON activity_logs(created_at);
POST /api/users # 匿名ユーザー作成
GET /api/users/[userId] # ユーザー情報取得
PUT /api/users/[userId] # ユーザー情報更新
GET /api/users/[userId]/stats # ユーザー統計取得
GET /api/unko/current # 現在うんこ中リスト
POST /api/unko/start # うんこ開始
PUT /api/unko/[sessionId]/finish # うんこ終了
GET /api/unko/[sessionId] # セッション詳細
DELETE /api/unko/[sessionId] # セッション削除(管理用)
POST /api/cheer # 応援送信
GET /api/cheer/[sessionId] # セッションの応援一覧
GET /api/cheer/messages # 応援メッセージ定義取得
GET /api/stats # 全体統計
GET /api/stats/ranking # ランキング
GET /api/activity # 最近のアクティビティ
GET /api/health # ヘルスチェック
GET /api/events # Server-Sent Events エンドポイント
リクエスト:
{
"nickname": "うんこマスター#1234" // オプション、未指定時は自動生成
}
レスポンス:
{
"id": "user_abc123",
"nickname": "うんこマスター#1234",
"totalUnkoCount": 0,
"createdAt": 1672531200000
}
リクエスト:
{
"userId": "user_abc123"
}
レスポンス:
{
"sessionId": "session_xyz789",
"userId": "user_abc123",
"status": "in_progress",
"startTime": 1672531200000
}
リクエスト:
{
"sessionId": "session_xyz789",
"fromUserId": "user_def456",
"cheerTypeId": 1
}
レスポンス:
{
"cheerId": "cheer_uvw012",
"cheerMessage": "💪 がんばれ〜!",
"timestamp": 1672531200000
}
ストリームイベント:
// うんこ開始イベント
data: {
"type": "unko_started",
"sessionId": "session_xyz789",
"user": {
"id": "user_abc123",
"nickname": "うんこマスター#1234"
},
"timestamp": 1672531200000
}
// 応援受信イベント
data: {
"type": "cheer_received",
"sessionId": "session_xyz789",
"fromUser": {
"id": "user_def456",
"nickname": "応援団長#5678"
},
"cheerMessage": "💪 がんばれ〜!",
"timestamp": 1672531200000
}
// うんこ終了イベント
data: {
"type": "unko_finished",
"sessionId": "session_xyz789",
"user": {
"id": "user_abc123",
"nickname": "うんこマスター#1234"
},
"duration": 180,
"totalCheers": 5,
"timestamp": 1672531200000
}
// ユーザー参加イベント
data: {
"type": "user_joined",
"user": {
"id": "user_ghi789",
"nickname": "新参者#9999"
},
"timestamp": 1672531200000
}
/ # メイン画面
├── うんこボタン # 大きな💩ボタン
├── 現在うんこ中リスト # リアルタイム更新
├── 応援パネル # 定型文ボタン8個
├── 統計ダッシュボード # 個人・全体統計
└── アクティビティ # 最近の活動
interface AppState {
user: User | null
currentUnkoUsers: UnkoSession[]
userSession: string | null
onlineUsers: number
todayStats: DailyStats
cheerMessages: CheerMessage[]
}
interface User {
id: string
nickname: string
totalUnkoCount: number
lastUnkoAt?: number
}
interface UnkoSession {
sessionId: string
userId: string
nickname: string
startTime: number
totalCheers: number
}
interface CheerMessage {
id: number
text: string
emoji: string
}
// lib/sse.ts
export class SSEConnection {
private eventSource: EventSource | null = null
private reconnectDelay = 1000
private maxReconnectDelay = 30000
connect(onMessage: (event: SSEEvent) => void) {
this.eventSource = new EventSource('/api/events')
this.eventSource.onmessage = (event) => {
const data = JSON.parse(event.data)
onMessage(data)
}
this.eventSource.onerror = () => {
this.handleReconnect()
}
}
private handleReconnect() {
setTimeout(() => {
this.connect()
this.reconnectDelay = Math.min(
this.reconnectDelay * 2,
this.maxReconnectDelay
)
}, this.reconnectDelay)
}
disconnect() {
this.eventSource?.close()
}
}
// Rate limiting設定
const rateLimits = {
unkoStart: { maxRequests: 10, windowMs: 60000 }, // 1分間に10回
cheerSend: { maxRequests: 30, windowMs: 60000 }, // 1分間に30回
userCreation: { maxRequests: 5, windowMs: 300000 }, // 5分間に5回
apiGeneral: { maxRequests: 100, windowMs: 60000 } // 1分間に100回
}
- 匿名ユーザーID: UUID v4使用
- IPアドレス: ハッシュ化保存(必要時のみ)
- 個人特定情報: 一切保存しない
- XSS対策: すべてのユーザー入力をサニタイズ
- CSRF対策: Next.js標準保護機能
// セッション自動クリーンアップ
const cleanupAbandonedSessions = async () => {
const thirtyMinutesAgo = Date.now() - (30 * 60 * 1000)
await turso.execute({
sql: `UPDATE unko_sessions
SET status = 'abandoned'
WHERE status = 'in_progress'
AND start_time < ?`,
args: [thirtyMinutesAgo]
})
}
// 5分ごとに実行
setInterval(cleanupAbandonedSessions, 5 * 60 * 1000)
// Next.js API Routes キャッシュ
export async function GET() {
const stats = await getGlobalStats()
return Response.json(stats, {
headers: {
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300'
}
})
}
// Turso接続プール
const tursoPool = {
connections: [],
maxConnections: 10,
getConnection: () => { /* プール管理 */ }
}
// 監視対象メトリクス
const metrics = {
concurrentUnkoUsers: () => getCurrentUnkoUsers().length,
totalActiveConnections: () => sseConnections.size,
averageUnkoDuration: () => calculateAverageUsingDB(),
dailyUnkoCount: () => getTodayUnkoCount(),
cheersSentPerDay: () => getTodayCheersCount(),
responseTime: () => measureAPILatency(),
errorRate: () => calculateErrorRate()
}
想定負荷:
├── ~100同時接続: 現構成で余裕
├── ~1000同時接続: Vercel Proプラン
├── ~10000同時接続: Redis追加検討
└── ~50000同時接続: マイクロサービス化
Vercel Free Tier:
├── 100GB帯域幅/月
├── Function実行時間: 100時間/月
├── Edge Functions: 500KB-秒/日
└── データベース接続: 無制限
Turso Free Tier:
├── 9GBストレージ
├── 1億行読み取り/月
├── 2500万行書き込み/月
├── 最大500データベース
└── 3拠点レプリケーション
GitHub:
└── パブリックリポジトリ無料
Phase 1 (~1000ユーザー):
├── Vercel: $0
├── Turso: $0
├── ドメイン: $12/年
└── 合計: $1/月
Phase 2 (~10000ユーザー):
├── Vercel Pro: $20/月
├── Turso: $0-8.25/月
├── 監視ツール: $0-10/月
└── 合計: $20-38/月
Phase 3 (~100000ユーザー):
├── Vercel Enterprise: $400/月
├── Turso Scale: $50-200/月
├── 追加インフラ: $100-300/月
└── 合計: $550-900/月
Day 1: 基盤構築
├── Next.js プロジェクト作成
├── Turso データベース設定
├── 基本API実装
└── 匿名ユーザー機能
Day 2: 完成・リリース
├── SSE実装
├── フロントエンドUI
├── 応援機能
├── 統計機能
├── PWA設定
└── 本番デプロイ
# 1. 環境構築
git clone https://github.com/your/unkonow
cd unkonow
npm install
# 2. Turso設定
turso auth login
turso db create unkonow
turso db tokens create unkonow
# 3. 環境変数設定
echo "TURSO_DATABASE_URL=..." > .env.local
echo "TURSO_AUTH_TOKEN=..." >> .env.local
# 4. ローカル開発
npm run dev
# 5. Vercel デプロイ
vercel --prod
# .github/workflows/deploy.yml
name: Deploy to Vercel
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run build
- run: npm run test
- uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
// 構造化ログ
const logger = {
info: (event, metadata) => {
console.log(JSON.stringify({
level: 'info',
event,
timestamp: new Date().toISOString(),
...metadata
}))
},
error: (error, context) => {
console.error(JSON.stringify({
level: 'error',
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString(),
...context
}))
}
}
// GET /api/health
export async function GET() {
const checks = {
database: await checkTursoConnection(),
sse: checkSSEConnections(),
memory: process.memoryUsage(),
uptime: process.uptime()
}
const isHealthy = Object.values(checks).every(check => check.status === 'ok')
return Response.json({
status: isHealthy ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
checks
}, {
status: isHealthy ? 200 : 503
})
}
# Turso自動バックアップ(標準機能)
# 追加のデータエクスポート
turso db dump unkonow --output backup-$(date +%Y%m%d).sql
# 定期バックアップスクリプト
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
turso db dump unkonow --output "backups/unkonow_backup_$DATE.sql"
aws s3 cp "backups/unkonow_backup_$DATE.sql" s3://unkonow-backups/
- 可用性: 99.9%以上
- レスポンス時間: API 200ms以下、SSE 100ms以下
- 同時接続数: 1000接続まで安定動作
- エラー率: 0.1%以下
- DAU (Daily Active Users): 目標100人
- 平均セッション時間: 5分以上
- 応援送信率: うんこ1回あたり平均3回
- 継続率: 7日後30%、30日後10%
- デプロイ頻度: 週1回以上
- MTTR (平均復旧時間): 30分以内
- カバレッジ: テストカバレッジ80%以上
- 技術負債: 月1回の定期リファクタリング
結論: この構成により、最小コストで最大効果のうんこ共有サービスを構築可能。技術的制約が少なく、スケーラビリティも確保されている。