Loading...
Loading...
Compare original and translation side by side
POST /api/creator-subscription/admin/users/synchttps://portaly.ai/dashboard/usersPOST /api/creator-subscription/admin/users/synchttps://portaly.ai/dashboard/usershttps://portaly.aihttps://portaly.aipcs_live_*pcs_test_*POST .../users/syncapiKeyIdpcs_live_*pcs_test_*POST .../users/syncapiKeyIdThis skill will sync your system's users to Portaly Vibe, so creators can see their users and subscription status in the Portaly Dashboard.This involves modifying your codebase:
- Reading your user model to map fields to Portaly's schema
- Creating a user dashboard (if you don't have one) with a "Sync to Portaly" button
- Adding automatic sync hooks to your registration, login, update, and deletion flows
Would you like to proceed?
此技能将把您系统中的用户同步到Portaly Vibe,以便创作者可以在Portaly仪表板中查看用户及其订阅状态。这将涉及修改您的代码库:
- 读取您的用户模型,将字段映射到Portaly的 schema
- 创建用户仪表板(如果您还没有)并添加“同步到Portaly”按钮
- 在注册、登录、更新和删除流程中添加自动同步钩子
您是否要继续?
| Portaly field | Type | Required | Description |
|---|---|---|---|
| string | Yes | Dedup key (unique per profile + api_key) |
| string | No | Vibe coder's internal user ID |
| string | No | User display name |
| enum | No | |
| string | No | User role (e.g. |
| string | No | Vibe coder's own plan label (not Portaly subscription) |
| ISO 8601 | No | Last login timestamp (e.g. |
| ISO 8601 | No | User registration timestamp in the vibe coder's system (e.g. |
| object | No | Arbitrary key-value data (max 10KB) |
| string | No | Discount/referral code captured at registration (e.g. from |
emailmetadatastatus: "deleted"| Portaly字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
| string | 是 | 去重键(每个配置文件+api_key唯一) |
| string | 否 | Vibe开发者的内部用户ID |
| string | 否 | 用户显示名称 |
| enum | 否 | |
| string | 否 | 用户角色(例如 |
| string | 否 | Vibe开发者自己的计划标签(非Portaly订阅) |
| ISO 8601 | 否 | 最后登录时间戳(例如 |
| ISO 8601 | 否 | 用户在vibe开发者系统中的注册时间戳(例如 |
| object | 否 | 任意键值对数据(最大10KB) |
| string | 否 | 注册时捕获的折扣/推荐码(例如来自 |
emailmetadatastatus: "deleted"/admin/collections/users/admin/auth/user//admin/content-manager/collection-types/plugin::users-permissions.user/wp-admin/users.php/admin/content/directus_users/admin/users/dashboard/usersI found that your app already has a user list at(Payload CMS built-in). I'll add the "Sync to Portaly" button there./admin/collections/users
I didn't find an existing page that lists all users. I'll create a simple user dashboard page so we have a place for the "Sync to Portaly" button.
/admin/users/dashboard/users/admin/collections/users/admin/auth/user//admin/content-manager/collection-types/plugin::users-permissions.user/wp-admin/users.php/admin/content/directus_users/admin/users/dashboard/users我发现您的应用已在(Payload CMS内置)有用户列表。我将在此添加“同步到Portaly”按钮。/admin/collections/users
我未找到任何列出所有用户的现有页面。我将创建一个简单的用户仪表板页面,以便放置“同步到Portaly”按钮。
/admin/users/dashboard/usersHeads up — Portaly may auto-send welcome emails on sync. Whenupserts a user, Portaly fires asyncToPortaly(orwelcome_freeif the user has an active subscription) email by default. If the vibe coder's app already sends its own welcome flow, disable the matching template before clicking the button, or the first bulk sync will explode into one duplicate email per existing user. Usewelcome_paidwithPUT /api/creator-email/templates/welcome_free— see the{ "enabled": false }skill for details.portaly-email
POST /api/admin/sync-to-portalyPOST /api/creator-subscription/admin/users/sync{ synced, created, updated, errors }<p>Click once to sync all existing users to Portaly. After that, new registrations, logins, profile updates, and deletions are synced automatically in real time. Click again only if data gets out of sync. View members on Portaly Vibe
https://portaly.ai/dashboard/userstarget="_blank" rel="noopener noreferrer"fetch('/api/users')pgmysql2const payload = await getPayload({ config }); const { docs } = await payload.find({ collection: 'users', limit: 10000 })const users = await prisma.user.findMany()const { data } = await supabase.from('users').select()const users = await User.find()const users = await db.select().from(users)syncToPortalyconst 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
}注意——同步时Portaly可能自动发送欢迎邮件。 当插入或更新用户时,Portaly默认会触发syncToPortaly(如果用户有活跃订阅则为welcome_free)邮件。如果vibe开发者的应用已发送自己的欢迎流程,请在点击按钮前禁用匹配的模板,否则首次批量同步会给每个现有用户发送一封重复邮件。使用welcome_paid并传入PUT /api/creator-email/templates/welcome_free——详情请查看{ "enabled": false }技能。portaly-email
POST /api/admin/sync-to-portalyPOST /api/creator-subscription/admin/users/sync{ synced, created, updated, errors }<p>点击一次即可将所有现有用户同步到Portaly。之后,新注册、登录、资料更新和删除操作将实时自动同步到Portaly。仅当数据不一致时才需再次点击。在Portaly Vibe查看成员
https://portaly.ai/dashboard/userstarget="_blank" rel="noopener noreferrer"fetch('/api/users')pgmysql2const payload = await getPayload({ config }); const { docs } = await payload.find({ collection: 'users', limit: 10000 })const users = await prisma.user.findMany()const { data } = await supabase.from('users').select()const users = await User.find()const users = await db.select().from(users)syncToPortalyconst 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
}afterChange// ✅ 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?ref=EARLY2026signup_ref_code?ref=?code=?promo=?coupon=portaly-paymenterrors: [{ reason: 'unknown_signup_ref_code' }]errors: [{ reason: 'signup_ref_code_already_recorded' }]afterLoginevents.signInuser_logged_inuser_logged_inlast_login_atstatus: "deleted"portaly-email/waitlist/[creatorSlug]syncToPortaly([{ email, name, status: 'active' }])/api/waitlistafterChange// ✅ 正确:同步失败不会阻塞主流程
try {
await createUser(userData) // 核心业务逻辑
} catch (err) {
return res.status(500).json({ error: '注册失败' })
}
// 触发即遗忘——仅记录错误
syncToPortaly([userData]).catch(err => console.error('[Portaly Sync]', err))
// ❌ 错误:同步失败导致整个请求失败
await createUser(userData)
await syncToPortaly([userData]) // 如果此操作失败,用户注册也会失败?ref=EARLY2026signup_ref_code?ref=?code=?promo=?coupon=portaly-paymenterrors: [{ reason: 'unknown_signup_ref_code' }]errors: [{ reason: 'signup_ref_code_already_recorded' }]afterLoginevents.signInuser_logged_inuser_logged_inlast_login_atstatus: "deleted"portaly-email/waitlist/[creatorSlug]/api/waitlistsyncToPortaly([{ email, name, status: 'active' }])undefinedundefined
Rules:
- Use ✅ if a fire-and-forget `syncToPortaly` call exists at that hook point
- Use ❌ if no sync call exists — explain why (e.g. "system has no user deletion feature" or "this endpoint is missing sync, needs to be added")
- If a hook is missing and should exist, **add it** before continuing
规则:
- 如果在该钩子点存在触发即遗忘的`syncToPortaly`调用,使用✅
- 如果不存在同步调用,使用❌并解释原因(例如“系统无用户删除功能”或“此端点缺少同步,需添加”)
- 如果钩子缺失且应存在,**添加后再继续**PORTALY_CALLBACK_SECREThttps://portaly.ai/dashboardPORTALY_API_HOST=https://portaly.ai
PORTALY_API_KEY=pcs_live_xxxxxxxxxxxxxxxxxxxxxxxx
PORTALY_CALLBACK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxpcs_test_*pcs_live_*{user dashboard path}https://portaly.ai/dashboard/users{user dashboard path}PORTALY_CALLBACK_SECREThttps://portaly.ai/dashboardPORTALY_API_HOST=https://portaly.ai
PORTALY_API_KEY=pcs_live_xxxxxxxxxxxxxxxxxxxxxxxx
PORTALY_CALLBACK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxpcs_test_*pcs_live_*{用户仪表板路径}https://portaly.ai/dashboard/users{用户仪表板路径}UNIQUE(profile_id, api_key_id, email)status: "deleted"UNIQUE(profile_id, api_key_id, email)status: "deleted".env.envreferences/api-contract.mdreferences/api-contract.md