DELETE /user/?userId={userId}&password={password}
内部端点,无 JWT。 用 query 上的固定密码保护:password 不匹配 $INTERNAL_DELETE_PASSWORD 时返回 404(不是 401,避免暴露端点存在)。
这是内部硬删除入口(GDPR/CCPA right-to-erasure),通常由 account_deletion PGMQ worker 在 30 天倒计时后调用。不要暴露给前端。密码占位符 $INTERNAL_DELETE_PASSWORD,真值见 Notion Key 页。
| 参数 | 位置 | 说明 |
|---|
password | query | 必填,不匹配 → 404 |
userId | query | 必填,缺失 → 404 |
{
"message": "ok",
"deletedUser": {
"id": "uuid",
"email": "user@example.com",
"nickname": "Owen",
"plan": "free",
"credit": 0
}
}
deletedUser 各字段来自删除前 best-effort 预取(读不到为 null,不阻断删除)。
删除顺序(全程 idempotent,auth user 留到最后)
先清完所有下游再删 auth user —— 任何一步失败,caller retry 都安全,不会因 auth 先删导致下游 cleanup 跑不到留孤儿。
- Stripe — US(
STRIPE_SECRET_KEY)+ SG(STRIPE_SECRET_KEY_SG)两区域 customer 删除。resource_missing 已捕获不报错
- PostHog — GDPR person 删除(
delete_events=true),person 不存在自然 no-op
- Supabase 业务表 — 显式删(不依赖 FK CASCADE 的可观察性):
user_liked_feeds / user_liked_filters / feed(legacy)/ feeds_v2 / threads(按 resource_id,CASCADE 带走 thread_version、messages)/ user_profiles(主表,最后删)
- cdn per-user cascade —
cdnClient.deleteScope:R2 prefix u/{userId}/ + legacy {userId}/、feed/{userId}/ + cdn DB rows
- cdn media_tasks —
deleteTasksByOwner 清 task 索引行
- avatar 兜底 —
avatar_image_id 可能不在 u/{userId}/ 下(历史 OAuth pic / cross-owner ref),补删一刀
- Supabase auth user — 最后删主键。
User not found / user_not_found 视为已删成功(return ok);其他错误抛 500 让 worker retry
| 状态码 | 含义 |
|---|
| 404 | password 不匹配或 userId 缺失 |
| 500 | auth user 删除失败(非 already-gone) |
下游各步(Stripe/PostHog/Supabase 表/cdn/avatar)失败只 warn/error log,不阻断。
src/hono/user/index.ts(DELETE /)