AhsanAyaz / AhsanAyaz/code-with-ahsan
Ghost ingested Focusdoro Stripe subscriptions as blog paid members
- Ngôn ngữ chính
- TypeScript
- Star
- 27
- Fork
- 53
- Merge trung bình
- 3 giờ 46 phút
- Pull request đã merge (30 ngày)
- 13
Mô tả
## 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.
Hướng dẫn đóng góp
Đánh giá
Issue này chưa được đánh giá.