AI

Vercel Connect入門——Next.jsエージェントから長期トークンをなくす

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

2026年6月21日
Vercel ConnectNext.jsFirestoreGitHub ActionsAIエージェント
Vercel Connect入門——Next.jsエージェントから長期トークンをなくす

はじめに

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へ通知するエージェント」を作るときの設計を整理します。

参考にした一次情報は次の通りです。

何が変わるのか

従来のSlack連携では、Vercelの環境変数やGitHub ActionsのSecretsにbot tokenを保存し、Route HandlerやServer Actionからその値を読んでいました。この設計は単純ですが、トークンの権限が広いほど漏えい時の被害が大きくなります。

Vercel Connectは、コネクタをVercel側に登録し、実行時に @vercel/connectgetToken で認証情報を取得する形に変えます。公式ブログでは、Vercel上のデプロイはOIDC identityで自分のプロジェクトであることを証明し、Connect側がプロジェクト・環境・コネクタの許可を確認してから短命トークンを返す、と説明されています。

考え方としては、認証情報を「置いておくもの」から「必要な瞬間に取りに行くもの」へ変えるイメージです。

項目長期トークン方式Vercel Connect方式
保存場所アプリ環境変数やCI SecretsVercel 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.postMessagePOST 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_TOKENLINEAR_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で別コネクタにできているか
subjectapp共通でよいか、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で重要になりそうです。