Next.js、Supabase、Cronを使ったエージェント駆動アプリの構築(2026年版)
Next.js、Supabase、Cron を使用したエージェント駆動アプリの構築
この半年間、クライアント向けにエージェント駆動型アプリケーション 3 つの構築に没頭してきました。2 つは完全に本番稼働しており、毎日数千のタスクを自動的に処理しています。何がこれらのプロジェクトを推し進めているのか?それは Vercel にデプロイされた Next.js、永続化とリアルタイム対応用の Supabase、そしてエージェントの調整と過剰な API 料金の防止のための従来の cron ベースのオーケストレーションの強力な組み合わせです。これは単なる理論ではなく、私は何が機能するか、落とし穴、そして飛び込む前に知っておきたかったすべての洞察を説明します。

2026 年のエージェント駆動型アプリケーションの実態
「エージェント駆動型アプリケーション」について語るとき、私は単にシステムプロンプトをオウム返しするチャットボットについて議論しているわけではありません。多段階のタスクを自律的に処理し、リアルタイムデータに基づいた意思決定を行い、スケジュールで機能し、すべて人間の助長なしで動作するソフトウェアを考えてください。
本番稼働中の実例をご紹介します:
- コンテンツパイプライン。RSS フィードをスキャンし、サマリーを生成し、ソーシャル投稿のドラフトを作成し、レビュー用にキューに入れます。このサイクルは 30 分ごとに時計仕掛けのように繰り返されます。
- データ調整エージェント。毎晩複数の API に接続し、矛盾を検出し、自動修正を試み、解決できない問題をエスカレーションします。
- 顧客オンボーディングエージェント。新規サインアップウェブフックによってトリガーされます。これらのエージェントはリソースをプロビジョニングし、カスタムシーケンスを送信し、ユーザーの動作を学習しながら適応します。
共通点は何か?彼らは状態永続化、スケジュール実行、ツールアクセス、および人間が覗き込んで必要に応じて介入する方法が必要です。それが、私たちが深掘りしているスタックの最適なポイントです。
このスタックが機能する理由
他のセットアップを試してみました。Python バックエンド と組み合わせた LangChain、オーケストレーション用の Temporal、カスタム AWS キュー システムがあります。これらはすべて機能しますが、2026 年までにほとんどのチームが対応できない山ほどの運用上の複雑性を伴います。
私の率直な比較は以下のとおりです:
| スタック | セットアップ時間 | 運用上のオーバーヘッド | 1 日 10K タスク時のコスト | 最適なケース |
|---|---|---|---|---|
| Next.js + Supabase + Vercel Cron | 1-2 日 | 低 | 約 $85-150/月 | ほとんどのエージェントアプリ |
| Python + Celery + Redis + AWS | 3-5 日 | 高 | 約 $200-400/月 | ヘビーコンピューティング タスク |
| Temporal + カスタムワーカー | 5-10 日 | 非常に高い | 約 $300-600/月 | 複雑なワークフロー |
| Inngest + Next.js | 1-2 日 | 低~中 | 約 $100-200/月 | イベント駆動エージェント |
Next.js の組み合わせは速度で圧勝します。インフラストラクチャを心配することなく、サーバーレス関数、ダッシュボード UI、データベース、認証、リアルタイム更新、時間指定実行を取得できます。私たちの大多数はエージェントアプリを構築していますが、それは十分以上です。
アーキテクチャの概要
プロジェクト全体で成功してきたアーキテクチャの全体像を描きましょう:
┌─────────────────┐ ┌──────────────────┐
│ Vercel Cron │────▶│ Next.js API Route │
│ (Scheduler) │ │ (Agent Runner) │
└─────────────────┘ └────────┬───────────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
┌──────────┐ ┌─────────┐ ┌───────────┐
│ Supabase │ │ LLM API │ │ External │
│ (State + │ │ (OpenAI/│ │ Tools & │
│ Queue) │ │ Claude) │ │ APIs │
└──────────┘ └─────────┘ └───────────┘
│
▼
┌──────────┐
│ Supabase │
│ Realtime │──▶ Dashboard UI
└──────────┘
cron ジョブは Next.js API ルートに接続し、Supabase からタスクをプルし、エージェントロジックを実行し、結果を書き戻し、完了します。一方、ダッシュボードは Supabase Realtime がライブ更新を配信するアクションを見守っています。
はい、十分シンプルですが、悪魔は詳細に潜んでいます。

