-
Notifications
You must be signed in to change notification settings - Fork 14
Description
Summary
When Stripe fires a customer.deleted webhook event, the component silently drops it (falls through to the default case in processEvent()). There is no handleCustomerDeleted private mutation, so the customer record — including PII (email, name, metadata) — persists indefinitely in the component's customers table.
This prevents downstream applications from fulfilling GDPR Article 17 (Right to Erasure) and CCPA/CPRA deletion obligations.
Current behavior
- Customer is deleted in Stripe (via dashboard, API, or data subject request)
- Stripe sends
customer.deletedwebhook to the registered endpoint processEvent()insrc/client/index.tshits thedefaultcase:default: console.log(`ℹ️ Unhandled event type: ${event.type}`);- The customer row in the component's
customerstable is never removed or anonymized - Associated records in
subscriptions,payments, andinvoicestables also retain PII (stripeCustomerId, metadata with potential user
identifiers)
Expected behavior
A customer.deleted event should:
- Remove or anonymize the customer record in the
customerstable - Optionally cascade to associated records (subscriptions, payments, invoices) — at minimum clearing PII fields while retaining financial records
needed for accounting
Why this is urgent
Under GDPR Article 17, data controllers must erase personal data "without undue delay" (within 30 days) when requested. Because the customers table lives inside the component's namespace, consuming applications cannot directly delete these records — they depend on the component exposing a private mutation for this.
The workaround of adding a custom event handler via registerRoutes({ events: { 'customer.deleted': ... } }) is insufficient because the handler cannot access the component's internal tables to delete the customer row.
Proposed fix
1. Add handleCustomerDeleted to src/component/private.ts
export const handleCustomerDeleted = mutation({
args: {
stripeCustomerId: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const customer = await ctx.db
.query("customers")
.withIndex("by_stripe_customer_id", (q) =>
q.eq("stripeCustomerId", args.stripeCustomerId),
)
.unique();
if (customer) {
await ctx.db.delete(customer._id);
}
return null;
},
});2. Add the case to processEvent() in src/client/index.ts
case "customer.deleted": {
const customer = event.data.object as StripeSDK.Customer;
await ctx.runMutation(component.private.handleCustomerDeleted, {
stripeCustomerId: customer.id,
});
break;
}Environment
@convex-dev/stripeversion:0.1.3stripeSDK:^20.0.0