Skip to content

arch: dual plan mutation paths (mock RPC vs Stripe webhooks) can desync state #37

Description

@drozhzhin-n-e

Problem

Two paths can mutate organizations.plan_id and seats_limit:

  1. Webhook path — Stripe webhook → Edge Function → apply_billing_subscription (service_role)
  2. RPC pathupdate_organization_plan (granted to authenticated, admin check only)

The RPC path is intentional for mock billing, but in production Stripe mode an org admin can still call it directly via PostgREST, potentially putting Postgres plan state out of sync with Stripe.

Impact

  • Plan/seat state in Postgres can diverge from Stripe subscription
  • Entitlements (seat limits, email quotas) may not match what customer is actually paying for
  • Hard to detect unless monitoring is in place

Proposed fix

Option A: Revoke EXECUTE on update_organization_plan from authenticated when Stripe is the billing provider (requires app-level enforcement or a DB flag).

Option B: Add a guard inside update_organization_plan that checks organization_billing.provider; if Stripe, reject the call.

Option C: Document as intentional design for mixed providers and accept the risk.

Files

  • supabase/migrations/0008_org_billing_plan.sql
  • supabase/migrations/0017_update_organization_plan_seats.sql
  • supabase/migrations/0019_guard_seats_limit_override.sql
  • docs/adr/0002-billing-multi-provider.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    mediumMedium severitysecuritySecurity vulnerability or hardening

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions