AhsanAyaz / AhsanAyaz/code-with-ahsan

Ghost ingested Focusdoro Stripe subscriptions as blog paid members

Đã đóng
#332 0 bình luận 0 reaction 0 người được giao Xem trên GitHub
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

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.