mcpField
P
portaly-user

portaly-user

github.com/portaly-aiデータ・データベース264 回インストール3 日前に更新
リポジトリ

portaly-user について

portaly-userは、Portaly VibeのUser Management APIを製品のユーザーライフサイクルに組み込むためのスキルです。既存フィールドの対応付け、同期フックの追加、既存ユーザーの一括同期を支援し、開始前に明示的な同意とAPIキーの確認を求めます。

主な機能

  1. 01アプリのフィールドをPortalyスキーマに対応付け
  2. 02Portaly API経由でユーザーを同期
  3. 03ライフサイクル連動の差分同期フック追加
  4. 04既存ユーザーの一括同期
  5. 05同意とAPIキーの確認

活用シーン

  1. 01Portalyダッシュボードで製品のユーザーを表示
  2. 02アカウントの更新をPortalyに反映
  3. 03デプロイ後に既存ユーザーを同期
  4. 04登録時の紹介コードをユーザーに関連付け

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/portaly-ai/portaly-skills --skill portaly-user

インストール後、Claude Code で/portaly-userを入力して読み込まれたか確認します。

同梱ファイルreferences/api-contract.mdscripts/sync_user.mjs

詳細

Portaly ユーザー管理インテグレーション

このスキルを使って、人間のユーザーが Portaly Vibe のユーザー管理 API を統合できるよう支援します。これにより、クリエイターは Portaly Vibe ダッシュボードでユーザーと、そのうち誰が支払いをしているかを確認できます。

主な概念

  • 信頼できる唯一の情報源:ユーザーデータは、Vibe コーダーのシステムに保存されています。Portaly Vibe は読み取り専用のミラーであり、サブスクリプション状況が付加されます。
  • 同期 API:プッシュ方式です。Vibe コーダーは POST /api/creator-subscription/admin/users/sync を呼び出して、ユーザーデータを Portaly Vibe に送信します。
  • ダッシュボード:クリエイターは https://portaly.ai/dashboard/members でユーザーを確認できます。読み取り専用で、変更はすべて Sync API 経由で行われます。
  • サブスクリプション情報の付加:各ユーザーの行には、Portaly のサブスクリプション状況(該当する場合)が属性として表示されます。サブスクリプションがない場合は「Free」と表示されます。

API ホスト

https://portaly.ai(デフォルト。PORTALY_API_HOST 環境変数で上書きできます)。

Portaly API を呼び出すコードを生成するときは、URL をハードコーディングするのではなく、次のパターンを使用してください。

const PORTALY_API_HOST = process.env.PORTALY_API_HOST || 'https://portaly.ai'

scripts/sync_user.mjs のヘルパー関数は、すでにこのパターンに従っています。バックエンドの互換性に関する仕様については、リポジトリのルートにある PROVIDER.md を参照してください。

認証

Creator Subscription API Key(pcs_live_* / pcs_test_*)を使用します。

  • Sync API(POST .../users/sync)で受け付けるのは API Key 認証のみです(データの所有者を特定するために apiKeyId が必要です)。
  • GET エンドポイントでは、API Key と Firebase JWT の両方を使用できます。

ワークフロー

ステップ 1 — 同意

何かを行う前に、AI エージェントは作業を進めることについて、人間のユーザーに明示的な同意を必ず求めてください。以下を提示し、ステップ 2 に進む前にユーザーの返答を待ってください。

このスキルは、クリエイターが Portaly Dashboard でユーザーとサブスクリプション状況を確認できるように、お使いのシステムのユーザーを Portaly Vibe に同期します。

これにはコードベースの変更が伴います。

  1. ユーザーモデルを読み取り、フィールドを Portaly のスキーマに対応付ける
  2. 登録、ログイン、更新、削除の各フローに自動同期フックを追加する
  3. 既存ユーザーの一括バックフィルを一度だけ実行する — 私が直接実行するか、デプロイ後に実行するワンオフスクリプトをユーザーが実行する方法があります。UI ボタンは不要です

進めてもよろしいですか?

ユーザーが明示的に同意するまで、続行しないでください。 拒否された場合は、ここで停止してください。後続のステップには進まないでください。

ステップ 2 — ユーザースキーマのマッピング

バイブコーダーがユーザーフィールドを Portaly のスキーマにマッピングできるようサポートします。

まずバイブコーダーのユーザーモデルを確認し(DB スキーマ、ORM モデル、または型定義)、次に、ユーザーのフィールド → Portaly のフィールドを示すマッピング表を作成します。不足しているフィールドがないか確認してください。

