Integrating subscriptions with Stripe is one of those tasks that looks straightforward on paper but quickly becomes complex in practice. Between ephemeral keys, client secrets, webhooks, and subscription lifecycles, it’s easy to get lost.
In this post, I’ll walk through the technical design of how Ensemble integrates Stripe for mobile apps, why we chose a frontend-only abstraction, and how you can set up a backend to complete the flow.
⚙️ The Design Philosophy
Ensemble is a low-code platform, but we made an intentional design decision:
- Frontend logic (initialize Stripe, show Payment Sheet) is built in.
- Backend logic (create Payment Intents, manage subscriptions, handle webhooks) is left to the developer.
Why? Because billing systems vary wildly. Some apps require a single plan, while others necessitate multi-tier pricing, trials, discounts, or usage-based billing. Offloading this to developers’ backends gives flexibility without locking you in.
🔄 The Flow
Here’s how the Stripe integration works in Ensemble:
- User taps Subscribe in your app.
- Ensemble initializes Stripe with your publishable key.
- Your backend creates a Payment Intent and returns a clientSecret.
- Ensemble presents the Payment Sheet with that
clientSecret
. - Stripe processes the payment.
Your backend listens to webhooks to update subscription state.
📐 Architecture Diagram
📝 Ensemble Config Example
First, initialize Stripe:
Button: label: Initialize Stripe onTap: initializeStripe: publishableKey: "pk_test_your_publishable_key_here" merchantIdentifier: "merchant.com.yourapp" onSuccess: showToast: message: "Stripe initialized successfully" onError: showToast: message: "Failed to initialize Stripe"
Then, show the Payment Sheet using the clientSecret
from your backend:
initializeStripe: publishableKey: "pk_test_your_publishable_key_here" merchantIdentifier: "merchant.com.yourapp" onSuccess: showPaymentSheet: clientSecret: ${paymentIntentClientSecret} configuration: merchantDisplayName: "My Store" style: "system" primaryButtonLabel: "Pay $29.99" onSuccess: showToast: message: "Payment successful!" onError: showToast: message: "Payment failed" onError: showToast: message: "Failed to initialize payment system"
🖥 Backend Implementation (Node.js Example)
Your backend needs to create a Payment Intent and return the clientSecret
. Here’s a minimal Express setup:
import express from "express"; import Stripe from "stripe"; const app = express(); const stripe = new Stripe("sk_test_your_secret_key_here"); app.post("/create-payment-intent", async (req, res) => { try { const paymentIntent = await stripe.paymentIntents.create({ amount: 2999, // $29.99 currency: "usd", automatic_payment_methods: { enabled: true }, }); res.json({ clientSecret: paymentIntent.client_secret }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.listen(4242, () => console.log("Server running on port 4242"));
🧩 Webhooks: The Source of Truth
One of the most important design lessons: don’t rely solely on the client for subscription state.
Stripe webhooks tell you when:
- A payment succeeds or fails.
- A subscription renews or cancels.
- A trial starts or ends.
This means your backend should update your database on every webhook event and expose that state back to your app. Ensemble can then reflect the user’s status (trial, active, canceled).
🛠 Lessons Learned
While building and testing this integration, here are a few takeaways:
- Ephemeral keys are tricky: They expire quickly, so your backend must issue fresh client secrets reliably.
- Testing saves time: The Stripe CLI is a must-have for simulating webhook events.
- Graceful error handling matters: Expired cards, failed renewals, and canceled subscriptions are real-world cases you need to handle.
- Separation of concerns: Keeping the frontend in Ensemble and backend in your stack of choice made the integration more flexible and maintainable.
✅ Wrapping Up
Stripe subscription management isn’t easy, but with Ensemble, the frontend flow becomes dead simple: initialize Stripe and show the Payment Sheet.
The complexity shifts to your backend, where you create Payment Intents and manage subscription state via webhooks. This design provides developers with flexibility while eliminating 80% of the boilerplate typically required to integrate Stripe.
If you’re building a subscription-based app, I’d love to hear how you’re handling Stripe integration — drop your thoughts in the comments!
Top comments (0)