Next.js と Vercel を使用した基盤の構築
2026 年にエージェントアプリを構築するとき、App Router 付きの Next.js 15 は必須です。Server Actions とRoute Handlers のおかげで、最もクリーンな実行パターンが提供されます。Social Animal では、この Next.js スキルを完成させ、App Router がバックエンド集約的なプロジェクトに見事に成熟したことを教えてくれます。
プロジェクトの開始は簡単です:
pnpx create-next-app@latest agent-app --typescript --tailwind --app
cd agent-app
pnpm add @supabase/supabase-js ai @ai-sdk/openai
Vercel AI SDK を利用しています。異なる LLM プロバイダー間での統一インターフェースのおかげです。2026 年までに、ai パッケージのバージョン 4.x は構造化出力、ツール呼び出し、ストリーミングをすぐにサポートします – LangChain は誰?
これで、vercel.json の cron 設定は次のようになります:
{
"crons": [
{
"path": "/api/agents/run",
"schedule": "*/15 * * * *"
},
{
"path": "/api/agents/cleanup",
"schedule": "0 2 * * *"
}
]
}
最初の cron は 15 分ごとにエージェントを実行します。2 番目は毎晩クリーンアップします。Vercel Pro ($20/月) は 1 分の頻度で cron ジョブをサポートしていることを覚えておいてください。ただし、Hobby プランを使用している場合は、日次 cron に限定されています。これはほとんどのエージェント駆動型ニーズには対応しません。
Vercel Function 設定
エージェントタスクは夏のブロックバスター以上に長くなる可能性があります (まあ、そこまでではありませんが)、デフォルトでは 10 秒以上の実行が必要です。このようにルートを設定します:
// app/api/agents/run/route.ts
export const maxDuration = 300; // Pro プランで 5 分
export const dynamic = 'force-dynamic';
export async function GET(request: Request) {
// 認可されていないアクセスを防ぐため、cron シークレットを検証してください
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Unauthorized', { status: 401 });
}
// エージェント ロジックはここに記述します
return Response.json({ success: true });
}
その maxDuration ですか?それは重要です。Pro では最大 300 秒、Enterprise でお金を泳いでいるなら 900 秒です。5 分以上続くルーチンタスク?それらを分割するか、実行モデルを再考する時が来ました。
Supabase をエージェントの脳として
Supabase – パワーハウスです。タスクキュー、状態ストア、監査ログ、リアルタイム更新の通知者として機能します。時間をかけて微調整してきたスキーマは次のとおりです:
-- エージェント タスク キュー
CREATE TABLE agent_tasks (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
agent_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'running', 'completed', 'failed', 'cancelled')),
priority INTEGER DEFAULT 0,
payload JSONB NOT NULL DEFAULT '{}',
result JSONB,
error TEXT,
attempts INTEGER DEFAULT 0,
max_attempts INTEGER DEFAULT 3,
locked_until TIMESTAMPTZ,
scheduled_for TIMESTAMPTZ DEFAULT NOW(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- キュー ポーリング クエリのインデックス
CREATE INDEX idx_agent_tasks_queue ON agent_tasks (
agent_type, status, priority DESC, scheduled_for
) WHERE status = 'pending';
-- 可観測性用のエージェント実行ログ
CREATE TABLE agent_logs (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
task_id UUID REFERENCES agent_tasks(id),
level TEXT NOT NULL DEFAULT 'info',
message TEXT NOT NULL,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- エージェント状態永続化 (マルチステップエージェント用)
CREATE TABLE agent_state (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
agent_type TEXT NOT NULL,
agent_instance_id TEXT NOT NULL,
state JSONB NOT NULL DEFAULT '{}',
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(agent_type, agent_instance_id)
);
その locked_until 列?それが重要です。それはいわば分散ロックをシミュレートしています。つまり、cron ジョブがタスクを選択するとき、locked_until を 5 分前に設定します。プロセスがクラッシュしても、ロックが期限切れになり、別のサイクルが別の試みを与えることができます。
タスク取得クエリ
王冠の宝石。これはタスクを再処理せずにキャッチするゴールデンチケットです:
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);
async function claimTasks(agentType: string, limit: number = 5) {
const { data: tasks, error } = await supabase
.rpc('claim_agent_tasks', {
p_agent_type: agentType,
p_limit: limit,
p_lock_duration: '5 minutes'
});
if (error) throw error;
return tasks;
}
そして対応する Postgres 関数:
CREATE OR REPLACE FUNCTION claim_agent_tasks(
p_agent_type TEXT,
p_limit INTEGER,
p_lock_duration INTERVAL
)
RETURNS SETOF agent_tasks AS $$
BEGIN
RETURN QUERY
UPDATE agent_tasks
SET
status = 'running',
locked_until = NOW() + p_lock_duration,
started_at = NOW(),
attempts = attempts + 1,
updated_at = NOW()
WHERE id IN (
SELECT id FROM agent_tasks
WHERE agent_type = p_agent_type
AND status = 'pending'
AND scheduled_for <= NOW()
AND (locked_until IS NULL OR locked_until < NOW())
AND attempts < max_attempts
ORDER BY priority DESC, scheduled_for ASC
LIMIT p_limit
FOR UPDATE SKIP LOCKED
)
RETURNING *;
END;
$$ LANGUAGE plpgsql;
FOR UPDATE SKIP LOCKED はここで無視されたヒーローです。2 つの並行呼び出しが互いを踏みにじらないようにします。信じてください、二重処理の発見と修正は笑いませんでした。
Cron ベースのエージェント オーケストレーション
Vercel Cron は、この全体の操作の定期的な鼓動です。15 分ごと、エージェント ランナーが動き始め、タスクをつかみ、処理し、次の呼び出しまで眠ります。
フル エージェント ランナー ルートがどのように表示されるかは次のとおりです:
// app/api/agents/run/route.ts
import { claimTasks, completeTask, failTask, logAgent } from '@/lib/agents';
import { runContentAgent } from '@/lib/agents/content';
import { runReconciliationAgent } from '@/lib/agents/reconciliation';
export const maxDuration = 300;
const AGENT_RUNNERS: Record<string, (task: AgentTask) => Promise<any>> = {
'content-pipeline': runContentAgent,
'data-reconciliation': runReconciliationAgent,
};
export async function GET(request: Request) {
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Unauthorized', { status: 401 });
}
const results = { processed: 0, failed: 0, skipped: 0 };
for (const [agentType, runner] of Object.entries(AGENT_RUNNERS)) {
const tasks = await claimTasks(agentType, 5);
for (const task of tasks) {
try {
await logAgent(task.id, 'info', `Starting ${agentType} task`);
const result = await runner(task);
await completeTask(task.id, result);
results.processed++;
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
await failTask(task.id, message);
await logAgent(task.id, 'error', message);
results.failed++;
}
}
}
return Response.json(results);
}
エージェントタイプあたりの 5 つのタスク上限の順序処理に注目してください。これは関数実行時間を確認するのに役立ちます。50 個の保留中のタスクのパイルがありますか?複数の cron サイクルに広げてください。急いではなく、問題ありません。
Cron が十分でない場合
時々、人生は急速に近づいてくるので、cron は単に切り取られていません。これらの場合、cron を Supabase データベース ウェブフックまたはエッジ関数と統合します。データベースの変更によってトリガーされます:
-- データベース変更時にエッジ関数をトリガーする
CREATE OR REPLACE FUNCTION notify_urgent_task()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.priority > 5 THEN
PERFORM net.http_post(
url := 'https://your-app.vercel.app/api/agents/urgent',
headers := jsonb_build_object(
'Authorization', 'Bearer ' || current_setting('app.webhook_secret')
),
body := jsonb_build_object('task_id', NEW.id)
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
このセットアップにより、予測可能な cron ベースの処理と緊急タスクの迅速な対応を混在させることができます。
エージェント ループの構築
ここは魔法が起こるところです。Vercel AI SDK のツール呼び出しを使用したエージェント実装の例:
// lib/agents/content.ts
import { generateText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
import { supabase } from '@/lib/supabase';
export async function runContentAgent(task: AgentTask) {
const { sourceUrl, targetPlatform } = task.payload;
const { text, toolCalls, usage } = await generateText({
model: openai('gpt-4o-mini'),
system: `You are a content processing agent. Fetch the source content,
analyze it, and generate appropriate social media content for ${targetPlatform}.
Use the provided tools to complete your task.`,
prompt: `Process this source: ${sourceUrl}`,
tools: {
fetchContent: tool({
description: 'Fetch content from a URL',
parameters: z.object({ url: z.string().url() }),
execute: async ({ url }) => {
const res = await fetch(url);
const html = await res.text();
// HTML をストリップ、主要コンテンツを抽出
return extractMainContent(html);
},
}),
saveContent: tool({
description: 'Save generated content for review',
parameters: z.object({
title: z.string(),
body: z.string(),
platform: z.string(),
suggestedPostTime: z.string(),
}),
execute: async (content) => {
const { data, error } = await supabase
.from('generated_content')
.insert({
...content,
task_id: task.id,
status: 'pending_review',
})
.select()
.single();
if (error) throw error;
return { saved: true, id: data.id };
},
}),
},
maxSteps: 5, // ツール呼び出しラウンドを最大 5 回まで許可
});
return {
summary: text,
toolCallCount: toolCalls.length,
tokensUsed: usage.totalTokens,
};
}
maxSteps を 5 に設定すると、LLM はツール呼び出し、結果評価、処理を行うことができます。ちょうど小さなワーカービーがやることのようなものです。ほとんどのコンテンツタスクは 3~4 ラウンドで完了します。
Supabase Realtime を使用したリアルタイム エージェント モニタリング
自律的なエージェントは素晴らしいですが、それらを注視することは最も重要です。Supabase Realtime は、エージェントが物事を成し遂げるにつれてリアルタイムで更新されるダッシュボードを提供します:
// components/AgentDashboard.tsx
'use client';
import { useEffect, useState } from 'react';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
export function AgentDashboard() {
const [tasks, setTasks] = useState<AgentTask[]>([]);
const [logs, setLogs] = useState<AgentLog[]>([]);
useEffect(() => {
// タスク変更をサブスクライブ
const taskChannel = supabase
.channel('agent-tasks')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'agent_tasks' },
(payload) => {
setTasks(prev => {
const updated = [...prev];
const idx = updated.findIndex(t => t.id === payload.new.id);
if (idx >= 0) updated[idx] = payload.new as AgentTask;
else updated.unshift(payload.new as AgentTask);
return updated.slice(0, 50);
});
}
)
.subscribe();
// ログ ストリームをサブスクライブ
const logChannel = supabase
.channel('agent-logs')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'agent_logs' },
(payload) => {
setLogs(prev => [payload.new as AgentLog, ...prev].slice(0, 100));
}
)
.subscribe();
return () => {
supabase.removeChannel(taskChannel);
supabase.removeChannel(logChannel);
};
}, []);
// ダッシュボード UI をレンダリングします...
}
これはエージェント活動のライブストリームを提供します。一般的に、インジケーター、タスク数、エラー率ビジュアル、さらには「今すぐ実行」ボタンを追加して、テストを行います。Supabase Realtime は無料ティアに含まれています – 追加費用は不要です。
障害とエッジケースの処理
それに備えてください – 何かが爆発します。API が応答しなくなるか、レート制限があなたを顔に叩きつけたり、サービスが単にナンセンスを返すだけです。爆発を乗り越えるシステムを作ることについて、私が学んだことはここにあります。
指数バックオフでの再試行
私たちが組み込んだ attempts と max_attempts 列は再試行を処理します。タスクが失敗すると、「失敗」状態でハングしますが、手動または自動で後で再試行できます:
async function failTask(taskId: string, error: string) {
const { data } = await supabase
.from('agent_tasks')
.select('attempts, max_attempts')
.eq('id', taskId)
.single();
const shouldRetry = data && data.attempts < data.max_attempts;
await supabase
.from('agent_tasks')
.update({
status: shouldRetry ? 'pending' : 'failed',
error,
locked_until: null,
// 指数バックオフ: 1分、4分、9分...
scheduled_for: shouldRetry
? new Date(Date.now() + Math.pow(data.attempts, 2) * 60000).toISOString()
: undefined,
updated_at: new Date().toISOString(),
})
.eq('id', taskId);
}
外部 API 用サーキット ブレーカー
外部 API がよろめくと、即座の再試行は再試行をすぐに飲み込みます。Supabase でエラー率を追跡でき、状況が悲しいように見える場合はエージェントを停止できます:
async function checkCircuitBreaker(service: string): Promise<boolean> {
const fiveMinutesAgo = new Date(Date.now() - 300000).toISOString();
const { count } = await supabase
.from('agent_logs')
.select('*', { count: 'exact', head: true })
.eq('level', 'error')
.ilike('message', `%${service}%`)
.gte('created_at', fiveMinutesAgo);
return (count ?? 0) < 10; // 5分以内に10以上のエラーの場合はトリップ
}
コスト分析と最適化
ここで私たちは本を話します。2026 年の本番エージェントアプリのコストはどのくらいですか?
| コンポーネント | 無料ティア | 典型的な本番 | ヘビーユーザー |
|---|---|---|---|
| Vercel Pro | - | $20/月 | $20/月 + 使用量 |
| Supabase Pro | - | $25/月 | $25/月 + 使用量 |
| OpenAI (GPT-4o-mini) | - | $30-80/月 | $200+/月 |
| Anthropic (Claude 3.5 Haiku) | - | $20-50/月 | $150+/月 |
| 合計 | ~$0 (開発) | $95-175/月 | $395+/月 |
これらの LLM 料金?それらはショーを実行します。蓋の上にそれらを保つ方法は次のとおりです:
- 可能なところでより小さなモデルを使用してください。 GPT-4o-mini と Claude 3.5 Haiku は、その大きな親戚の一部で、ほとんどのタスクを処理しています。
- API に到達する前にキャッシュしてください。 Supabase は LLM 応答を隠すことができ、新しい API 呼び出しを削減します。
- バッチ、バッチ、バッチ。 タスクごとに 1 つの API 呼び出しではなく、同様のタスクをバッチし、一緒に送信します。
- トークン制限?はい、お願いします。 AI SDK 呼び出し中に常に
maxTokensを設定します – 無分別な生成を防ぎます。
本番デプロイメント パターン
デプロイメント用に調理しているときに、いくつかのことが重要です:
環境の分離。 Supabase ブランチング (Pro が必要) を利用して、ステージングと本番用の異なるデータベースを保持します。Supabase ブランチを使用した Vercel プレビュー デプロイメントと組み合わせて、完全な分離を実現します。
モニタリング。 Vercel の可観測性は cron 実行履歴と関数ログを提供します。これを Supabase ヘルス チェックと組み合わせます:
SELECT
agent_type,
status,
COUNT(*) as count,
AVG(EXTRACT(EPOCH FROM (completed_at - started_at))) as avg_duration_seconds
FROM agent_tasks
WHERE created_at > NOW() - INTERVAL '24 hours'
GROUP BY agent_type, status
ORDER BY agent_type, status;
シークレット管理。 Vercel 環境変数を使用して API キーを保護し、絶対にハードコードしないでください。Supabase を使用する場合は、サーバー側でのみサービス ロール キーを使用し、クライアント側で anon キーを保持します。
このようなシステムのセットアップについてサポートが必要な場合は、当社のチームはこの正確なスタックを使用して複数のエージェント駆動型アプリケーションを提供しています。当社の ヘッドレス CMS 開発 と Next.js 開発 機能を確認するか、プロジェクトについてチャットするために お問い合わせ してください。
FAQ
Vercel Cron は高頻度エージェントタスクを処理できますか?
Vercel Cron は Pro プランで 1 分と同じ短い間隔で管理できます。ほとんどのエージェント タスクは 5~15 分のサイクルに適しています。より速い必要がありますか?Supabase Database Webhooks を検討するか、Inngest に切り替えてイベント駆動アクションを実行してください。
Cron でタスク処理の重複を防ぐにはどうすればよいですか?
それは PostgreSQL の FOR UPDATE SKIP LOCKED が大いに輝いているタスク クレーム クエリです。cron タイミングがオーバーラップしても、それはあなたの安全ネットであり、各タスクがユニークに要求されていることを保証しています。locked_until は、実行中のディザスター ストライクの場合のセキュリティの追加レイヤーを追加します。
本番エージェント ワークロード用に Supabase は信頼できますか?
絶対に。Supabase Pro は専用 Postgres インスタンス上で実行され、自動バックアップと 99.9% のアップタイム SLA があります。実際、このタスク キュー システムでは、Postgres は完全に適格です – 同時アクセスと複雑なクエリを簡単に処理できます。
本番環境で AI エージェントを実行するのにどのくらいの費用がかかりますか?
通常、1 日に 5,000~10,000 タスクを処理すると、GPT-4o-mini などのモデルを使用して、1 月あたり $95-175 (Vercel Pro + Supabase Pro) と 1 月あたり $30-80 の LLM API 呼び出しがかかります。可変層は LLM のコストです – タスクの複雑さとトークンの使用に応じます。
エージェント ロジックに LangChain または Vercel AI SDK を使用する必要がありますか?
2026 年までに、Vercel の AI SDK (v4.x) はほとんどのエージェント ニーズをネイティブにカバーしています – ツール使用、マルチステップ推論、構造化出力、ストリーミング。LangChain は、高度なチェーン オブ ソート パターン、RAG セットアップ、または専門的なツール統合を詳しく調べるときに有用になります。このスタックでは、AI SDK の複雑さが少ないほど最高です。
このパターンに代わって Astro を使用できますか?
Astro は cron トリガー API ルートを処理できますが、cron 設定やより長いタイムアウトなど、Vercel の最適化から恩恵を受けることはできません。ダッシュボードがコンテンツが多い、低インタラクティビティを好む場合は、Astro がフロントエンドに適しています。エージェント バックエンドに Next.js を使用してください。
エージェントのパフォーマンスとエラーを監視するにはどうすればよいですか?
3 つのレイヤーをブレンドします。実行レベルの洞察については Vercel のログ、アプリケーション ログ追跡については Supabase の agent_logs テーブル、動的ダッシュボード監視については Supabase Realtime。失敗、実行時間、キューの長さについてのヘルス チェックを確立してください。Vercel は PagerDuty や Slack などのアラート サービスとシームレスに同期します。
Vercel の関数タイムアウトを超えるエージェント タスクの場合はどうなりますか?
Vercel Pro では、300 秒の天井があります。タスクが頻繁にこれを超える場合は、cron サイクル全体にわたって、バイトサイズのサブタスクに分割することを検討してください。たとえば、コンテンツ プロセスを「フェッチ」、「分析」、「生成」タスクに分割します。または、Supabase Edge Functions を探索してください。150 秒の上限があり、必要に応じてトリガーできます。