Portaly のフィールド型必須説明
emailstringはい重複排除キー(プロファイルごとに一意、かつ api_key と組み合わせて使用)
external_user_idstringいいえバイブコーダーの内部ユーザー ID
display_namestringいいえユーザーの表示名
statusenumいいえactive(デフォルト)、deleted(ユーザーを削除します)
rolestringいいえユーザーのロール(例:admin、member、viewer)
plan_namestringいいえバイブコーダー独自のプランラベル(Portaly のサブスクリプションではありません)
last_login_atISO 8601いいえ最終ログイン日時(例:2026-04-15T08:30:00.000Z)
created_atISO 8601いいえバイブコーダーのシステムにおけるユーザー登録日時(例:2026-01-10T12:00:00.000Z)
metadataobjectいいえ任意のキーと値のデータ(最大 10KB)
signup_ref_codestringいいえ登録時に取得した割引/紹介コード(例:?ref=EARLY2026 から取得)。このユーザーが後でチェックアウトを開始すると、メールアドレスが確認済みで、コードが引き続き有効かつ顧客ごとの上限に達していない場合、Portaly は一致するルールを自動適用します。初回書き込みを優先 — 一度記録されると、以降の同期では上書きできません。ユーザーがこのコードを使って登録する前に、portaly-payment スキルを使って Portaly でコードを作成しておく必要があります。

マッピング方法: バイブコーダーのユーザーモデルを読み、利用できるフィールドを Portaly のスキーマに対応させます。実際に存在するフィールドのみマッピングし、システムにないフィールドはスキップします。必須フィールドは email のみです。

  • コアスキーマに合わないフィールドは metadata に入れます
  • ユーザーを削除するには、status: "deleted" を指定して同期します(レコードは Portaly から削除されます)
ステップ 3 — API キーを確認し、初回同期方法を選択する

コードを書く前に、人間のユーザーと 2 つのことを決めてください。両方が決まるまでステップ 4 に進んではいけません。

3a — PORTALY_API_KEY が利用可能か確認する

このスキルが Sync API を呼び出すには PORTALY_API_KEY が必要です。増分フック(ステップ 4)と初回のバックフィル(ステップ 5)の両方で使用します。

  1. ユーザーのローカル環境ファイル(.env、.env.local、.env.development、またはフレームワークの環境設定)に既存のキーがあるか確認します。
  2. 見つかった場合は、同期先の環境に対する正しいキーかユーザーに確認します。pcs_test_* キーは Portaly のテスト環境に、pcs_live_* キーは本番環境に接続します。
  3. 見つからない場合は、ユーザーに https://portaly.ai/dashboard からキーを取得して貼り付けてもらいます。受け取ったら、適切なローカル環境ファイルに保存します。

キーが利用できない場合は、ここで停止してください。 キーなしではこのスキルを実行できません。キーを用意してからスキルを再実行するようユーザーに伝えてください。

3b — 初回同期の方法を選ぶ

実際のユーザーがどこにいるか、ユーザーに尋ねてください。その回答によって、一度限りのバックフィルの実行方法が決まります。以下のプロンプトをそのまま使用してください(会話の言語に翻訳してください)。

初回同期では、既存ユーザー全員を一度 Portaly に送信する必要があります。方法は 2 つあります。

  • オプション A — 今すぐローカル環境から実行する。 ローカルのコードが接続しているデータベースからユーザーを読み取り、Portaly API を直接呼び出します。同期したいユーザーがローカル DB にある場合に選んでください(例:本番環境に直接接続して開発している場合、または本番データをすでにローカルに読み込んでいる場合)。
  • オプション B — デプロイ後に本番環境で実行する、一度限りのスクリプトを作成する。 scripts/sync-portaly-once.mjs(または .ts)を作成します。PORTALY_API_KEY を設定してデプロイした後、本番環境でスクリプトを一度実行します。開発 DB と本番 DB が別々の場合に選んでください。

どちらが現在の環境に合っていますか?

続行する前に、ユーザーの選択を待ってください。 ステップ 5 では、この回答に応じて処理が分岐します。

Step 4 — 同期ヘルパーとインクリメンタルフックを追加する

syncToPortaly ヘルパーを追加し、フレームワークのユーザーライフサイクルフックに組み込みます。同じヘルパーは、Step 5 の初期同期でも再利用します。

一括同期ヘルパー:

Step 2 のマッピングに基づいて、syncToPortaly 関数を生成します。バイブコーダーのシステムに実際にあるフィールドのみを含めます。以下は完全な例です。該当しないフィールドは削除してください:

const PORTALY_API_KEY = process.env.PORTALY_API_KEY
const PORTALY_API_HOST = process.env.PORTALY_API_HOST || 'https://portaly.ai'

