Skip to content

Payment infrastructure: Stripe integration #294

@simonsmallchua

Description

@simonsmallchua

Context

Paddle rejected our application for milestone 5.2 (payment infrastructure). We need an alternative payment provider. Stripe is the recommended path — no approval gate, mature Go SDK, and our DB schema is already prepared.

Current state

  • plans table exists with tiers and daily page quotas (supabase/migrations/20260104212025_add_daily_usage_quotas.sql)
  • organisations.plan_id already references plans, defaults to free tier
  • Zero payment code — no SDK, no webhooks, no checkout flow in Go
  • Quota enforcement is handled server-side via SQL functions — payment provider just needs to flip plan_id

Proposed: Stripe + Stripe Checkout + Stripe Tax

Why Stripe over alternatives

Option Type Fees Approval Tax handling Go SDK
Stripe Direct ~2.9% + 30c None Stripe Tax add-on Mature
Paddle MoR ~5-8% Rejected Included OK
Lemon Squeezy MoR ~5% + 50c Easier Included Limited
Polar.sh MoR ~5% Easy Included Early

Stripe gives us the lowest fees, no approval blocker, and the most mature Go integration. Stripe Tax can be added later to handle GST/VAT collection automatically if we sell internationally at scale.

Implementation scope

1. Stripe SDK + configuration

  • Add github.com/stripe/stripe-go to go.mod
  • Add STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET to env config
  • Add STRIPE_PUBLISHABLE_KEY for frontend checkout redirect
  • Update .env.example and docs/operations/ENV_VARS.md

2. Checkout session endpoint

  • POST /v1/checkout — creates a Stripe Checkout Session for a given plan
  • Redirects user to Stripe's hosted payment page
  • Success/cancel URLs route back to /settings
  • Maps our plans table rows to Stripe Price IDs (store mapping in config or DB)

3. Customer portal endpoint

  • POST /v1/billing/portal — creates a Stripe Billing Portal session
  • Lets users manage subscription, update payment method, cancel
  • Zero UI to build on our side

4. Webhook handler

  • POST /webhooks/stripe — receives Stripe events
  • Key events to handle:
    • checkout.session.completed → set org plan_id to purchased plan
    • customer.subscription.updated → handle plan changes/downgrades
    • customer.subscription.deleted → revert org to free plan
    • invoice.payment_failed → flag org, optionally notify
  • Verify webhook signature using STRIPE_WEBHOOK_SECRET

5. Frontend integration

  • Add "Upgrade" / "Manage billing" buttons to settings.html
  • "Upgrade" calls checkout endpoint, redirects to Stripe
  • "Manage billing" calls portal endpoint, redirects to Stripe
  • No card forms or payment UI to build — Stripe hosts everything

6. Plan mapping

  • Create Stripe Products + Prices matching our plans table tiers
  • Store Stripe Price ID ↔ plan mapping (either in plans table as a stripe_price_id column, or in config)
  • Migration to add stripe_price_id and stripe_customer_id columns

7. Stripe Tax (future, optional)

  • Enable Stripe Tax on checkout sessions to auto-calculate GST/VAT
  • Requires setting our tax registration in the Stripe dashboard
  • No code changes beyond automatic_tax: {enabled: true} on session creation

Files likely touched

  • cmd/app/main.go — route registration
  • internal/api/handlers.go — new checkout/portal/webhook handlers
  • internal/api/middleware.go — exempt webhook route from CSRF/auth
  • internal/db/organisations.go — update plan on webhook events
  • settings.html + web/static/app/pages/settings.js — billing UI buttons
  • supabase/migrations/ — add stripe_price_id, stripe_customer_id columns
  • .env.example, docs/operations/ENV_VARS.md — new env vars
  • Dockerfile — no changes needed (Go binary, no new static assets)

Effort estimate

~1-2 days for core flow (checkout + webhooks + settings UI). Stripe Checkout and Billing Portal eliminate most of the frontend work.

Open questions

  • Do we want to reapply to Paddle with a better application, or commit to Stripe?
  • Should Stripe Tax be enabled from day one, or deferred until international volume warrants it?
  • Do we need a trial period, or is the free tier sufficient as the entry point?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions