Vercel Connect入門——Next.jsエージェントから長期トークンをなくす
Vercel ConnectのRuntime Credential Exchangeを使い、Next.js + Firestoreの業務エージェントから長期Slackトークンをなくす設計を解説します。

はじめに
AIエージェントを業務システムに組み込むと、すぐに外部サービス連携の問題が出てきます。
- Slackへ通知したいが、
SLACK_BOT_TOKENをアプリの環境変数に置き続けたくない - GitHubやLinearへアクセスする権限を、全ユーザー共通のbotトークンに寄せたくない
- Preview、Production、ローカル開発で同じ認証情報が使い回される状態を避けたい
- トークン漏えい時の影響範囲を、タスク単位で小さくしたい
2026年6月17日、Vercelは Vercel Connect をPublic Betaとして発表しました。公式発表では、アプリやエージェントが長期のプロバイダートークンを保持する代わりに、タスク実行時だけ短命でスコープ付きの認証情報を取得する仕組みとして説明されています。
本記事では、架空の問い合わせ管理SaaS「SupportDock」を題材に、Next.js App Router + Firestore + TypeScriptで「重要問い合わせをSlackへ通知するエージェント」を作るときの設計を整理します。
参考にした一次情報は次の通りです。
- Introducing Vercel Connect
- Vercel Connect documentation
- @vercel/connect - npm
- Slack chat.postMessage method
何が変わるのか
従来のSlack連携では、Vercelの環境変数やGitHub ActionsのSecretsにbot tokenを保存し、Route HandlerやServer Actionからその値を読んでいました。この設計は単純ですが、トークンの権限が広いほど漏えい時の被害が大きくなります。
Vercel Connectは、コネクタをVercel側に登録し、実行時に @vercel/connect の getToken で認証情報を取得する形に変えます。公式ブログでは、Vercel上のデプロイはOIDC identityで自分のプロジェクトであることを証明し、Connect側がプロジェクト・環境・コネクタの許可を確認してから短命トークンを返す、と説明されています。
考え方としては、認証情報を「置いておくもの」から「必要な瞬間に取りに行くもの」へ変えるイメージです。
| 項目 | 長期トークン方式 | Vercel Connect方式 |
|---|---|---|
| 保存場所 | アプリ環境変数やCI Secrets | Vercel Connectのコネクタ |
| 利用タイミング | 常に参照可能 | タスク実行時に取得 |
| 権限の考え方 | botやアプリ単位で広くなりがち | subject、scope、resourceで絞る |
| 環境分離 | 手動で別トークンを管理 | project / environment単位で接続 |
| 失効対応 | ローテーションと再デプロイが必要 | コネクタ単位でtoken revoke |
事前準備
SupportDockでは、問い合わせのうち緊急度が高いものをSlackの運用チャンネルへ通知します。まずVercel側でSlackコネクタを作ります。公式ブログで確認できるCLI例は次の形式です。
vercel connect create slack --name supportbot
ローカル開発では、Vercelプロジェクトと紐づけてから環境情報を取得します。
vercel link
vercel env pull
アプリ側にはSDKを追加します。npmに @vercel/connect が公開されていることを確認済みです。
yarn add @vercel/connect firebase-admin
Firestoreには、問い合わせ本体と通知ログを分けて保存します。
// src/types/support.ts
export type SupportTicket = {
id: string;
title: string;
priority: "low" | "normal" | "high" | "urgent";
customerPlan: "free" | "pro" | "enterprise";
assignedTeamId: string;
status: "open" | "waiting" | "resolved";
};
export type SlackNotificationLog = {
ticketId: string;
actorId: string;
connector: "slack/supportbot";
channelId: string;
result: "posted" | "failed";
createdAt: FirebaseFirestore.FieldValue;
};
ポイントは、FirestoreにSlack tokenを保存しないことです。保存するのは、どの問い合わせで、誰が、どのコネクタを使い、どのチャンネルへ通知したかという監査メタデータだけにします。
ハンズオン1: Server Actionから短命トークンを取得する
まず、重要問い合わせをSlackへ通知するServer Actionを作ります。getToken("slack/supportbot", { subject: { type: "app" } }) は、公式ブログで紹介されている基本形です。
// src/app/support/actions/escalateToSlack.ts
"use server";
import { getToken } from "@vercel/connect";
import { FieldValue } from "firebase-admin/firestore";
import { adminDb } from "@/lib/firebase/admin";
type EscalateInput = {
ticketId: string;
actorId: string;
channelId: string;
};
type SlackPostMessageResponse = {
ok: boolean;
channel?: string;
ts?: string;
error?: string;
};
export async function escalateToSlack(input: EscalateInput) {
const ticketSnap = await adminDb
.collection("supportTickets")
.doc(input.ticketId)
.get();
if (!ticketSnap.exists) {
throw new Error("Ticket not found");
}
const ticket = ticketSnap.data() as {
title: string;
priority: string;
assignedTeamId: string;
};
if (!["high", "urgent"].includes(ticket.priority)) {
throw new Error("Only high priority tickets can be escalated");
}
const token = await getToken("slack/supportbot", {
subject: { type: "app" },
});
const response = await fetch("https://slack.com/api/chat.postMessage", {
method: "POST",
headers: {
authorization: `Bearer ${token}`,
"content-type": "application/json",
},
body: JSON.stringify({
channel: input.channelId,
text: `重要問い合わせ: ${ticket.title}`,
}),
});
const result = (await response.json()) as SlackPostMessageResponse;
await adminDb.collection("slackNotificationLogs").add({
ticketId: input.ticketId,
actorId: input.actorId,
connector: "slack/supportbot",
channelId: input.channelId,
result: result.ok ? "posted" : "failed",
slackChannel: result.channel ?? null,
slackTs: result.ts ?? null,
error: result.error ?? null,
createdAt: FieldValue.serverTimestamp(),
});
if (!result.ok) {
throw new Error(result.error ?? "Slack notification failed");
}
}
Slack公式ドキュメントでは、chat.postMessage は POST https://slack.com/api/chat.postMessage にJSON bodyを送れます。channel はチャンネルIDを使う設計にしておくと、チャンネル名変更の影響を受けにくくなります。
ハンズオン2: ユーザー単位の権限へ広げる
最初は subject: { type: "app" } で十分ですが、監査要件が強い業務では「誰の権限で実行したか」が重要になります。公式発表では、ユーザー単位で認可済みのsubjectを指定する例も紹介されています。
import { getToken } from "@vercel/connect";
export async function getUserScopedLinearToken(userId: string) {
return getToken("linear/supportbot", {
subject: { type: "user", id: userId },
});
}
この形にすると、全員が同じbotとして振る舞うのではなく、ユーザーが承認した範囲のトークンで外部サービスへアクセスできます。問い合わせのステータス更新、GitHub Issue作成、Linearチケット起票のように「担当者本人の操作として残したい」処理では、app subjectとuser subjectを分ける設計が有効です。
ハンズオン3: GitHub Actionsで長期トークンを混入させない
Vercel Connectを入れても、CIに古い SLACK_BOT_TOKEN や LINEAR_API_KEY が残っていると、設計が中途半端になります。GitHub Actionsでは、ビルドに必要なSecretsと、外部サービスの長期トークンを分けて棚卸しします。
# .github/workflows/ci.yml
name: ci
on:
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: yarn
- run: yarn install --frozen-lockfile
- run: yarn build
- name: Block legacy provider tokens
run: |
if rg "SLACK_BOT_TOKEN|LINEAR_API_KEY|GITHUB_PAT" .; then
echo "Long-lived provider token names remain in the codebase."
exit 1
fi
ここで見ているのは「Secretsの値」ではなく、コードベースに残った旧設計の変数名です。実際のGitHub repository secretsは、GitHubのSettings画面やAPIで別途棚卸しします。CIでは、少なくとも新しいPRで長期トークン前提の実装が戻らないようにします。
実運用で見るポイント
Vercel ConnectはPublic Betaのため、最初から全連携を置き換えるより、権限境界が明確な1ワークフローから始めるのが現実的です。
| 観点 | 確認すること |
|---|---|
| コネクタ | Slack、GitHub、Linearなど対象プロバイダーが対応しているか |
| 環境分離 | PreviewとProductionで別コネクタにできているか |
| subject | app共通でよいか、user単位の承認が必要か |
| 監査 | Firestoreにtokenではなく実行結果とactorを残しているか |
| 失効 | vercel connect revoke-tokens の運用手順を決めているか |
公式ブログでは、トークン失効コマンドとして次の例が紹介されています。
vercel connect revoke-tokens slack/supportbot --my-tokens
vercel connect revoke-tokens slack/supportbot --all-tokens
プロバイダーによって、失効の効き方やトークン寿命、スコープ粒度は異なります。ここは抽象化に任せきらず、使うプロバイダーごとに公式ドキュメントで確認してから本番導入します。
まとめ
Vercel Connectの要点は、外部APIの権限を長期トークンとしてアプリに持たせず、必要なタスクごとに短命の認証情報として取得することです。
Next.js + Firestoreの業務アプリでは、まずSlack通知やGitHub Issue作成のような小さなエージェント処理から導入し、Firestoreには実行結果・actor・connector名だけを監査ログとして残す構成が扱いやすいです。GitHub Actions側では、古い長期トークン前提の変数名やSecretsを棚卸しし、PRで逆戻りしないチェックを入れておくと移行が進めやすくなります。
AIエージェントが実際に外部サービスを操作するほど、認証情報の扱いはアプリケーション設計そのものになります。Vercel Connectは、その境界をタスク単位に細かく切るための選択肢として、今後のAgent Stackで重要になりそうです。