async function syncToPortaly(users: Array<{
  email: string;
  id?: string | number;       // → external_user_id
  name?: string;              // → display_name
  role?: string;              // → role
  planName?: string;          // → plan_name
  lastLoginAt?: Date | null;  // → last_login_at (ISO 8601)
  createdAt?: Date | null;    // → created_at (ISO 8601)
  status?: string;            // → status ('active' or 'deleted')
  metadata?: Record<string, unknown>;
  signupRefCode?: string;     // → signup_ref_code (only on initial registration sync)
}>) {
  const BATCH_SIZE = 100
  const results = { synced: 0, created: 0, updated: 0, errors: [] as any[] }

  for (let i = 0; i < users.length; i += BATCH_SIZE) {
    const batch = users.slice(i, i + BATCH_SIZE)
    const payload = batch.map(user => ({
      email: user.email,
      external_user_id: user.id != null ? String(user.id) : undefined,
      display_name: user.name,
      role: user.role,
      plan_name: user.planName,
      last_login_at: user.lastLoginAt?.toISOString(),
      created_at: user.createdAt?.toISOString(),
      status: user.status || 'active',
      metadata: user.metadata,
      signup_ref_code: user.signupRefCode,
    }))

    try {
      const res = await fetch(
        `${PORTALY_API_HOST}/api/creator-subscription/admin/users/sync`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${PORTALY_API_KEY}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ users: payload }),
        }
      )

      if (res.status === 429) {
        const retryAfter = parseInt(res.headers.get('Retry-After') || '5', 10)
        await new Promise(r => setTimeout(r, retryAfter * 1000))
        i -= BATCH_SIZE // retry this batch
        continue
      }

      if (!res.ok) {
        const text = await res.text()
        throw new Error(`API error ${res.status}: ${text}`)
      }

      const { data } = await res.json()
      results.synced += data.synced
      results.created += data.created
      results.updated += data.updated
      results.errors.push(...data.errors)
    } catch (err) {
      results.errors.push({ batch: i / BATCH_SIZE + 1, reason: String(err) })
    }
  }

  return results
}

このヘルパーは共有モジュールに配置します — 例:lib/portaly-user-sync.ts(プロジェクトの規約に合わせて調整) — syncToPortaly をエクスポートします。同じモジュールを、以下のインクリメンタルフックと Step 5 の初期同期エントリーポイントの両方でインポートします。このヘルパーは、単一ユーザーの呼び出し(要素数が 1 の配列を渡す)と一括処理を同じように扱います。

インクリメンタルフックを挿入する:

フレームワークのフック/イベントシステム(例:Payload の afterChange、Prisma ミドルウェア、Mongoose の post-save)を使います。同期ヘルパーが呼び出すのはPortaly の外部 APIのみです。アプリ独自の API は決して呼び出さないでください。

重要:同期の呼び出しはすべて、必ず fire-and-forget にしてください。

// ✅ Correct: sync failure does not block the main flow
try {
  await createUser(userData) // main business logic
} catch (err) {
  return res.status(500).json({ error: 'Registration failed' })
}
// fire-and-forget — only log errors
syncToPortaly([userData]).catch(err => console.error('[Portaly Sync]', err))

// ❌ Wrong: sync failure causes the whole request to fail
await createUser(userData)
await syncToPortaly([userData]) // if this fails, user registration fails too

同期呼び出しを挿入する場所 — 各フックで利用可能なマッピング済みフィールドをすべて渡します:

  • ユーザー登録 — 登録の成功後、新しいユーザーを利用可能なすべてのフィールドとともに同期します。登録フォームまたは URL で紹介/プロモーションパラメーター(例:?ref=EARLY2026)を取得している場合は、signup_ref_code として渡してください。これにより Portaly は、このユーザーが次回対象となるチェックアウトを行う際に、一致する割引を自動適用できます。対応する一般的な URL パターン:?ref=、?code=、?promo=、?coupon=。コードは Portaly にすでに存在している必要があります(portaly-payment スキルで作成)。不明なコードは errors: [{ reason: 'unknown_signup_ref_code' }] とともに黙って破棄されます。ユーザーの同期は引き続き行われます。初回書き込みが優先されます — 最初に成功した同期でのみコードが記録されます。その後の同期で異なるコードを渡した場合は、errors: [{ reason: 'signup_ref_code_already_recorded' }] とともに破棄されます。
  • プロフィールの更新 — 保存の成功後、更新されたフィールドを同期します
  • ログイン — フレームワークの認証フック(例:Payload の afterLogin、NextAuth の events.signIn、Supabase 認証ウェブフック、Django の user_logged_in シグナル、Flask-Login の user_logged_in シグナル)で同期を呼び出し、現在の時刻を ISO 8601 形式で設定した last_login_at を渡します。これをバイブコーダー独自のデータベースに保存する必要はありません。呼び出し時にタイムスタンプを生成して Portaly に送信するだけです。
  • アカウント削除 — Portaly から削除するため、status: "deleted" とともに同期します
  • ウェイトリスト登録 — セルフホストモード(Mode B)でマーチャントが portaly-email スキルを使っている場合、/waitlist/[creatorSlug] ページでは、フォロワーがメールアドレスと名前を登録します。新しいユーザーとして扱い、/api/waitlist への POST が成功した後に syncToPortaly([{ email, name, status: 'active' }]) を fire-and-forget で呼び出します
Step 5 — 初期同期を実行する

ご注意 — この同期で Portaly がウェルカムメールを自動送信する場合があります。 syncToPortaly がユーザーを upsert すると、Portaly はデフォルトで welcome_free メールを送信します(ユーザーが有効なサブスクリプションを持つ場合は welcome_paid)。バイブコーダーのアプリがすでに独自のウェルカムフローを送信している場合は、初期同期を実行する前に、対応するテンプレートを無効にしてください。そうしないと、バックフィルによって既存ユーザーごとに重複メールが 1 通ずつ送信されます。無効にするには、GET /api/creator-email/templates/welcome_free で現在のテンプレートを取得し、同じペイロードを enabled: false にして PUT します(エンドポイントでは subject、greeting、body が空でない文字列として必須です。{ enabled: false } だけを送信することはできません)。welcome_paid も同じ手順です。詳細は portaly-email スキルを参照してください。

Step 3b で選択したパスに応じて分岐します。

オプション A — 今すぐローカル環境から実行

エージェントがバックフィルを直接実行します。UI、ボタン、追加のルートは不要です。

  1. フレームワークの Local API または ORM を使って、データベースからすべてのユーザーを読み込みます(以下の例を参照)。アプリ独自の HTTP API は呼び出さないでください。 生の DB ドライバーをインストールしないでください — フレームワークがすでに提供しているものを使ってください。
  2. Step 2 のマッピングを使って、各ユーザーを Portaly のスキーマにマッピングします。
  3. Step 4 で作成した共有モジュールから syncToPortaly をインポートし、マッピングしたユーザーを渡して呼び出します — このヘルパーがバッチ処理、429 のバックオフ、エラー報告を処理します。
  4. 結果 { synced, created, updated, errors } をユーザーに表示します。errors が空でない場合は、エラーを提示して再試行を提案します。

フレームワークごとの読み込み例:

  • Payload CMS:const payload = await getPayload({ config }); const { docs } = await payload.find({ collection: 'users', limit: 10000 })
  • Prisma:const users = await prisma.user.findMany()
  • Supabase:const { data } = await supabase.from('users').select()
  • Mongoose:const users = await User.find()
  • Drizzle:const users = await db.select().from(users)

プロジェクトですでに使われている node、tsx、またはその他のアドホックなランナーで実行します。非常に大規模なデータセット(数万件のユーザー)の場合は、実行に時間がかかる可能性があることをユーザーに伝えてください — 実行間隔の推奨事項については、以下のガードレールを参照してください。

オプション B — 一度だけ実行するスクリプトを生成

script/sync-portaly-once.mjs(または、プロジェクトが TypeScript を優先する場合は .ts)を作成します。スクリプトは次の要件を満たす必要があります。

  • Step 4 で作成した共有モジュールから syncToPortaly をインポートする(ヘルパーをインラインで重複させないでください — 信頼できる情報源を一つに保つことで、差異を防げます)
  • オプション A の例と同じフレームワーク ORM/Local API を使って、すべてのユーザーを読み込む
  • Step 2 に従ってマッピングし、syncToPortaly(users) を呼び出す
  • 結果 { synced, created, updated, errors } を表示し、errors が空でない場合は 0 以外の終了コードで終了する
  • 環境変数から PORTALY_API_KEY と PORTALY_API_HOST を読み込む(ハードコードしない)

次に、ユーザーに次の内容をそのまま伝えてください(会話の言語に翻訳してください)。

スクリプトは scripts/sync-portaly-once.mjs にあります。本番環境で PORTALY_API_KEY を設定してデプロイした後、本番環境で一度実行してください。

PORTALY_API_KEY=pcs_live_xxx node scripts/sync-portaly-once.mjs

実行は一度だけで十分です。その後は Step 4 で追加した差分同期フックによって、Portaly が自動的に同期されます。データにずれが生じた場合(例:以前の同期が失敗した、またはアプリ外で DB が変更された場合)にのみ再実行してください。

Step 6 — 検証して完了

すべての同期フックを実装したら、コードベースの最終レビューを行い、環境変数を確認して、結果をユーザーに提示します。

6a — エンドポイントチェックリスト

コードベースを確認し、チェックリストをユーザーに提示します。各ユーザーのライフサイクルイベントについて、Portaly の同期呼び出しがあるか確認してください。

## Portaly Sync Endpoint Checklist

✅ / ❌ User registration — {file path and line}
   Reason: {why}
✅ / ❌ User login (update last_login_at) — {file path and line}
   Reason: {why}
✅ / ❌ User profile update — {file path and line}
   Reason: {why}
✅ / ❌ User deletion (status: "deleted") — {file path and line}
   Reason: {why}

ルール:

  • そのフック箇所に fire-and-forget の syncToPortaly 呼び出しがある場合は ✅ を使います
  • 同期呼び出しがない場合は ❌ を使い、理由を説明します(例:「システムにユーザー削除機能がない」または「このエンドポイントには同期処理がないため、追加が必要」)
  • フックが不足しており、存在すべき場合は、続行する前に追加してください
6b — 次のステップと完了

連携が完了したことをユーザーに伝えたうえで、次のアクション項目を提示します。これらはユーザー自身が行う必要があります。3 つの環境変数をすべて含めてください。PORTALY_CALLBACK_SECRET を省略してはいけません。 項目 3 は、ステップ 3b でオプション B を選んだユーザーにのみ適用されます。オプション A の場合は完全に省略してください(初回同期はすでにステップ 5 で実行されています)。

アクション項目:

  1. 本番環境またはステージング環境で環境変数を設定します。環境変数は https://portaly.ai/dashboard で取得できます。以下の 3 つがすべて必要です。

    PORTALY_API_HOST=https://portaly.ai
    PORTALY_API_KEY=pcs_live_xxxxxxxxxxxxxxxxxxxxxxxx
    PORTALY_CALLBACK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
    

    pcs_test_* キーは Portaly のテスト環境に同期され、pcs_live_* キーは本番環境に同期されます。

  2. アプリケーションをデプロイします。増分同期フックはデプロイ後にのみ実行されます。

  3. デプロイ後、本番環境で一度だけバックフィルを実行します。

    PORTALY_API_KEY=pcs_live_xxx node scripts/sync-portaly-once.mjs
    

その後、次のように説明します。初回同期の後は、日常的な利用で手動操作は不要です。 ユーザーが登録、ログイン、プロフィールの更新、またはアカウントの削除を行うと、システムが Portaly にリアルタイムで自動同期します。スクリプトを再実行する(オプション B)か、スキルの初回同期を再実行する(オプション A)のは、データに不整合が生じた場合のみです。たとえば、以前の同期に失敗した場合や、アプリの外部でデータベースが変更された場合です。

最後に、同期を確認するため、ユーザーに Portaly Dashboard を案内します:https://portaly.ai/dashboard/members

ガードレール

  • ファイア・アンド・フォーゲット:同期 API 呼び出しは必ずノンブロッキングにしてください。Portaly の障害によって、バイブコーダーの主要なビジネスフローが停止しないようにしてください。
  • バッチ上限:1 回の同期呼び出しにつき最大 100 ユーザーです。これを超える場合は分割してください。
  • 重複排除キーはメールアドレス:UNIQUE(profile_id, api_key_id, email)。重複して送信しても安全にアップサートされます。
  • メタデータ上限:ユーザー 1 人あたり 10KB です。
  • 実行間隔:v1 ではレート制限はありませんが、一括移行ではバッチ間に 200ms の間隔を設けることを推奨します。
  • モードの分離:テストデータと本番データは完全に分離されています。
  • 削除:status: "deleted" で同期すると、Portaly からユーザーが削除されます。個別の DELETE エンドポイントはありません。
  • 同期ログ:すべての同期呼び出しが Portaly 側で記録されます。クリエイターは Dashboard で同期履歴とエラーを確認できます。

出力の優先事項

  • アーキテクチャの説明より、コードスニペットを優先してください。
  • バイブコーダーが既存のフレームワークと言語を使用してください。
  • 同期呼び出しは常にファイア・アンド・フォーゲットパターンでラップしてください。
  • API 呼び出しより前に .env の設定を示してください。

参考ドキュメント

  • references/api-contract.md — API 仕様の全体(エンドポイント 5 件)

コメント