For API-based products it is not always easy to get the developer experience smooth. I believe this
can be greatly improved if it was easier for the service provider to offer full-stack integration
primitives. By distributing frontend, backend, and database primitives in one package you shift much
of the integration work for the customer to the service provider.
A common issue when integrating with a third party is the split brain problem: a copy of the service
provider's data is replicated to the customers application and this data must be kept in sync. This
places the burden on the customer to understand and implement all the subtle details involved in
keeping that data aligned. A smoother integration experience would be if most of the integration
code, e.g. webhook handlers and database schemas, were provided by the service provider directly.
We explore these ideas by implementing a Stripe subscription library that provides components for
every layer of the stack and we see how it makes integrating with Stripe significantly simpler.
Stripe's Billing product for subscriptions handles payment schedules, subscription lifecycle
management and invoicing. Application developers need to know if their customers have an active
subscription, so they keep a local record of Stripe subscription data. To keep this copy aligned,
developers integrate with Stripe's webhooks, updating their records as events come in. This solution
gives them the ability to query data as they see fit while keeping the app latency low.
The figure below shows what that would look like. In blue we have annotated the components that we
have built in our Stripe integration library.
A typical Stripe integration where webhook events are processed then stored in the database.
Implementing a few webhooks seems simple enough on the surface, but in reality always requires more
work than you'd think.
The development experience is not ideal. You need a way to generate and then test the events the
integration relies on, and you also need a tunnel from Stripe to your dev machine. In order to
create test events, the right preconditions must be set up. Stripe's CLI helps here by creating
products, customers, and prices, and by opening that tunnel for you. This covers a large part of the
workflow, but sadly it does not cover every event type or scenario.
Beyond testing, developers also need to understand Stripe's delivery guarantees for events and how
these shape their webhook handlers. Handlers must be idempotent because Stripe guarantees 'at least
once' delivery and they must take into account that events can arrive out of order. On top of that
developers have to account for the possibility of a sudden burst of retries so their system is not
overloaded.
All the details quickly pile up to become a bit of a headache. The funny thing is, it seems that
everyone using Stripe for subscriptions has to deal with the exact same set of problems.
It'd be great if Stripe could take over some of the integration burden, but that is not easy. For
one, they can't make any assumptions about their customers' tech stack: which database they use,
which ORM, which backend framework, or even which programming language. Maintaining bespoke
solutions across all of these options and product features is seemingly an intractable problem.
Fragno is our open-source framework-agnostic toolkit for building full-stack TypeScript libraries.
In short, it gives library builders primitives for frontend hooks, backend request handlers, and a
database layer with schema and query support, all without worrying about the specific systems the
end users might use. This is how we built our Stripe integration.
With Fragno a Stripe integration looks very much like how you'd implement it yourself, but what is
different is who is implementing it.
This way much of the headache of implementing Stripe shifts from the customer to the fragment
provider. It would be preferable to have Stripe maintain this fragment of course, updating it as
they change their APIs and data model, but for now we have taken on this task ourselves.
The Fragno CLI reads which fragments you've installed and generates schema files for the ORM and
database that the developer is using. In this example it's Drizzle with PostgreSQL.
From a data model perspective, a subscription must be owned by some kind of entity, like a user or
organization, and this mapping is one of the few things that remains to be implemented by the
developer. It basically amounts to a stripe_customer_id column on the user table. The fragment
requires you to implement the onStripeCustomerCreated callback that sets this value when Stripe
Customers are created.
Similarly, when one of the end users interacts with the /api/stripe/upgrade or
/api/stripe/cancel routes, that request must be authenticated and resolved back to a Stripe
Customer that belongs to that user (if one exists). This is implemented with the
resolveEntityFromRequest callback.
Note that this new way of integrating is more configuration-based rather than implementation-based.
Instead of writing route handlers and designing database schemas, you're primarily connecting
pre-built components to each layer of the stack.
In the end, the cognitive load for the developer is mostly spent on the parts that are unique to
their application: the data model. Answering the question of, how does a Stripe subscription relate
to the entities in my system? Many of the peculiarities of implementing Stripe have become the
fragment author's problem, this way the developer can focus on the product they're building.
Fragno and the Stripe fragment are very much in a developer preview state. If you're interested in
trying it out take a look at our quickstart guide. Code is available on
GitHub under the MIT license.
If you're interested in building better SDKs for your users, we'd love to
discuss what you might need.