AhsanAyaz / AhsanAyaz/code-with-ahsan

Ghost ingested Focusdoro Stripe subscriptions as blog paid members

已关闭
#332 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
主要语言
TypeScript
星标
27
派生
53
平均合并
3 小时 46 分钟
30 天内合并 PR
13

描述

## Summary

Ghost (blog.codewithahsan.dev) was connected to the shared VisionWise AB Stripe account. Ghost's Stripe integration subscribes to the **entire account**, not to a specific product or price, so every subscription created by any property on that account was ingested by Ghost as its own paid member.

Between 2026-07-30 and 2026-09-13 this turned 6 Focusdoro Pro customers into phantom "AHSYNC BYTES" paid members on the blog. Resolved on 2026-09-13. This issue records the constraint so the trap is not re-armed.

## How it showed up

Ghost analytics reported `Paid members 5`, `MRR $15`, `400%` growth. None of it was real:

- The blog has never had a paid-gated post. Only 3 non-public posts exist, all `members` (free tier) visibility, newest from 2026-05-30.
- All 5 "paid members" were created in the same second as a Stripe `customer.subscription.created`, source `stripe`, with no prior signup event and no attribution.
- Ghost's AHSYNC BYTES tier is configured at $5/mo, but every subscription reported $3/mo. $3 is the Focusdoro Pro monthly price (`price_1T5LZOGm1lQTGSK4pO9AHja5`).

Cross-checked all 5 emails against Focusdoro prod (`focusdoro-acd23`): 5/5 were real Focusdoro Auth users with `stripeCustomerId`, `subscriptionId` and `priceId` in `users/{uid}`, each created seconds to minutes before the Ghost member record.

## Impact

1. **Newsletter sent without consent.** 4 of 5 were auto-subscribed to AHSYNC BYTES. A newsletter was delivered to 3 of them on 2026-09-09 and opened by 2. One unsubscribed themselves 59 minutes after opening. These people bought a Pomodoro app and received a dev newsletter they never signed up for. VisionWise AB is a Swedish entity, so this is a consent problem, not just untidy data.
2. **Revenue double-counted.** Focusdoro MRR appeared as blog MRR.
3. **Cancellation risk.** Ghost believed it owned those subscriptions. A member delete with `cancel=true`, or a Stripe disconnect, could have cancelled 5 live Focusdoro subscriptions.

## Root cause

Ghost Stripe Connect has no per-product or per-price filter. It registers one webhook destination and treats every subscription event on the connected account as its own. Five properties share this Stripe account:

```
enabled https://www.codewithahsan.dev/api/webhooks/stripe
disabled https://blog.codewithahsan.dev/members/webhooks/stripe/ <- Ghost, disabled 2026-09-13
enabled https://focusdoro.app/api/stripe/webhook
enabled https://www.z2website.com/api/stripe/webhook
enabled https://www.blurjoy.com/api/webhooks/stripe
```

Focusdoro's own billing code is correct and was never at fault: its own price IDs, its own webhook secret, `subscription_data.metadata.firebaseUID` set on checkout (`src/app/api/stripe/checkout/route.ts`).

## Resolution (completed 2026-09-13)

1. Unsubscribed the 4 affected people from the AHSYNC BYTES newsletter via the Ghost Admin API.
2. Recorded every member to uid to customer to subscription to price mapping as a restore reference (kept out of git).
3. Disabled the Ghost webhook destination in Stripe (`we_1TVpHoGm1lQTGSK4Axbe0Enl`). Done from the Stripe side on purpose, so Ghost issued no cancellation calls.
4. Verified all 5 subscriptions still `active` in Stripe.
5. Deleted the 5 phantom members via `DELETE /members/{id}/` with the `cancel` param omitted, one as a canary first, re-verifying Stripe after each pass.
6. Disconnected Stripe from Ghost. Ghost now shows the Free tier only, 0 paid members, 4,140 total.

Final state: all 5 Focusdoro subscriptions `active`, `cancel_at_period_end: false`, $3 on the Focusdoro price. No revenue affected.

## The constraint (read before enabling a paid tier on the blog)

**Do not reconnect Ghost to the VisionWise AB Stripe account.** Ghost will immediately begin re-ingesting subscriptions from Focusdoro, z2website, blurjoy and the codewithahsan.dev consulting flow as blog members.

If a paid newsletter tier is ever wanted:

- Create a **separate Stripe account** for the blog and connect Ghost to that one. Stripe allows multiple accounts under one legal entity.
- Leave the `blog.codewithahsan.dev/members/webhooks/stripe/` destination disabled. Re-enabling it re-arms the same bug.
- Before trusting any Ghost paid/MRR number, confirm the member has a Ghost `signup_event` with attribution. A member created in the same second as a Stripe event, with no attribution, came from another product.

## Related

- Focusdoro billing code (not at fault): `code-with-ahsan/focusdoro`, `src/app/api/stripe/checkout/route.ts`, `src/app/api/stripe/webhook/route.ts`
- Separately worth fixing: the blog's highest-traffic pages (the Angular v22 migration guides, 88% of search clicks) have no signup path, while the only converting post is the one gated `members`. Search traffic and member growth are currently disconnected.

贡献指南

打开贡献指南

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。