About the Project — SubSplit
Inspiration
Subscriptions are everywhere, but most "family" or "team" plans assume you already have a stable group to split with. A lot of people want access to services like Spotify Family or YouTube Premium Family, but either don't have enough friends on the same plan, or can't justify the full price alone.
But the problem extends beyond streaming. Roommates split rent and utilities with Venmo requests and spreadsheets. Freelancers pay full price for Figma and Notion seats they could share. Study groups want Coursera Plus or Chegg access for one semester, not a full year. In every case, the core challenge is the same: coordinating recurring payments across a group of people who may not fully trust each other.
SubSplit was built to solve that gap. It's a general-purpose group cost-splitting API that helps users form sharing groups, discover open groups through a marketplace, and manage the messy realities of group payments — proration when members leave, democratic plan changes, and fair handling of remainder cents. Subscription splitting is the first use case, but the architecture generalizes to shared housing expenses, team software licensing, education cohorts, and family financial management.
What We Built
SubSplit is an API-first group cost-splitting platform with four major systems:
- Groups + Membership: create a group, generate a join code, invite friends, and manage member roles (
OWNER/ADMIN/MEMBER) with lifecycle transitions and proper authorization at each level. - Subscription selection: attach a real "family/team" plan from a seeded service catalog with category, pricing, max users, and sharing method metadata.
- Payment lifecycle engine: each member pays their portion via Stripe Checkout, and the backend tracks payments through an explicit state machine (
PENDING → PROCESSING → SUCCEEDED → SETTLED, orFAILED → REQUIRES_RETRY). Every mutating payment endpoint respects idempotency keys, so webhook retries and network hiccups never double-charge a user. Groups configure how costs divide —equal_split,percentage_based, orcustom— with validation that shares always sum to the plan cost exactly. - Marketplace (v1): authenticated discovery of open groups with cursor-based pagination (
limit+starting_after,{ data: [...], has_more: boolean }), multi-field filtering (?category=streaming&max_monthly_cost=20.00&has_open_slots=true), and full-text search. Users apply to join groups through a stateful application workflow (APPLIED → ACCEPTED / REJECTED) with owner approval. - Governance: any member can propose changes (
add_member,remove_member,change_plan,transfer_ownership,dissolve_group) via proposals that follow their own state machine (OPEN → APPROVED / REJECTED / EXPIRED). Proposals resolve automatically when a configurable voting threshold is met. Every state change across the platform — payments, membership transitions, proposal outcomes — is captured as an immutable Event resource. - Trust system: a 0.0–5.0 trust score computed from payment history, group tenure, and peer reviews. Groups can set minimum trust thresholds for new applicants, and the marketplace supports filtering by trust score.
- Developer experience: consistent error shapes (structured JSON with
error.type,error.code,error.message,error.param), Stripe-prefixed object IDs (grp_,usr_,pay_,svc_,prp_) for instant debugging, expandable objects (?expand[]=members&expand[]=subscription.service), strong input validation with Zod, and a complete cURL walkthrough so the API is usable without any frontend.
This matches the track focus on building a well-crafted, stateful API with clear behavior and great documentation.
How We Built It
API Design (Thin Handlers, Service Layer)
We used Next.js route handlers as "thin controllers": each handler authenticates the user, validates input with Zod, calls a service function, and returns a predictable JSON response.
Business logic lives in a service layer (e.g., group.service.ts, billing.service.ts, governance.service.ts) so the API stays consistent and easy to extend. For example, group creation is atomic using a Prisma transaction (create the group and add the owner membership in one commit). Proposal resolution is similarly transactional — when a vote crosses the approval threshold, the proposal state update and the resulting action (adding a member, changing a plan) execute in a single commit.
Data Model (Prisma + Postgres)
The schema is built around a set of interconnected resources:
GroupandGroupMemberfor membership + roles with lifecycle statesSubscriptionServicefor the plan catalog (price in cents, max users, category, sharing method)GroupSubscriptionfor the plan selected by a group, with split rule configurationPaymentfor per-user payments tied to a billing period, with an explicit state machineInvitationand marketplaceApplicationfor joining workflows with stateful approvalProposalandVotefor democratic group governanceTrustScoreandReviewfor reputation trackingEventas an immutable audit log of all state transitions
We intentionally store all money values in integer cents (never floats) to avoid rounding issues.
Stripe Integration (Checkout + Webhooks)
Stripe Checkout handles the actual payment UX. The API creates a Checkout session for "pay my share," then finalizes state via a Stripe webhook:
/api/billing/checkoutcomputes what the current user owes (factoring in proration and split rules) and returns a Stripe-hosted payment URL./api/webhooks/stripeverifies the Stripe signature using the raw request body and handlescheckout.session.completed, marking the payment asSUCCEEDEDand updating the group subscription state.
To keep the API safe under retries, webhook processing is idempotent (duplicate Stripe events do not double-apply updates). All mutating payment endpoints also accept an Idempotency-Key header — the server stores the key-to-response mapping and returns identical results on retry.
Marketplace v1 + Stripe-Style Pagination
The marketplace layer (/api/v1/...) supports discovery and search with cursor-based pagination:
limit+starting_after- response envelope
{ data: [...], has_more: boolean } - multi-field filtering:
?category=streaming&max_monthly_cost=20.00&min_trust_score=4.0&has_open_slots=true
This makes list endpoints predictable and scalable, and it mirrors patterns developers already recognize from Stripe's own API.
Use Cases Beyond Subscription Splitting
SubSplit's architecture generalizes cleanly because the core abstractions — groups, members, payments, proposals, events — aren't subscription-specific:
- Shared Housing: roommates splitting rent, utilities, and internet. Variable-amount billing cycles handle fluctuating electricity bills, and
percentage_basedsplit rules handle unequal room sizes. - Team Software Licensing: freelancers pooling Figma, GitHub Team, or Adobe Creative Cloud seats. The marketplace connects independent contractors who want to share tool costs.
- Education Cohorts: study groups splitting Coursera Plus, MasterClass, or Chegg for a semester. The governance engine handles democratic plan decisions, and proration handles members who leave when courses end.
- Family Financial Management: parents managing children's subscriptions with spending limits and approval workflows via the proposals system.
What We Learned
- API UX matters: clear resource naming, consistent error formats, prefixed IDs, and predictable pagination make the difference between an API that "works" and one that developers actually enjoy using.
- State transitions are the hard part: payments, membership, proposals, and trust aren't just CRUD — they're workflows. Webhooks, retries, idempotency, and transactional consistency are essential if you want correctness.
- Governance is the missing layer: most group-payment tools assume everyone agrees. Real groups argue about plan changes, deal with non-paying members, and need transparent decision-making. Modeling this explicitly as an API is what makes SubSplit more than just another bill-splitting tool.
- Rounding rules must be explicit: cost-splitting sounds simple until you define what happens to remainder cents.
A key design point was splitting a group plan across members without losing money to rounding. Conceptually, we want:
$$\sum_{i=1}^{n} \text{share}_i = \text{monthlyPriceCents}$$
So our implementation ensures the total paid equals the plan cost exactly, even when division isn't even.
Challenges Faced (Summary)
This project had several "real-world" challenges that showed up immediately once we connected auth + database + payments + governance:
- Keeping group creation, membership updates, and proposal resolution transactional
- Preventing duplicate joins and double-votes (unique constraints + friendly errors)
- Handling Stripe webhooks correctly (raw body + signature verification)
- Making payment processing safe under retries (idempotency keys + stored responses)
- Designing marketplace endpoints that support filtering + pagination cleanly
- Computing prorated shares when members join or leave mid-billing-cycle
- Building a governance system where proposal resolution and its side effects (membership changes, plan updates) execute atomically
- Balancing trust score computation to reward consistent payment behavior without punishing new users who haven't built history yet
Built With
- google-gmail-oauth
- next.js
- postgresql
- stripe
- typescript
- vercel
Log in or sign up for Devpost to join the conversation.