Tamagui Takeout 订阅权限同步体系实战:Stripe、GitHub、Discord 三方数据一致性运维指南
【免费下载链接】tamaguiStyle React fast with 100% parity on React Native, an optional UI kit, and optimizing compiler.项目地址: https://gitcode.com/GitHub_Trending/ta/tamagui
本指南以 Tamagui 开源仓库中的 scripts/takeout/README.md 为核心,系统讲解 Takeout 商业化产品(Tamagui Pro、Takeout Stack、Team Seats)在 Stripe 订阅、Supabase 数据库、GitHub 团队授权与 Discord 角色授权四者之间的同步架构、巡检脚本、清理流程与历史故障复盘。读完本文,你将掌握如何用仓库内提供的 6 个 TypeScript 运维脚本完成三方数据一致性审计、Webhook 故障排查、月度过期 Claim 清理与 Discord 角色回收,并理解底层权限同步链路在code/tamagui.dev中的真实实现。
一、体系架构:订阅 → Claim → GitHub/Discord 的同步链路
Takeout 是 Tamagui 的商业化产品,其权限发放不再依赖人工操作,而是由一套「Stripe 订阅 + Supabase 数据库 + GitHub 团队 + Discord 角色」的自动化链路驱动。文档给出了四个核心步骤:
- 用户订阅:客户在 Stripe 中订阅 Tamagui Pro 或 Takeout Stack。
- Webhook 创建 Claim:Stripe 事件触发 Webhook,在数据库
claims表中创建一条领取记录,并写入team_slug: 'early-access'。 - 用户领取权限:用户通过官网(tamagui.dev)点击 "Claim Access",系统将其加入 GitHub 的
early-access团队。 - 订阅取消:Stripe Webhook 将对应 Claim 标记为未领取(
unclaimed_at),同时从 GitHub 团队移除用户,并回收 Discord Takeout 角色。
这一流程对应的真实实现位于 code/tamagui.dev/app/api/stripe/webhook+api.ts,它通过stripe.webhooks.constructEvent()校验签名后分发product.*、price.*、invoice.*、customer.subscription.*等事件类型;取消/续费类事件最终会调用 code/tamagui.dev/features/api/unclaimProduct.ts 中的unclaimSubscription()完成 GitHub 与 Discord 的双向回收。
授予 early-access 的三个产品
文档明确列出以下三个 Stripe 产品会授予 early-access 权限(产品 ID 与 scripts/takeout/check-discord-sync.ts 中的EARLY_ACCESS_PRODUCT_IDS常量一一对应):
| 产品 | Stripe 产品 ID | 权限说明 |
|---|---|---|
| Tamagui Pro | prod_RlRd2DVrG0frHe | 包含 takeout 访问权限 |
| Takeout Stack | prod_NzLEazaqBgoKnC | 独立的 takeout 产品 |
| Tamagui Pro Team Seats | prod_Rxu0x7jR0nWJSv | 团队订阅席位 |
值得注意的差异:check-subscription-sync.ts判断 takeout 订阅时采用的是「名称匹配 + ID 兜底 + metadata 关键字」的复合策略(name含takeout/tamagui pro,或metadata.type === 'repo'、metadata.includes_takeout === 'true'、metadata.repository_name为takeout/unistack等),而check-discord-sync.ts仅依据三个硬编码产品 ID 精确过滤。前者覆盖面更广,适合做总量审计;后者更严格,适合做 Discord 角色对账。
涉及的数据库表
整套体系依赖 Supabase 中的三张核心表(README「Related Files」一节列出):
claims:权限领取记录,核心字段包括subscription_id、product_id、data(JSON,内含user_github、team_slug、repository_name等)、created_at、unclaimed_at;discord_invites:Discord 用户与订阅的映射(discord_user_id↔subscription_id);subscriptions:本地订阅镜像表,用于通过user_id反查订阅归属用户。
二、运行前置条件与环境变量
所有脚本均为 TypeScript 源码(scripts/takeout/ 目录下共 6 个),以#!/usr/bin/env tsx作为 shebang,运行时依赖tsx、@supabase/supabase-js、stripe与@discordjs/*(core、rest、ws)等包。执行方式统一为:
npx tsx scripts/takeout/<脚本名>.ts [参数]按 README 说明,脚本所需的环境变量会自动从code/tamagui.dev/.env加载;从源码看,脚本本身直接读取process.env.*并在缺失时打印错误、process.exit(1)退出(例如 check-subscription-sync.ts)。因此运行前需确认对应环境已注入下列变量:
| 变量 | 用途 | 需要该变量的脚本 |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Supabase 项目 URL | 全部(除 check-webhook-events) |
SUPABASE_SERVICE_ROLE_KEY | Supabase 服务端密钥(service key) | 全部(除 check-webhook-events) |
STRIPE_SECRET_KEY | Stripe 密钥 | 全部 |
GITHUB_ADMIN_TOKEN | 具备 org/team 管理权限的 GitHub PAT | check-subscription-sync |
DISCORD_BOT_TOKEN | Discord Bot Token | check-discord-sync、remove-discord-users |
三、健康巡检:三个「只读审计」脚本
1. check-subscription-sync.ts:Stripe 与 GitHub 团队的对账
这是最核心的同步健康检查脚本,运行方式:
npx tsx scripts/takeout/check-subscription-sync.ts它并行拉取三类数据后交叉比对(源码见 check-subscription-sync.ts):
- 数据库活跃 Claim:查询
claims表中unclaimed_at IS NULL的记录(最多 1 万条),再按data.repository_name === 'takeout'过滤出 takeout 相关 Claim; - Stripe 活跃订阅:分页拉取所有
status: 'active'的订阅,按 takeout 产品集合过滤; - GitHub 团队成员:通过 GitHub REST API 分页拉取
https://api.github.com/orgs/tamagui/teams/early-access/members(每页 100),并统计待处理邀请.../invitations。
最终输出摘要与差异分析:对比「数据库 Claims vs Stripe 订阅」「GitHub 团队 vs 数据库 Claims」两个差值,当任一差值绝对值超过5时输出 ⚠️ 警告(提示可能存在的订阅无 Claim、已取消订阅残留 Claim、迁移不完整、手动增删成员等情况),并列出「数据库有而 Stripe 无」「Stripe 有而数据库无」的订阅 ID 前 10 条。脚本最后返回结构化摘要对象,便于程序化消费。
典型应用场景:怀疑同步异常、迁移前后验证、或例行巡检。
2. check-discord-sync.ts:Discord Takeout 角色对账
npx tsx scripts/takeout/check-discord-sync.ts该脚本将「Discord 中持有 Takeout 角色(角色 ID1131082605052301403,所属 Tamagui 服务器 ID909986013848412191)」的用户与「Stripe 活跃订阅」进行比对(源码见 check-discord-sync.ts):
- 通过
@discordjs/ws网关连接 Discord,拉取服务器内最多 1000 名成员并筛选出持有 Takeout 角色的用户; - 通过 Supabase
discord_invites表查询这些用户的subscription_id映射; - 逐一判断:无映射记录的用户归入「⚠️ 无 Supabase 映射(可能是旧账号/测试账号)」;有映射但订阅全部不活跃的用户归入「❌ 应移除」;至少有一个活跃订阅的用户为「✅ 有效」。
脚本会将应移除用户写入tmp/discord-users-to-remove-<时间戳>.json(源码中使用process.cwd()下的tmp/目录,mkdirSync(tmp, { recursive: true })自动创建),文件包含generated_at、count与users数组(含discord_id、username、global_name、inactive_subscription_ids),供后续移除脚本消费。
典型应用场景:审计 Discord 访问、找出已取消订阅却仍保留角色的过期用户。
3. check-webhook-events.ts:Webhook 事件排障
npx tsx scripts/takeout/check-webhook-events.ts [--limit 100]该脚本是纯 Stripe 侧诊断工具(源码见 check-webhook-events.ts),输出三部分信息:
- 事件类型分布:调用
stripe.events.list({ limit })拉取最近 N 条事件(--limit默认 100),按event.type分组计数排序展示; - 订阅相关事件明细:过滤
customer.subscription.*前缀的事件,最多展示最近 20 条,包含事件时间(ISO 格式)、订阅 ID、状态,并对customer.subscription.deleted标注「该订阅已取消」; - Webhook 端点状态:调用
stripe.webhookEndpoints.list()列出已配置端点,展示 URL、enabled状态与启用的事件列表,若未配置任何端点则直接警告。
典型应用场景:排查 Webhook 是否收到事件、订阅创建/取消事件是否按时到达、端点配置是否正确。
四、月度清理:分析—复核—执行三段式脚本
清理类操作统一遵循「先分析出文件 → 人工复核 → dry-run 试跑 → 正式执行」的安全流程,避免误删有效权限。
1. analyze-stale-claims.ts:过期 Claim 安全分析(只读)
npx tsx scripts/takeout/analyze-stale-claims.ts该脚本不删除任何数据,只做只读分析(源码注释明确标注 "Does NOT delete anything")。它并行拉取「Stripe 全部活跃订阅」与「Supabase 全部活跃 Claim(unclaimed_at IS NULL,最多 1 万条)」,然后逐条判定:
- 订阅 ID 不是
sub_/in_开头 → 归入「无效订阅 ID」; - 订阅 ID 存在于活跃订阅集合 → 归入「有效 Claim(保留)」;
- 否则 → 归入「过期 Claim(清理候选)」。
分析结果写入tmp/(README 记为/tmp/,源码实际为仓库工作目录下的tmp/)目录,共生成 3~4 个文件:
| 文件 | 内容 |
|---|---|
active-subscriptions-<ts>.json | 去重后的全部活跃 Stripe 订阅 ID 列表 |
valid-claims-<ts>.json | 应保留的 Claim(含 id、subscription_id、product_id、GitHub 用户名、repository_name、created_at) |
stale-claims-<ts>.json | 待清理 Claim 候选(含team_slug字段,并带 "REVIEW CAREFULLY BEFORE CLEANING UP" 警告) |
invalid-claims-<ts>.json | 仅当存在无效订阅 ID 时才生成 |
cleanup-summary-<ts>.txt | 汇总报告:总数、按 product_id / repository_name 分组的过期 Claim 分布、前 20 条样本、文件清单与后续步骤 |
汇总报告中特别强调安全原则:未删除/修改任何数据、全部数据已备份为 JSON、可与 Stripe 后台交叉核验、执行清理前必须复核。
典型应用场景:定位已取消订阅的残留 Claim,为清理做准备。
2. cleanup-stale-claims.ts:过期 Claim 批量标记
# 先试跑 npx tsx scripts/takeout/cleanup-stale-claims.ts tmp/stale-claims-*.json --dry-run # 确认无误后正式执行 npx tsx scripts/takeout/cleanup-stale-claims.ts tmp/stale-claims-*.json脚本读取分析阶段产出的stale-claims-*.json,将其中的 Claim ID 按每批 100 条分批执行supabase.from('claims').update({ unclaimed_at: new Date().toISOString() }).in('id', batch)(源码见 cleanup-stale-claims.ts)。关键安全机制:
--dry-run只打印「Would update N claims」与样本 ID,不写库;- 非 dry-run 时启动前有3 秒倒计时(
Ctrl+C可取消),给操作者最后的反悔窗口; - 分批执行并统计成功/失败数量,失败批次会打印具体错误。
典型应用场景:对已复核的过期 Claim 执行批量回收,将其标记为unclaimed。
3. remove-discord-users.ts:Discord 角色批量移除
# 先试跑 npx tsx scripts/takeout/remove-discord-users.ts tmp/discord-users-to-remove-*.json --dry-run # 确认后正式移除 npx tsx scripts/takeout/remove-discord-users.ts tmp/discord-users-to-remove-*.json脚本读取check-discord-sync.ts产出的discord-users-to-remove-*.json,逐一调用 Discord REST API(discordClient.api.guilds.removeRoleFromMember)移除用户的 Takeout 角色(源码见 remove-discord-users.ts)。两个工程化细节值得关注:
- 限速保护:
DELAY_MS = 1000,每处理一个用户后 sleep 1 秒,规避 Discord 速率限制; - 3 秒倒计时:与 cleanup 脚本一致,非 dry-run 正式执行前同样有确认缓冲期。
典型应用场景:复核discord-users-to-remove文件后,批量回收已取消订阅用户的 Discord 访问权。
五、标准运维流程速查
README 提供了四组可直接照搬的运维流程:
检查同步是否健康
npx tsx scripts/takeout/check-subscription-sync.ts npx tsx scripts/takeout/check-discord-sync.ts调试 Webhook 问题
npx tsx scripts/takeout/check-webhook-events.ts月度过期 Claim 清理
# 1. 分析 npx tsx scripts/takeout/analyze-stale-claims.ts # 2. 人工复核 tmp/ 下的分析文件 # 3. 清理 npx tsx scripts/takeout/cleanup-stale-claims.ts tmp/stale-claims-*.json月度 Discord 清理
# 1. 检查 Discord 同步 npx tsx scripts/takeout/check-discord-sync.ts # 2. 人工复核 tmp/discord-users-to-remove-*.json # 3. 移除已取消订阅的用户 npx tsx scripts/takeout/remove-discord-users.ts tmp/discord-users-to-remove-*.json六、历史故障复盘:unclaimProduct.ts 的「WHERE 缺失」事故
README 记录了 2024 年 11 月修复的一个关键线上 Bug,这是理解本套体系为何需要「巡检 + 清理」双保险的最佳案例。
问题:订阅取消时,Webhook 更新 Claim 记录却遗漏了 WHERE 子句,导致更新波及全表:
// BEFORE (BROKEN) await supabaseAdmin.from('claims').update({ unclaimed_at: Number(new Date()).toString(), }) // 这会更新数据库中 ALL 的 claims 记录!修复:为更新语句补充id条件,并修正时间戳格式为 ISO 字符串:
// AFTER (FIXED) await supabaseAdmin .from('claims') .update({ unclaimed_at: new Date().toISOString(), }) .eq('id', claim.id) // ← 补上的 WHERE 子句对照当前源码 code/tamagui.dev/features/api/unclaimProduct.ts,该修复已稳定落地:循环遍历claimRes.data,逐条update(...).eq('id', claim.id)。
同批附加修复(README 完整列出):
- 在
unclaimRepoAccess()调用前补上await,避免未等待的异步回收; - 为没有
team_slug的旧 Claim 增加回退逻辑,默认按early-access团队移除(源码 unclaimProduct.ts 中可见:team_slug非字符串时console.warn并调用removeUserFromTeam('early-access', login)); - 日期格式统一改为 ISO 字符串;
- 新增 Discord 角色移除:订阅取消时同步回收 Takeout 角色。当前实现中
unclaimDiscordAccess()会查询该订阅的discord_invites,Promise.allSettled并行移除所有关联用户的角色,随后删除discord_invites记录(见 unclaimProduct.ts)。
七、架构演进:从「仓库协作者」到「团队授权」
2024 年 11 月的另一项重要变更是授权模型整体迁移:
迁移前(Before):
- 用户被逐个添加为单个仓库的协作者(collaborator);
- Claim 记录中带有
repository_name: 'takeout'或'unistack'。
迁移后(After):
- 用户被加入统一的
early-accessGitHub 团队; - 团队统一持有所有相关仓库的访问权;
- Claim 记录写入
team_slug: 'early-access'。
这一迁移的收益显而易见:新增/回收权限只需增删团队成员即可,不必逐仓库维护协作者列表,也避免了订阅频繁变动时的仓库级权限漂移。脚本中的TEAM_SLUG = 'early-access'、ORG_NAME = 'tamagui'常量(check-subscription-sync.ts)即是该模型在当前运维工具中的直接体现。
迁移统计(README 记录):
- 邀请 197 名活跃订阅者加入 GitHub 团队;
- 移除 40 名(已取消订阅)用户的 Discord Takeout 角色;
- 清理 590 条来自已取消订阅的过期 Claim;
- 修复 Webhook Bug,防止后续再次产生过期 Claim 与 Discord 清理缺口。
八、当前状态基线(2024 年 11 月快照)
README 记录的时点数据可作为对账基线与容量参考:
| 指标 | 数值 |
|---|---|
| 活跃 Stripe 订阅 | ~370 |
| GitHub 团队成员 | ~350(活跃 + 待接受邀请) |
| Discord Takeout 角色 | ~27(仅活跃订阅) |
| Webhooks | 正常工作 |
| Claim 创建 | 正确写入team_slug |
| Claim 清理 | 取消时正确标记unclaimed |
| Discord 清理 | 取消时移除角色并删除discord_invites |
说明:以上为 README 记录的时点快照,随着业务推进数值会持续变化,建议以
check-subscription-sync.ts/check-discord-sync.ts的实时输出为准。
九、故障排查手册
「有订阅但没有 Claim」
可能原因:
- 用户尚未在官网点击 "Claim Access";
- 订阅创建时 Webhook 调用失败;
- 用户在 Claim 体系上线之前订阅。
解决方案:提示用户访问 tamagui.dev 的账户页面手动领取权限。
「用户不在 GitHub 团队」
排查步骤:
- 确认用户是否已在官网完成领取(claim);
- 运行
check-subscription-sync.ts定位缺失用户; - 用
check-webhook-events.ts验证 Webhook 是否正常工作。
「过期 Claim 持续堆积」
排查步骤:
- 确认 unclaimProduct.ts 中的 Bug 修复已部署;
- 检查 Webhook 事件是否持续收到(用
check-webhook-events.ts); - 查看 Webhook 日志中的报错。
十、相关文件索引
为便于继续深入阅读仓库源码,以下是本体系的关键实现文件:
- scripts/takeout/README.md:本指南所依据的运维文档;
- code/tamagui.dev/app/api/stripe/webhook+api.ts:Stripe Webhook 处理器(事件校验、签名验证、事件分发);
- code/tamagui.dev/features/api/unclaimProduct.ts:取消授权逻辑(GitHub 团队移除 + Discord 角色回收);
- code/tamagui.dev/features/user/claim-product.ts:Claim 创建逻辑;
- code/tamagui.dev/features/github/helpers.ts:GitHub 团队管理(含
removeUserFromTeam等); - code/tamagui.dev/features/discord/helpers.ts:Discord 客户端与常量(Takeout 角色 ID、服务器 ID);
- code/tamagui.dev/app/api/discord/channel+api.ts:Discord 频道管理;
- 数据库表:
claims、discord_invites、subscriptions。
综上,Tamagui 的 Takeout 权限体系是一个以「Stripe 为计费事实源、Supabase 为状态存储、GitHub 团队与 Discord 角色为交付载体」的闭环。运维人员只需掌握scripts/takeout/下的六个脚本,即可独立完成从日常健康巡检到月度权限回收的全部工作;而理解unclaimProduct.ts的历史 Bug 与修复,则能帮助你在未来遇到同类同步问题时快速定位根因。
【免费下载链接】tamaguiStyle React fast with 100% parity on React Native, an optional UI kit, and optimizing compiler.项目地址: https://gitcode.com/GitHub_Trending/ta/tamagui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考