portaly-user

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Portaly 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
    POST /api/creator-subscription/admin/users/sync
    to send user data to Portaly Vibe.
  • Dashboard: Creators view users at
    https://portaly.ai/dashboard/users
    . It is read-only — all changes come from the Sync API.
  • 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开发者调用
    POST /api/creator-subscription/admin/users/sync
    将用户数据发送到Portaly Vibe。
  • 仪表板:创作者可通过
    https://portaly.ai/dashboard/users
    查看用户。它是只读的——所有变更均来自Sync API。
  • 订阅信息增强:每个用户的行项会显示其Portaly订阅状态(如有)作为属性。无订阅则显示“免费”。

API Host

API 主机

https://portaly.ai
https://portaly.ai

Authentication

身份验证

Uses the same Creator Subscription API Key (
pcs_live_*
/
pcs_test_*
).
  • The Sync API (
    POST .../users/sync
    ) only accepts API Key auth (needs
    apiKeyId
    to identify data ownership).
  • GET endpoints accept both API Key and Firebase JWT.
使用与Creator Subscription API相同的密钥(
pcs_live_*
/
pcs_test_*
)。
  • Sync API(
    POST .../users/sync
    仅接受API密钥验证(需要
    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:
  1. Reading your user model to map fields to Portaly's schema
  2. Creating a user dashboard (if you don't have one) with a "Sync to Portaly" button
  3. 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仪表板中查看用户及其订阅状态。
这将涉及修改您的代码库:
  1. 读取您的用户模型,将字段映射到Portaly的 schema
  2. 创建用户仪表板(如果您还没有)并添加“同步到Portaly”按钮
  3. 在注册、登录、更新和删除流程中添加自动同步钩子
您是否要继续?
在用户明确同意之前,请勿继续。 如果用户拒绝,请在此停止——不要进行后续任何步骤。

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 fieldTypeRequiredDescription
email
stringYesDedup key (unique per profile + api_key)
external_user_id
stringNoVibe coder's internal user ID
display_name
stringNoUser display name
status
enumNo
active
(default),
deleted
(removes the user)
role
stringNoUser role (e.g.
admin
,
member
,
viewer
)
plan_name
stringNoVibe coder's own plan label (not Portaly subscription)
last_login_at
ISO 8601NoLast login timestamp (e.g.
2026-04-15T08:30:00.000Z
)
created_at
ISO 8601NoUser registration timestamp in the vibe coder's system (e.g.
2026-01-10T12:00:00.000Z
)
metadata
objectNoArbitrary key-value data (max 10KB)
signup_ref_code
stringNoDiscount/referral code captured at registration (e.g. from
?ref=EARLY2026
). When this user later starts a checkout, Portaly auto-applies the matching rule once their email is verified, provided the code is still active and the per-customer cap has not been reached. First-write-wins — once recorded, subsequent syncs cannot overwrite. The code must already be created in Portaly via the
portaly-payment
skill before users register with it.
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.
email
is the only required field.
  • Fields that don't fit core schema → put in
    metadata
  • To delete a user: sync with
    status: "deleted"
    (the record is removed from Portaly)
帮助vibe开发者将其用户字段映射到Portaly的schema。
首先读取vibe开发者的用户模型(数据库schema、ORM模型或类型定义),然后构建映射表,显示:他们的字段 → Portaly字段。询问是否有缺失的字段。
Portaly字段类型必填描述
email
string去重键(每个配置文件+api_key唯一)
external_user_id
stringVibe开发者的内部用户ID
display_name
string用户显示名称
status
enum
active
(默认)、
deleted
(移除用户)
role
string用户角色(例如
admin
member
viewer
plan_name
stringVibe开发者自己的计划标签(非Portaly订阅)
last_login_at
ISO 8601最后登录时间戳(例如
2026-04-15T08:30:00.000Z
created_at
ISO 8601用户在vibe开发者系统中的注册时间戳(例如
2026-01-10T12:00:00.000Z
metadata
object任意键值对数据(最大10KB)
signup_ref_code
string注册时捕获的折扣/推荐码(例如来自
?ref=EARLY2026
)。当该用户后续开始结账时,Portaly会在其邮箱验证后自动应用匹配规则,前提是该代码仍处于激活状态且未达到单用户限额。首次写入优先——一旦记录,后续同步无法覆盖。该代码必须在用户注册前通过
portaly-payment
技能在Portaly中创建。
映射方法:读取vibe开发者的用户模型,然后将可用字段与Portaly的schema匹配。仅映射实际存在的字段——跳过系统中没有的字段。
email
是唯一必填字段。
  • 不符合核心schema的字段 → 放入
    metadata
  • 删除用户:同步时设置
    status: "deleted"
    (记录将从Portaly中移除)

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:
  1. Framework built-in admin UI — these are often auto-generated and not visible in the codebase as explicit page files:
    • Payload CMS:
      /admin/collections/users
      (auto-generated from the Users collection)
    • Django:
      /admin/auth/user/
      (Django admin)
    • Strapi:
      /admin/content-manager/collection-types/plugin::users-permissions.user
    • WordPress:
      /wp-admin/users.php
    • Directus:
      /admin/content/directus_users
  2. Custom-built admin pages — search the codebase for routes like
    /admin/users
    ,
    /dashboard/users
    , or pages that query the users table/collection
  3. 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
    /admin/collections/users
    (Payload CMS built-in). I'll add the "Sync to Portaly" button there.
  • 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.
      /admin/users
      or
      /dashboard/users
      ) that lists all users from the database
    • 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
Do NOT create a new page if one already exists. The goal is to reuse what the framework provides.
在添加任何同步功能之前,彻底搜索用户已有的可查看所有用户的页面。许多框架都自带内置的管理UI——如果已有页面,请勿新建。
查找位置
  1. 框架内置管理UI——这些通常是自动生成的,不会作为明确的页面文件出现在代码库中:
    • Payload CMS:
      /admin/collections/users
      (从用户集合自动生成)
    • Django:
      /admin/auth/user/
      (Django管理后台)
    • Strapi:
      /admin/content-manager/collection-types/plugin::users-permissions.user
    • WordPress:
      /wp-admin/users.php
    • Directus:
      /admin/content/directus_users
  2. 自定义管理页面——在代码库中搜索类似
    /admin/users
    /dashboard/users
    的路由,或查询用户表/集合的页面
  3. 任何其他列出用户的页面——即使是简单的表格视图也算
然后告知用户您的发现及后续操作
  • 如果找到现有用户页面 → 告知用户您将在该页面添加“同步到Portaly”按钮,然后进入步骤4。示例:
    我发现您的应用已在
    /admin/collections/users
    (Payload CMS内置)有用户列表。我将在此添加“同步到Portaly”按钮。
  • 如果未找到用户页面 → 告知用户您将构建一个极简页面,然后进入步骤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. When
syncToPortaly
upserts a user, Portaly fires a
welcome_free
(or
welcome_paid
if 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. Use
PUT /api/creator-email/templates/welcome_free
with
{ "enabled": false }
— see the
portaly-email
skill for details.
Implementation:
  1. Create an API route (e.g.
    POST /api/admin/sync-to-portaly
    ) that:
    • 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
      POST /api/creator-subscription/admin/users/sync
      for each batch
    • Handles 429 with exponential backoff
    • Returns a summary:
      { synced, created, updated, errors }
  2. 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
      <p>
      , tooltip, or description element next to the button):
      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
      The "View members on Portaly Vibe" text must be a clickable link pointing to
      https://portaly.ai/dashboard/users
      (open in a new tab, e.g.
      target="_blank" rel="noopener noreferrer"
      ). 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.
    • 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
    ,
    mysql2
    ) — use what the framework already provides
  • 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)
Batch sync helper (used by both the button and incremental sync):
Generate a
syncToPortaly
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:
typescript
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可能自动发送欢迎邮件。
syncToPortaly
插入或更新用户时,Portaly默认会触发
welcome_free
(如果用户有活跃订阅则为
welcome_paid
)邮件。如果vibe开发者的应用已发送自己的欢迎流程,请在点击按钮前禁用匹配的模板,否则首次批量同步会给每个现有用户发送一封重复邮件。使用
PUT /api/creator-email/templates/welcome_free
并传入
{ "enabled": false }
——详情请查看
portaly-email
技能。
实现步骤
  1. 创建一个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 }
  2. 在用户仪表板页面添加按钮:
    • 标签:“同步到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)
批量同步辅助函数(按钮和增量同步均使用)
根据步骤2中的映射生成
syncToPortaly
函数。仅包含vibe开发者系统中实际存在的字段。以下是完整示例——移除不适用的字段:
typescript
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
afterChange
, Prisma middleware, Mongoose post-save). The sync helper only calls the Portaly external API — it should never call the app's own API.
Critical: 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 too
Where 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.
    ?ref=EARLY2026
    ), pass it as
    signup_ref_code
    so Portaly can auto-apply the matching discount on this user's next eligible checkout. Common URL patterns to support:
    ?ref=
    ,
    ?code=
    ,
    ?promo=
    ,
    ?coupon=
    . The code must already exist in Portaly (created via the
    portaly-payment
    skill); unknown codes are dropped silently with
    errors: [{ reason: 'unknown_signup_ref_code' }]
    — 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 with
    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
    afterLogin
    , NextAuth
    events.signIn
    , Supabase auth webhooks, Django
    user_logged_in
    signal, Flask-Login
    user_logged_in
    signal) and pass
    last_login_at
    set 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.
  • Account deletion — sync with
    status: "deleted"
    to remove from Portaly
  • Waitlist signup — if the merchant uses the
    portaly-email
    skill in self-hosted mode (Mode B), the
    /waitlist/[creatorSlug]
    page receives a follower's email-and-name signup. Treat that as a new user and call
    syncToPortaly([{ email, name, status: 'active' }])
    after the POST to
    /api/waitlist
    succeeds, fire-and-forget
使用框架的钩子/事件系统(例如Payload
afterChange
、Prisma中间件、Mongoose post-save)。同步辅助函数仅调用Portaly外部API——绝不能调用应用自身的API。
关键:所有同步调用必须是“触发即遗忘”(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
    ),请将其作为
    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认证webhook、Django
    user_logged_in
    信号、Flask-Login
    user_logged_in
    信号),并传入设置为当前ISO 8601格式时间的
    last_login_at
    。无需将此存储在vibe开发者的数据库中——只需在调用时生成时间戳并发送给Portaly。
  • 账户删除——同步时设置
    status: "deleted"
    以从Portaly中移除用户
  • 等待列表注册——如果商家在自托管模式(模式B)下使用
    portaly-email
    技能,
    /waitlist/[creatorSlug]
    页面会接收关注者的邮箱和名称注册。将其视为新用户,在
    /api/waitlist
    的POST请求成功后调用
    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同步调用:
undefined

Portaly 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_SECRET
.
Action items to present (output all of these):
  1. Set environment variables in your production/staging environment. Get them at
    https://portaly.ai/dashboard
    . All three are required:
    PORTALY_API_HOST=https://portaly.ai
    PORTALY_API_KEY=pcs_live_xxxxxxxxxxxxxxxxxxxxxxxx
    PORTALY_CALLBACK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
    pcs_test_*
    keys sync to Portaly's test environment;
    pcs_live_*
    keys sync to production.
  2. Deploy your application — the sync hooks and "Sync to Portaly" button only work after deployment.
  3. Click "Sync to Portaly" at
    {user dashboard path}
    to sync all existing users to Portaly.
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/users
Replace
{user dashboard path}
with the actual path where the button was added.
告知用户集成已完成,然后展示以下所有操作项。这些是用户必须自行完成的事项。必须包含所有三个环境变量——请勿遗漏
PORTALY_CALLBACK_SECRET
需展示的操作项(全部输出)
  1. 设置环境变量到您的生产/预发布环境。可在
    https://portaly.ai/dashboard
    获取。三个变量均为必填:
    PORTALY_API_HOST=https://portaly.ai
    PORTALY_API_KEY=pcs_live_xxxxxxxxxxxxxxxxxxxxxxxx
    PORTALY_CALLBACK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxx
    pcs_test_*
    密钥同步到Portaly的测试环境;
    pcs_live_*
    密钥同步到生产环境。
  2. 部署您的应用——同步钩子和“同步到Portaly”按钮仅在部署后生效。
  3. **点击“同步到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:
    UNIQUE(profile_id, api_key_id, email)
    . Duplicate pushes safely upsert.
  • 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
    status: "deleted"
    to remove the user from Portaly. No separate DELETE endpoint.
  • 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。
  • 环境隔离:测试数据和生产数据完全分离。
  • 删除操作:同步时设置
    status: "deleted"
    以从Portaly中移除用户。无单独的DELETE端点。
  • 同步日志:每次同步调用都会在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
    .env
    setup before any API call.
  • 优先使用代码片段而非架构说明。
  • 使用vibe开发者已有的框架和语言。
  • 始终将同步调用包装在触发即遗忘模式中。
  • 在任何API调用前展示
    .env
    设置。

Reference Documents

参考文档

  • references/api-contract.md
    — Full API specification (5 endpoints)
  • references/api-contract.md
    — 完整API规范(5个端点)