Building a Multi-Party Payments Marketplace with Stripe Connect and GraphQL
A technical deep-dive into building Mercurylist: an event signup and payments platform with Stripe Connect multi-party payouts, a unified GraphQL API serving both web and mobile, and real-time subscriptions. Shipped to production in 12 weeks.
Mercurylist started with a deceptively simple premise: let event organizers collect signups, sell time slots, accept payments, and send automated reminders. All in one place.
Deceptively simple because underneath that product description sits a genuinely hard technical problem. You are not just accepting payments. You are moving money between multiple parties: the organizer who runs the event, the platform that facilitates it, and the attendee paying to attend. That is a multi-party payment flow, and multi-party payment flows have a way of making everything else more complicated.
We shipped Mercurylist in 12 weeks: a full web app, a React Native mobile app, a GraphQL API, and Stripe Connect integration with automated payouts.
Here is how we built it.
The Problem Space
Most event tools handle payments the simple way: the platform collects all the money and manually pays out organizers later. This works until it doesn't. Organizers chase payouts. Reconciliation becomes a spreadsheet nightmare. The platform carries financial liability for funds it is holding on behalf of others.
The right architecture is a connected account model: each organizer has their own Stripe account, payments flow directly to them with the platform taking a cut automatically at transaction time, and nobody is waiting for a manual payout.
Stripe Connect exists precisely for this use case. The challenge is not knowing that Stripe Connect is the answer. It is implementing it correctly across a multi-platform product without creating a tangled mess of payment logic spread across web, mobile, and backend.
Before writing a line of production code, we needed clear answers to three questions:
- How do we structure connected account onboarding so organizers can get set up without friction?
- How do we share payment logic across web and mobile without duplicating it?
- How do we handle the failure modes (failed charges, disputed payouts, incomplete onboarding) without building a customer support nightmare?
Those questions shaped the architecture from day one.
Week 1 and 2: Discovery Before Code
Same principle as every project we build: two weeks of structured discovery before any production code.
The discovery sprint for Mercurylist focused on three areas.
Payment flow mapping. We diagrammed every money movement in the system: attendee pays, platform fee is split, organizer receives payout, refund is issued. Every edge case (partial refunds, failed charges, organizers with incomplete Stripe onboarding) was mapped before we wrote schema.
API contract design. Mercurylist needed to serve two clients simultaneously: a Next.js web app and a React Native mobile app. The only way to do that without duplicating business logic is to design a single API that both consume. We chose GraphQL and defined every query, mutation, and subscription before implementation began. Frontend and mobile development ran independently against mock resolvers while the backend was being built.
Schema design. The data model for an event signup platform has more nuance than it appears. Events have time slots. Time slots have capacity limits. Signups are attached to time slots, not just events. Payment records need to track the Stripe charge ID, the connected account that received funds, the platform fee withheld, and the payout status. We modeled all of this before writing a single migration.
That investment compressed the implementation timeline significantly. The remaining ten weeks were execution against a known plan.
The Payment Architecture
The core of Mercurylist is the Stripe Connect integration. Here is how it works.
Connected account onboarding. When an organizer creates an account, they go through Stripe's hosted onboarding flow via an account link. We generate the link from our backend, they complete identity verification and bank account setup on Stripe's side, and we receive a webhook confirming their account is active. Until that webhook arrives, the organizer cannot charge attendees.
This design keeps us out of the business of collecting or storing sensitive financial data. Stripe handles it. We just track account status.
Payment intents with application fees. When an attendee pays for a signup, we create a Stripe Payment Intent with two key fields: the transfer_data.destination set to the organizer's connected account ID, and application_fee_amount set to the platform's cut. Stripe handles the split automatically. The organizer's Stripe balance is credited immediately at charge time.
payment_intent = stripe.PaymentIntent.create(
amount=amount_in_cents,
currency="usd",
payment_method=payment_method_id,
confirm=True,
transfer_data={"destination": organizer_stripe_account_id},
application_fee_amount=platform_fee_in_cents,
)
This pattern means we never hold money on behalf of organizers. Every payment goes directly to the right place.
Webhook handling. Payment systems only work if you handle asynchronous events correctly. We built a webhook handler for every Stripe event that mattered: payment_intent.succeeded, payment_intent.payment_failed, account.updated (for connected account status changes), and charge.dispute.created. Each event updates the relevant records in our database and triggers downstream actions like confirmation emails and dashboard updates.
Refunds. Refunds in a split-payment model require reversing the application fee and the transfer separately. We built a refund service that handles this automatically based on how much time has elapsed since the original charge and the organizer's refund policy settings.
The GraphQL API
The decision to build a GraphQL API rather than REST was driven entirely by the multi-client requirement.
With REST, you end up building view-specific endpoints or returning more data than either client needs. GraphQL lets each client (web and mobile) query exactly what it needs with a single endpoint. The web app might need event metadata, time slot capacity, and organizer profile in a single request. The mobile app might only need the event title and the next available time slot. Both can express that precisely without the backend knowing or caring about their UI.
We used FastAPI with Strawberry for the GraphQL layer. Strawberry is a Python GraphQL library that uses type annotations to define your schema. The Python types and the GraphQL types stay in sync automatically.
The schema had three major areas:
Queries for fetching events, time slots, organizer dashboards, and signup history.
Mutations for creating events, processing signups, initiating payments, and managing connected account onboarding.
Subscriptions for real-time updates. When a time slot fills up, every connected client sees the capacity update immediately via a GraphQL subscription over WebSockets. Organizers watching their dashboard see signups arrive in real time without refreshing.
MongoDB change streams powered the subscriptions. When a signup document is inserted or a time slot's remaining_capacity field is decremented, the change stream triggers and the update is pushed to subscribed clients.
Cross-Platform Delivery
Shipping web and mobile simultaneously from a single backend is a forcing function for good API design.
The web app is Next.js with server-side rendering for event pages, which matters for SEO. Organizers share event links and those pages need to be indexable. The mobile app is React Native with Expo, which handles the build pipeline for iOS and Android.
Both clients share TypeScript type definitions generated from the GraphQL schema. When the schema changes, types regenerate automatically. There is no drift between what the API says it returns and what the clients expect to receive.
Platform-specific UI lives in each client. Shared business logic lives in the API. That separation is the only way to ship two platforms in parallel without one team constantly blocking the other.
Automated Reminders
The reminder system was a deliberate scope decision. Attendees who sign up for events and then forget to show up is a solved problem: you send reminder emails. But building a general-purpose email scheduling system is a rabbit hole.
We scoped it tightly: a background job that runs nightly, queries for signups with events in the next 24 hours and 1 hour, and sends templated reminder emails via a transactional email provider. No custom scheduling UI, no complex rule engine. Just the thing that needed to exist.
This is a recurring pattern in MVP delivery. The feature people ask for and the feature they actually need are often simpler than the first conversation suggests. The organizer does not need a drag-and-drop reminder workflow builder. They need attendees to show up.
What Shipped
Twelve weeks from kickoff:
- Event creation and management for organizers
- Time slot scheduling with per-slot capacity limits
- Attendee signup flow with payment via Stripe Connect
- Automated split payments with platform fee at transaction time
- Organizer dashboard with real-time signup tracking
- Automated reminder emails at 24-hour and 1-hour intervals
- React Native mobile app alongside the web app
Lessons
Get payment architecture right before writing product code. The Stripe Connect model touches everything: the data schema, the onboarding flow, the webhook handlers, the refund logic, the organizer dashboard. If you start building the product and bolt payments on later, you will rearchitect half of what you built.
GraphQL subscriptions are worth the setup cost for real-time products. The live capacity updates on time slots felt like a small feature during scoping. In practice, seeing signups arrive in real time on the organizer dashboard was one of the things early users mentioned most. Real-time feedback changes how people experience a product.
Two platforms in parallel is achievable with the right contract. Designing the API before building it, and generating types from the schema, meant the web and mobile teams could work independently with confidence. The integration at the end was genuinely boring. That is exactly what you want.
Scope the reminder system, not the reminder platform. We shipped a nightly job that sends two emails. It works. The temptation to build something more sophisticated was real. Resisting that temptation is how you ship in 12 weeks.
Newsletter
Straight talk on software from people who ship it
Occasional insights from the Signal Shift Labs team on building production-ready products. Plus a free copy of our 12-Week MVP Playbook. Unsubscribe anytime.
Does this sound like how you want to build?
We take on a small number of projects each quarter. If you have an idea, urgency, and budget, we'd love to hear from you.
Start a conversation →