Stripe Connect

Let your users get paid by their own audience. A user onboards a Stripe Connect Express account, sets a recurring membership price and/or a one-time payment price, and shares a public page where anyone can pay them by email. The platform collects an application fee on every transaction, and Stripe pays out the rest to the seller.

v1.8.3
Level: Advanced
15
1618
32

Connect configuration

The cron entry switches from the poller directly to the new enqueuer, and the platform fee percentage is configured here.

Per-account checkpoints

The checkpoint gains a connect:acct_xxx source key, so each connected account tracks its own polling cursor independently of the platform stream.

The Connect context

The public API, in four groups:

Onboarding. start_onboarding/3 lazily creates the local account row and the Stripe account on first call, then returns a one-time onboarding URL. refresh_onboarding_status/1 re-reads Stripe's details_submitted / charges_enabled flags when the seller returns. disconnect/1 deletes the connected account, cancels local active memberships, and resets the account while preserving payment history.

Pricing. set_membership_price/2 and set_one_time_price/2 — both go through the schema changeset, which validates the amount.

Buyer checkout. create_membership_checkout/4 and create_payment_checkout/4 guard on the relevant *_enabled? predicate, compute the platform fee, and hand off to the wrapper. sync_checkout_session/2 runs on return and branches on the session mode — recording a subscription or a payment.

Sync + events. upsert_subscription/3 and record_payment/2 both upsert on a Stripe id, making them idempotent across the return poll and the event poller. handle_stripe_event/2 is what the poller calls per connected account: it acts on customer.subscription.* (resync the membership) and charge.refunded (mark the payment refunded), and always returns :ok so one event can't stall the checkpoint.

The Account schema

The seller's account, with predicates that drive the whole UI. ready?/1 means onboarding is complete and Stripe will accept charges. membership_enabled?/1 and one_time_enabled?/1 layer a configured price on top of that — they gate whether each buy button shows. onboarding_in_progress?/1 distinguishes "started but not finished" so the dashboard can prompt the seller to resume.

The Payment schema

A one-time payment record, keyed uniquely on the Checkout Session id so the return poll and any later event can't double-insert.

The Connect-scoped Stripe wrapper

The crucial difference from MyApp.Billing.Stripe is scope : every call here goes to a connected account via the Stripe-Account header ( connect_opts/1 ), and checkout sessions carry an application fee routed to the platform — application_fee_percent for subscriptions, application_fee_amount (in cents) for one-time payments.

Both checkout modes build their price inline with price_data , so there's no need to pre-create Stripe Product or Price objects on each connected account.

It keeps the same two conventions as the billing wrapper: normalized return maps (never raw Stripe.* structs) and a stub mode that threads the checkout mode and amount through the round-trip — encoded into the stub session id — so the entire onboard → price → buy flow is clickable with no API keys.

The Subscription schema

A membership mirror. Like its billing counterpart, active?/1 treats past_due as still-active so a failed payment gets a grace period rather than an immediate cutoff.

The polling fan-out

A new cron worker fans out the work every minute: one platform poll plus one poll per onboarded connected account. Separate jobs mean the platform stream and each account stream run in parallel, a slow account never blocks another, and the poller's unique clause dedupes overlapping cycles per scope.

Fanning out the event poller

The billing feature's poller is generalized into a scoped worker. The same bootstrap/drain/checkpoint logic now serves two scopes, chosen by job args: platform (dispatched to MyApp.Billing ) and connect with an account_id (dispatched to MyApp.Connect ). Each scope reads from its own Stripe wrapper and dispatches to its own context — the only per-scope differences.

The buyer's checkout return

The buyer's public return leg. It syncs the Checkout Session — recording a membership or a one-time payment — and sends the buyer back to the seller's page with a flash.

The onboarding return

The two legs Stripe redirects the seller back to: return re-reads onboarding status and bounces to the dashboard; refresh mints a fresh onboarding link when the previous one expires.

The public payment page

The buyer-facing page at /c/:account_id . It's public — mounted in the unauthenticated live session — because buyers have no app account. They enter an email and pick a mode; a single form submits with the clicked button's mode , and the LiveView redirects out to Stripe Checkout on the seller's connected account.

The seller dashboard

/connect renders one of three states off the account's predicates: a call to onboard (no account yet), a resume prompt (onboarding in progress), or the full dashboard (ready). The dashboard sets prices, links to the public page, and shows members, payments, and revenue. Onboarding and disconnect are handled inline — onboarding redirects the seller out to Stripe via redirect(external:) .

Routing

Seller routes ( /connect , the onboarding return/refresh) sit in the authenticated scope. The buyer routes ( /c/:account_id and its return) are public — the LiveView goes in the :current_user live session and the return controller in the plain browser scope.

Database

Three tables: