portaly-user について
portaly-userは、Portaly VibeのUser Management APIを製品のユーザーライフサイクルに組み込むためのスキルです。既存フィールドの対応付け、同期フックの追加、既存ユーザーの一括同期を支援し、開始前に明示的な同意とAPIキーの確認を求めます。
主な機能
- 01アプリのフィールドをPortalyスキーマに対応付け
- 02Portaly API経由でユーザーを同期
- 03ライフサイクル連動の差分同期フック追加
- 04既存ユーザーの一括同期
- 05同意とAPIキーの確認
活用シーン
- 01Portalyダッシュボードで製品のユーザーを表示
- 02アカウントの更新をPortalyに反映
- 03デプロイ後に既存ユーザーを同期
- 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 に同期します。
これにはコードベースの変更が伴います。
- ユーザーモデルを読み取り、フィールドを Portaly のスキーマに対応付ける
- 登録、ログイン、更新、削除の各フローに自動同期フックを追加する
- 既存ユーザーの一括バックフィルを一度だけ実行する — 私が直接実行するか、デプロイ後に実行するワンオフスクリプトをユーザーが実行する方法があります。UI ボタンは不要です
進めてもよろしいですか?
ユーザーが明示的に同意するまで、続行しないでください。 拒否された場合は、ここで停止してください。後続のステップには進まないでください。
ステップ 2 — ユーザースキーマのマッピング
バイブコーダーがユーザーフィールドを Portaly のスキーマにマッピングできるようサポートします。
まずバイブコーダーのユーザーモデルを確認し(DB スキーマ、ORM モデル、または型定義)、次に、ユーザーのフィールド → Portaly のフィールドを示すマッピング表を作成します。不足しているフィールドがないか確認してください。
| Portaly のフィールド | 型 | 必須 | 説明 |
|---|---|---|---|
email | string | はい | 重複排除キー(プロファイルごとに一意、かつ api_key と組み合わせて使用) |
external_user_id | string | いいえ | バイブコーダーの内部ユーザー ID |
display_name | string | いいえ | ユーザーの表示名 |
status | enum | いいえ | active(デフォルト)、deleted(ユーザーを削除します) |
role | string | いいえ | ユーザーのロール(例:admin、member、viewer) |
plan_name | string | いいえ | バイブコーダー独自のプランラベル(Portaly のサブスクリプションではありません) |
last_login_at | ISO 8601 | いいえ | 最終ログイン日時(例:2026-04-15T08:30:00.000Z) |
created_at | ISO 8601 | いいえ | バイブコーダーのシステムにおけるユーザー登録日時(例:2026-01-10T12:00:00.000Z) |
metadata | object | いいえ | 任意のキーと値のデータ(最大 10KB) |
signup_ref_code | string | いいえ | 登録時に取得した割引/紹介コード(例:?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)の両方で使用します。
- ユーザーのローカル環境ファイル(
.env、.env.local、.env.development、またはフレームワークの環境設定)に既存のキーがあるか確認します。 - 見つかった場合は、同期先の環境に対する正しいキーかユーザーに確認します。
pcs_test_*キーは Portaly のテスト環境に、pcs_live_*キーは本番環境に接続します。 - 見つからない場合は、ユーザーに
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、ボタン、追加のルートは不要です。
- フレームワークの Local API または ORM を使って、データベースからすべてのユーザーを読み込みます(以下の例を参照)。アプリ独自の HTTP API は呼び出さないでください。 生の DB ドライバーをインストールしないでください — フレームワークがすでに提供しているものを使ってください。
- Step 2 のマッピングを使って、各ユーザーを Portaly のスキーマにマッピングします。
- Step 4 で作成した共有モジュールから
syncToPortalyをインポートし、マッピングしたユーザーを渡して呼び出します — このヘルパーがバッチ処理、429 のバックオフ、エラー報告を処理します。 - 結果
{ 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 で実行されています)。
アクション項目:
-
本番環境またはステージング環境で環境変数を設定します。環境変数は
https://portaly.ai/dashboardで取得できます。以下の 3 つがすべて必要です。PORTALY_API_HOST=https://portaly.ai PORTALY_API_KEY=pcs_live_xxxxxxxxxxxxxxxxxxxxxxxx PORTALY_CALLBACK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxpcs_test_*キーは Portaly のテスト環境に同期され、pcs_live_*キーは本番環境に同期されます。 -
アプリケーションをデプロイします。増分同期フックはデプロイ後にのみ実行されます。
-
デプロイ後、本番環境で一度だけバックフィルを実行します。
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 件)