portaly-user
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePortaly User Management Integration
Portaly 用户管理集成
Use this skill to help a human user integrate Portaly Vibe's User Management API. This lets creators see their users — and who is paying — in the Portaly Vibe Dashboard.
使用此技能帮助人类用户集成Portaly Vibe的用户管理API。这让创作者可以在Portaly Vibe仪表板中查看他们的用户——以及哪些用户是付费用户。
Key Concepts
核心概念
- Source of truth: The user's data lives in the vibe coder's system. Portaly Vibe is a read-only mirror + subscription status overlay.
- Sync API: Push-based. The vibe coder calls to send user data to Portaly Vibe.
POST /api/creator-subscription/admin/users/sync - Dashboard: Creators view users at . It is read-only — all changes come from the Sync API.
https://portaly.ai/dashboard/users - Subscription enrichment: Each user's row shows their Portaly subscription status (if any) as an attribute. No subscription = "Free".
- 可信数据源:用户数据存储在vibe开发者的系统中。Portaly Vibe是只读镜像+订阅状态覆盖层。
- Sync API:推送式。vibe开发者调用将用户数据发送到Portaly Vibe。
POST /api/creator-subscription/admin/users/sync - 仪表板:创作者可通过查看用户。它是只读的——所有变更均来自Sync API。
https://portaly.ai/dashboard/users - 订阅信息增强:每个用户的行项会显示其Portaly订阅状态(如有)作为属性。无订阅则显示“免费”。
API Host
API 主机
https://portaly.aihttps://portaly.aiAuthentication
身份验证
Uses the same Creator Subscription API Key ( / ).
pcs_live_*pcs_test_*- The Sync API () only accepts API Key auth (needs
POST .../users/syncto identify data ownership).apiKeyId - GET endpoints accept both API Key and Firebase JWT.
使用与Creator Subscription API相同的密钥( / )。
pcs_live_*pcs_test_*- Sync API()仅接受API密钥验证(需要
POST .../users/sync来标识数据归属)。apiKeyId - GET端点同时接受API密钥和Firebase JWT。
Workflow
工作流程
Step 1 — Consent
步骤1 — 同意授权
Before doing anything, the AI agent must ask the human user for explicit consent to proceed. Present the following and wait for the user's response before moving to Step 2:
This 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?
Do NOT continue until the user explicitly agrees. If they decline, stop here — do not proceed to any subsequent step.
在执行任何操作之前,AI代理必须明确询问人类用户是否同意继续。展示以下内容并等待用户回复后再进入步骤2:
此技能将把您系统中的用户同步到Portaly Vibe,以便创作者可以在Portaly仪表板中查看用户及其订阅状态。这将涉及修改您的代码库:
- 读取您的用户模型,将字段映射到Portaly的 schema
- 创建用户仪表板(如果您还没有)并添加“同步到Portaly”按钮
- 在注册、登录、更新和删除流程中添加自动同步钩子
您是否要继续?
在用户明确同意之前,请勿继续。 如果用户拒绝,请在此停止——不要进行后续任何步骤。
Step 2 — Map User Schema
步骤2 — 映射用户Schema
Help the vibe coder map their user fields to the Portaly schema.
Read the vibe coder's user model first (DB schema, ORM model, or type definition), then build a mapping table showing: their field → Portaly field. Ask if any fields are missing.
| 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 |
How to map: Read the vibe coder's user model, then match available fields to the Portaly schema. Only map fields that actually exist — skip any the system doesn't have. is the only required field.
email- Fields that don't fit core schema → put in
metadata - To delete a user: sync with (the record is removed from Portaly)
status: "deleted"
帮助vibe开发者将其用户字段映射到Portaly的schema。
首先读取vibe开发者的用户模型(数据库schema、ORM模型或类型定义),然后构建映射表,显示:他们的字段 → Portaly字段。询问是否有缺失的字段。
| Portaly字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
| string | 是 | 去重键(每个配置文件+api_key唯一) |
| string | 否 | Vibe开发者的内部用户ID |
| string | 否 | 用户显示名称 |
| enum | 否 | |
| string | 否 | 用户角色(例如 |
| string | 否 | Vibe开发者自己的计划标签(非Portaly订阅) |
| ISO 8601 | 否 | 最后登录时间戳(例如 |
| ISO 8601 | 否 | 用户在vibe开发者系统中的注册时间戳(例如 |
| object | 否 | 任意键值对数据(最大10KB) |
| string | 否 | 注册时捕获的折扣/推荐码(例如来自 |
映射方法:读取vibe开发者的用户模型,然后将可用字段与Portaly的schema匹配。仅映射实际存在的字段——跳过系统中没有的字段。是唯一必填字段。
email- 不符合核心schema的字段 → 放入
metadata - 删除用户:同步时设置(记录将从Portaly中移除)
status: "deleted"
Step 3 — Find Existing User Dashboard
步骤3 — 查找现有用户仪表板
Before adding any sync functionality, thoroughly search for existing pages where the user can already view all users. Many frameworks ship a built-in admin UI — do NOT build a new page if one already exists.
Where to look:
- Framework built-in admin UI — these are often auto-generated and not visible in the codebase as explicit page files:
- Payload CMS: (auto-generated from the Users collection)
/admin/collections/users - Django: (Django admin)
/admin/auth/user/ - Strapi:
/admin/content-manager/collection-types/plugin::users-permissions.user - WordPress:
/wp-admin/users.php - Directus:
/admin/content/directus_users
- Payload CMS:
- Custom-built admin pages — search the codebase for routes like ,
/admin/users, or pages that query the users table/collection/dashboard/users - Any other page that lists users — even a simple table view counts
Then tell the user what you found and what you will do:
-
If an existing user page was found → tell the user you will add the "Sync to Portaly" button to that page, then proceed to Step 4. Example:I 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 -
If no user page exists → tell the user you will build a minimal one, then proceed to Step 4. Example: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.Build a minimal page:
- A page (e.g. or
/admin/users) that lists all users from the database/dashboard/users - Use the framework and UI library the vibe coder already uses
- Show at minimum: email, display name, role, status
- Support pagination if user count could be large
- A page (e.g.
Do NOT create a new page if one already exists. The goal is to reuse what the framework provides.
在添加任何同步功能之前,彻底搜索用户已有的可查看所有用户的页面。许多框架都自带内置的管理UI——如果已有页面,请勿新建。
查找位置:
- 框架内置管理UI——这些通常是自动生成的,不会作为明确的页面文件出现在代码库中:
- Payload CMS:(从用户集合自动生成)
/admin/collections/users - Django:(Django管理后台)
/admin/auth/user/ - Strapi:
/admin/content-manager/collection-types/plugin::users-permissions.user - WordPress:
/wp-admin/users.php - Directus:
/admin/content/directus_users
- Payload CMS:
- 自定义管理页面——在代码库中搜索类似、
/admin/users的路由,或查询用户表/集合的页面/dashboard/users - 任何其他列出用户的页面——即使是简单的表格视图也算
然后告知用户您的发现及后续操作:
-
如果找到现有用户页面 → 告知用户您将在该页面添加“同步到Portaly”按钮,然后进入步骤4。示例:我发现您的应用已在(Payload CMS内置)有用户列表。我将在此添加“同步到Portaly”按钮。
/admin/collections/users -
如果未找到用户页面 → 告知用户您将构建一个极简页面,然后进入步骤4。示例:我未找到任何列出所有用户的现有页面。我将创建一个简单的用户仪表板页面,以便放置“同步到Portaly”按钮。构建极简页面:
- 一个页面(例如或
/admin/users),列出数据库中的所有用户/dashboard/users - 使用vibe开发者已在使用的框架和UI库
- 至少显示:邮箱、显示名称、角色、状态
- 如果用户数量较多,支持分页
- 一个页面(例如
如果已有页面,请勿新建。 目标是复用框架提供的现有功能。
Step 4 — Add "Sync to Portaly" Button
步骤4 — 添加“同步到Portaly”按钮
Add a "Sync to Portaly" button to the user dashboard. When clicked, it batch-syncs all users to Portaly.
Heads 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
Implementation:
-
Create an API route (e.g.) that:
POST /api/admin/sync-to-portaly- Reads all users from the database using the framework's ORM/Local API
- Maps each user to Portaly schema (using the mapping from Step 2)
- Batches into groups of 100
- Calls for each batch
POST /api/creator-subscription/admin/users/sync - Handles 429 with exponential backoff
- Returns a summary:
{ synced, created, updated, errors }
-
Add the button to the user dashboard page:
-
Label: "Sync to Portaly"
-
Helper text next to the button (required) — render this as visible UI copy so the user understands they only need to click once. Use the vibe coder's existing typography (e.g. a small, tooltip, or description element next to the button):
<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 VibeThe "View members on Portaly Vibe" text must be a clickable link pointing to(open in a new tab, e.g.https://portaly.ai/dashboard/users). Translate the helper copy into the dashboard's primary language if the rest of the UI is not in English — but keep the link destination unchanged.target="_blank" rel="noopener noreferrer" -
Show loading state while syncing
-
Show result summary (created / updated / errors) after completion
-
If there are errors, display them to the user
-
Key rules:
- Read users using the framework's Local API or ORM that directly queries the database
- Do NOT call the app's own HTTP/REST API (e.g. )
fetch('/api/users') - Do NOT install raw DB drivers (e.g. ,
pg) — use what the framework already providesmysql2 - Framework examples:
- 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)
- Payload CMS:
Batch sync helper (used by both the button and incremental sync):
Generate a function based on the mapping from Step 2. Only include fields that the vibe coder's system actually has. Below is a full example — remove any fields that don't apply:
syncToPortalytypescript
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
}For single-user incremental sync (used in Step 5), the same helper works — just pass an array with one user.
在用户仪表板中添加**“同步到Portaly”按钮。点击时,将所有**用户批量同步到Portaly。
注意——同步时Portaly可能自动发送欢迎邮件。 当插入或更新用户时,Portaly默认会触发syncToPortaly(如果用户有活跃订阅则为welcome_free)邮件。如果vibe开发者的应用已发送自己的欢迎流程,请在点击按钮前禁用匹配的模板,否则首次批量同步会给每个现有用户发送一封重复邮件。使用welcome_paid并传入PUT /api/creator-email/templates/welcome_free——详情请查看{ "enabled": false }技能。portaly-email
实现步骤:
-
创建一个API路由(例如),该路由:
POST /api/admin/sync-to-portaly- 使用框架的ORM/本地API从数据库读取所有用户
- 使用步骤2中的映射将每个用户转换为Portaly schema格式
- 按100个用户为一组分批处理
- 为每批用户调用
POST /api/creator-subscription/admin/users/sync - 处理429错误并使用指数退避重试
- 返回汇总信息:
{ synced, created, updated, errors }
-
在用户仪表板页面添加按钮:
-
标签:“同步到Portaly”
-
按钮旁的辅助文本(必填)——作为可见的UI文案呈现,让用户了解只需点击一次。使用vibe开发者已有的排版(例如按钮旁的小、工具提示或描述元素):
<p>点击一次即可将所有现有用户同步到Portaly。之后,新注册、登录、资料更新和删除操作将实时自动同步到Portaly。仅当数据不一致时才需再次点击。在Portaly Vibe查看成员“在Portaly Vibe查看成员”文本必须是指向的可点击链接(在新标签页打开,例如https://portaly.ai/dashboard/users)。如果仪表板的主要语言不是英文,请将辅助文案翻译成对应语言,但链接目标保持不变。target="_blank" rel="noopener noreferrer" -
同步过程中显示加载状态
-
完成后显示结果汇总(创建/更新/错误数量)
-
如果有错误,向用户显示错误信息
-
核心规则:
- 使用框架的本地API或ORM直接查询数据库读取用户
- 请勿调用应用自身的HTTP/REST API(例如)
fetch('/api/users') - 请勿安装原始数据库驱动(例如、
pg)——使用框架已提供的工具mysql2 - 框架示例:
- 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)
- Payload CMS:
批量同步辅助函数(按钮和增量同步均使用):
根据步骤2中的映射生成函数。仅包含vibe开发者系统中实际存在的字段。以下是完整示例——移除不适用的字段:
syncToPortalytypescript
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
}对于单用户增量同步(步骤5使用),同一个辅助函数适用——只需传入包含一个用户的数组。
Step 5 — Insert Incremental Sync Hooks
步骤5 — 插入增量同步钩子
Use the framework's hooks / event system (e.g. Payload , Prisma middleware, Mongoose post-save). The sync helper only calls the Portaly external API — it should never call the app's own API.
afterChangeCritical: All sync calls MUST be fire-and-forget.
typescript
// ✅ 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 tooWhere to insert sync calls — pass all mapped fields available at each hook point:
- User registration — after successful signup, sync the new user with all available fields. If the registration form or URL captured a referral / promo parameter (e.g. ), pass it as
?ref=EARLY2026so Portaly can auto-apply the matching discount on this user's next eligible checkout. Common URL patterns to support:signup_ref_code,?ref=,?code=,?promo=. The code must already exist in Portaly (created via the?coupon=skill); unknown codes are dropped silently withportaly-payment— the user is still synced. First-write-wins — only the first successful sync records the code; later syncs that pass a different code are dropped witherrors: [{ reason: 'unknown_signup_ref_code' }].errors: [{ reason: 'signup_ref_code_already_recorded' }] - Profile update — after successful save, sync updated fields
- Login — call sync in the framework's auth hook (e.g. Payload , NextAuth
afterLogin, Supabase auth webhooks, Djangoevents.signInsignal, Flask-Loginuser_logged_insignal) and passuser_logged_inset to the current time in ISO 8601 format. No need to store this in the vibe coder's own database — just generate the timestamp at call time and send it to Portaly.last_login_at - Account deletion — sync with to remove from Portaly
status: "deleted" - Waitlist signup — if the merchant uses the skill in self-hosted mode (Mode B), the
portaly-emailpage receives a follower's email-and-name signup. Treat that as a new user and call/waitlist/[creatorSlug]after the POST tosyncToPortaly([{ email, name, status: 'active' }])succeeds, fire-and-forget/api/waitlist
使用框架的钩子/事件系统(例如Payload 、Prisma中间件、Mongoose post-save)。同步辅助函数仅调用Portaly外部API——绝不能调用应用自身的API。
afterChange关键:所有同步调用必须是“触发即遗忘”(fire-and-forget)。
typescript
// ✅ 正确:同步失败不会阻塞主流程
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]) // 如果此操作失败,用户注册也会失败插入同步调用的位置——在每个钩子点传入所有可用的映射字段:
- 用户注册——成功注册后,同步新用户的所有可用字段。如果注册表单或URL捕获了推荐/促销参数(例如),请将其作为
?ref=EARLY2026传入,以便Portaly在该用户下次符合条件的结账时自动应用匹配的折扣。需支持的常见URL模式:signup_ref_code、?ref=、?code=、?promo=。该代码必须已在Portaly中创建(通过?coupon=技能);未知代码会被静默丢弃,返回portaly-payment——用户仍会被同步。首次写入优先——仅首次成功同步会记录该代码;后续同步传入不同代码会被丢弃,返回errors: [{ reason: 'unknown_signup_ref_code' }]。errors: [{ reason: 'signup_ref_code_already_recorded' }] - 资料更新——成功保存后,同步更新后的字段
- 登录——在框架的认证钩子中调用同步(例如Payload 、NextAuth
afterLogin、Supabase认证webhook、Djangoevents.signIn信号、Flask-Loginuser_logged_in信号),并传入设置为当前ISO 8601格式时间的user_logged_in。无需将此存储在vibe开发者的数据库中——只需在调用时生成时间戳并发送给Portaly。last_login_at - 账户删除——同步时设置以从Portaly中移除用户
status: "deleted" - 等待列表注册——如果商家在自托管模式(模式B)下使用技能,
portaly-email页面会接收关注者的邮箱和名称注册。将其视为新用户,在/waitlist/[creatorSlug]的POST请求成功后调用/api/waitlist,采用触发即遗忘模式syncToPortaly([{ email, name, status: 'active' }])
Step 6 — Verify & Done
步骤6 — 验证与完成
After implementing all sync hooks, perform a final review of the codebase, check environment variables, and present the results to the user.
实现所有同步钩子后,对代码库进行最终检查,核对环境变量,并向用户展示结果。
6a — Endpoint Checklist
6a — 端点检查清单
Review the codebase and present a checklist to the user. For each user lifecycle event, check whether a Portaly sync call exists:
undefined检查代码库并向用户展示检查清单。对于每个用户生命周期事件,检查是否存在Portaly同步调用:
undefinedPortaly Sync Endpoint Checklist
Portaly同步端点检查清单
✅ / ❌ 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}
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✅ / ❌ 用户注册 — {文件路径和行号}
原因:{说明}
✅ / ❌ 用户登录(更新last_login_at) — {文件路径和行号}
原因:{说明}
✅ / ❌ 用户资料更新 — {文件路径和行号}
原因:{说明}
✅ / ❌ 用户删除(status: "deleted") — {文件路径和行号}
原因:{说明}
规则:
- 如果在该钩子点存在触发即遗忘的`syncToPortaly`调用,使用✅
- 如果不存在同步调用,使用❌并解释原因(例如“系统无用户删除功能”或“此端点缺少同步,需添加”)
- 如果钩子缺失且应存在,**添加后再继续**6b — Next Steps & Done
6b — 后续步骤与完成
Tell the user the integration is complete, then present exactly the following action items. These are things the user must do themselves. You MUST include all three environment variables — do NOT omit .
PORTALY_CALLBACK_SECRETAction items to present (output all of these):
-
Set environment variables in your production/staging environment. Get them at. All three are required:
https://portaly.ai/dashboardPORTALY_API_HOST=https://portaly.ai PORTALY_API_KEY=pcs_live_xxxxxxxxxxxxxxxxxxxxxxxx PORTALY_CALLBACK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxkeys sync to Portaly's test environment;pcs_test_*keys sync to production.pcs_live_* -
Deploy your application — the sync hooks and "Sync to Portaly" button only work after deployment.
-
Click "Sync to Portaly" atto sync all existing users to Portaly.
{user dashboard path}
Then explain: after the first sync, no manual sync is needed for daily use. When users register, log in, update their profile, or delete their account, the system automatically syncs to Portaly in real time. The user only needs to click "Sync to Portaly" again if data is out of sync (e.g. a previous sync failed, or the database was manually modified).
Finally, point the user to the Portaly Dashboard to verify:
https://portaly.ai/dashboard/usersReplace with the actual path where the button was added.
{user dashboard path}告知用户集成已完成,然后展示以下所有操作项。这些是用户必须自行完成的事项。必须包含所有三个环境变量——请勿遗漏。
PORTALY_CALLBACK_SECRET需展示的操作项(全部输出):
-
设置环境变量到您的生产/预发布环境。可在获取。三个变量均为必填:
https://portaly.ai/dashboardPORTALY_API_HOST=https://portaly.ai PORTALY_API_KEY=pcs_live_xxxxxxxxxxxxxxxxxxxxxxxx PORTALY_CALLBACK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx密钥同步到Portaly的测试环境;pcs_test_*密钥同步到生产环境。pcs_live_* -
部署您的应用——同步钩子和“同步到Portaly”按钮仅在部署后生效。
-
**点击“同步到Portaly”**按钮,位置在,将所有现有用户同步到Portaly。
{用户仪表板路径}
然后说明:首次同步后,日常使用无需手动同步。当用户注册、登录、更新资料或删除账户时,系统会实时自动同步到Portaly。仅当数据不一致时(例如之前的同步失败,或数据库被手动修改),用户才需再次点击“同步到Portaly”。
最后,引导用户前往Portaly仪表板验证:
https://portaly.ai/dashboard/users将替换为按钮实际添加的路径。
{用户仪表板路径}Guardrails
防护规则
- Fire-and-forget: Sync API calls MUST be non-blocking. Never let a Portaly failure break the vibe coder's core business flow.
- Batch limit: Max 100 users per sync call. Split larger batches.
- Email is the dedup key: . Duplicate pushes safely upsert.
UNIQUE(profile_id, api_key_id, email) - Metadata limit: 10KB per user.
- Pacing: No rate limit in v1, but recommend 200ms delay between batches for bulk migration.
- Mode isolation: Test and live data are completely separate.
- Deletion: Sync with to remove the user from Portaly. No separate DELETE endpoint.
status: "deleted" - Sync logs: Every sync call is logged on the Portaly side. Creators can view sync history and errors in the Dashboard.
- 触发即遗忘:Sync API调用必须是非阻塞的。绝不能让Portaly的失败影响vibe开发者的核心业务流程。
- 批量限制:每次同步调用最多100个用户。拆分更大的批次。
- 邮箱为去重键:。重复推送会安全地执行插入或更新操作。
UNIQUE(profile_id, api_key_id, email) - 元数据限制:每个用户最多10KB。
- ** pacing**:v1版本无速率限制,但建议批量迁移时每批之间延迟200ms。
- 环境隔离:测试数据和生产数据完全分离。
- 删除操作:同步时设置以从Portaly中移除用户。无单独的DELETE端点。
status: "deleted" - 同步日志:每次同步调用都会在Portaly端记录。创作者可在仪表板中查看同步历史和错误。
Output Preferences
输出偏好
- Prefer code snippets over architecture explanations.
- Use the vibe coder's existing framework and language.
- Always wrap sync calls in fire-and-forget pattern.
- Show setup before any API call.
.env
- 优先使用代码片段而非架构说明。
- 使用vibe开发者已有的框架和语言。
- 始终将同步调用包装在触发即遗忘模式中。
- 在任何API调用前展示设置。
.env
Reference Documents
参考文档
- — Full API specification (5 endpoints)
references/api-contract.md
- — 完整API规范(5个端点)
references/api-contract.md