Skip to content

Integrating In-house Data and Workflows with Stripe Using Private Stripe Apps

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Stripe Apps is a recently-announced platform that allows developers to embed content within Stripe's web UI, extending its functionality to allow interaction with non-Stripe services.

Your immediate thought upon hearing of such a platform might be that it is useful for public services, such as customer support, to develop Stripe integrations. This is a core use-case and high-profile public integrations like those for Intercom and DocuSign have featured prominently in demonstrations of the platform's capabilities.

However, you shouldn't overlook the value of private apps, developed specifically for your organization and visible only to your employees. Private apps may prove to be even more valuable, because they can specifically address your business' problems, automating domain-specific workflows, and integrate with in-house data and services.

What is a private Stripe App?

Stripe Apps published to the marketplace act like apps you might be familiar with from iOS or Android. They are developed for use by the public, each version of them goes through a strict review by Stripe, and they are published to the Stripe Marketplace where anyone can install them.

Private Stripe Apps, in contrast, are published directly to the Stripe account that owns them. Since they are not going to be visible to users outside of the organization they are developed for, they don't have to go through the app review process, simplifying the development and maintenance process.

Accessing internal services

Since Stripe Apps are browser apps which execute on a user's own machine — as opposed to on Stripe's servers — private Stripe Apps can make use of resources that are only accessible from company-controlled devices. Intranet services and any internal authentication are accessible from your private Stripe App just as they are from any other browser-based internal tooling.

There are only two caveats to accessing HTTP services from Stripe Apps. Both of them are driven by the security model of apps. The first is that services must be served over HTTPS, which is standard for internet-facing services, but might not be the case for private services on an intranet.

The second one is that services must allow all cross-origin requests, since requests from Stripe Apps are made with a null origin, and therefore CORS allowlisting cannot be used to secure services against cross-site request forgery.

If this is a major concern, endpoints specific to your Stripe App can be constructed that are secured through Stripe's request signing mechanism, and which proxy requests to the internal services only for requests signed with the App's secret.

Enriching views with context-based data and workflows

Stripe Apps are displayed on the same screen as Stripe objects like customers or invoices, and can access information about those objects and interact with them. This enables smoother workflows by operators by showing important context all in the same view. For example:

  • Displaying product shipping and returns information on the Invoice Details screen in order to make processing refunds more efficient.
  • Allowing operators to see — and maybe edit — the features that a given subscription plan includes directly in the Product Details screen.
  • Displaying account activity from multiple sources like access and change logs in the Customer Details screen in order to make it easier to resolve support queries.

If anyone in your organization is currently working with multiple open browser tabs to manually collate information or execute workflows that cross service boundaries, a private Stripe App could help automate that process. This will free them up to do more valuable tasks, and reduce the likelihood of errors by making contextual information more reliably available, and eliminating manual steps.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

How to Login in to Third Party Services in Stripe Apps with OAuth PKCE cover image

How to Login in to Third Party Services in Stripe Apps with OAuth PKCE

One of the benefits of Stripe Apps is that they allow you to connect to third-party services directly from the Stripe Dashboard. There are many ways to implement the OAuth flows to authenticate with a third-party service, but the ideal one for Stripe Apps is PKCE. Unlike other OAuth flows, a Stripe app authenticating with a third-party using PKCE does not require any kind of backend. The entire process can take place in the user's browser. What is OAuth PKCE Proof Code for Key Exchange (PKCE, pronounced "pixie") is an extension of regular OAuth flows. It is designed for when you've got a client where it would be possible to access a secret key, such as a native app, or a single-page app. Because Stripe Apps are very restricted for security purposes, the OAuth PKCE flow is the only OAuth flow that works in Stripe Apps without requiring a separate backend. Not all third-party services support the PKCE authorization flow. One that does is Dropbox, and we will use that for our code examples. Using createOAuthState and oauthContext to Get an Auth Token To use the OAuth PKCE flow, you'll use createOAuthState from the Stripe UI Extension SDK to generate a state and code challenge. We will use these to request a code and verifier from Dropbox. Dropbox will then respond to a specific endpoint for our Stripe App with the code and verifier, which we'll have access to in the oauthContext. With these, we can finally get our access token. If you wish to follow along, you'll need to both create a Stripe App and a Dropbox App. We'll start by creating state to save our oauthState and challenge, and then get a code and verifier if we don't have one already. If we do have a code and verifier, we'll try to get the token, and put it in tokenData state. ` ` ` Fetch Dropbox User Data To prove to ourselves that the token works, let's fetch Dropbox user data using the token. We'll create a new function to fetch this user data, and call it from within our Stripe App's view. We'll store this user data in state. ` ` ` Storing Tokens with the Secret Store Currently, we're only persisting the retrieved token data in memory. As soon as we close the Stripe App, it will be forgotten and the user would have to fetch it all over again. For security reasons, we can't save it as a cookie or to local storage. But Stripe has a solution: the secret store. The secret store allows us to persist key-value data with Stripe itself. We can use this to save our token data and load it whenever a user opens our Stripe App. To make it easier to work with the secret store, we'll create a custom hook: useSecretStore. ` Once we've got our custom hook ready, we can integrate it into our App.tsx view. We will rewrite the useEffect to check for a saved token in the secret store, and use that if it's valid. Only if there is no token available do we create a new one, which will then be persisted to the secret store. We also add a Log Out button, which will reset the tokenData and secret store values to null. The Log Out button creates an issue. If we have oauthContext from logging in, and then we log out, the Stripe App still has the same oauthContext. If we tried logging in again without closing the app, we would get an error because we're re-using old credentials. To fix this, we also add a React ref to keep track of whether or not we've used our current oauthContext values. ` We've done a lot to create our authorization flow using PKCE. To see this entire example all together, check out this code sample on GitHub....

Creating Complex UIs Using the Stripe UI Extension SDK cover image

Creating Complex UIs Using the Stripe UI Extension SDK

Anyone who embarks on a journey to develop apps for the Stripe dashboard will need to grasp the concepts of frontend development using the Stripe UI Extension SDK. While a backend for your Stripe app may not always be necessary, a frontend is most certainly needed. In this blog, I will provide you with some tips & tricks that you might want to consider if you plan to develop more complex UIs. The Basics Stripe's UI Extension SDK is React-based. This means that you will need to have some React experience if you want to create Stripe apps. While Stripe apps are web apps, you will never touch HTML or CSS directly. In fact, you're forbidden to. This experience feels almost like developing in React Native. You use the provided UI primitives or components to scaffold your UI. When you open your app on the Stripe dashboard, you will see one of your app's *views*. Depending on where you are in the dashboard, you can open a view that is customer-related, payment-related, etc. Or, in the default case, you will open the default view if you are on the main dashboard page. A typical drawer view will have a ContextView component as its first child, and any other components from the UI toolkit as necessary. The main building block for your components is a Box. It's a plain block-level container, so you can think of it as a div. The Box, just like most other components, has a css property where you can apply styling. As mentioned before, you can't use real CSS though. Instead, you'll need to rely on the predefined CSS properties, most of which are very similar to real CSS properties in both naming and functionality. Just like we have a block-level container, we also have inline containers similar to span. In Stripe's case, this component is called Inline. All other components provide some additional functionality. Some of them are meant to be containers as well, while others are usually standalone. Navigating Within Views When you navigate through the Stripe dashboard, the dashboard will be opening different views in your app. On the Customers page, it will open a view for customers, for example (if such view is defined in stripe-app.json). However, what if you are on a view, but wish to navigate somewhere else within the view? Or maybe have functionality where you need to go back and forth between components? A typical scenario is having a widget, which, when clicked, opens more details, but within the same dashboard view. For this scenario, you can utilize React Router, specifically its MemoryRouter. Install it first: ` Since we don't have access to the DOM, and we cannot manipulate the browser location either, using MemoryRouter is good enough for our needs. It will store the current location as well as history in an internal memory array. You use it just like you would use the regular router. Let's assume that you need to authenticate your user before using the app. Let's also assume that, once logged in, you are shown an FAQ component with multiple questions. Once you click a FAQ question, you are directed to a FAQ answer within the view, as shown below: To model such transitions, you would use the React Router. The default view could have the following implementation: ` There are several things of note here. The ContextView is the root component in our DefaultView component. When we don't want to provide title and description of the ContextView, we use a space. You can't use an empty string, or leave out title and description all together. The first child of the ContextView is a Router. This is where our components will render, depending on our route. Remember that we use a MemoryRouter, and the route is an internal property of the router. It's not reflected in the window's location. When you open the view for the first time, you are presented with the AuthInit component. This component could check if you are authenticated, and if so, redirect you to /, and consequently the React Router will render the Faq component. Within the Faq component, we can use the useNavigate hook to navigate to a FaqEntry component to show the expanded question, and answer for a FAQ entry. For example: ` You can even try to extract the Router part into its own component, e.g. NavigationWrapper, and re-use it across views. In that case, you might consider including view IDs in the route to distinguish between different views. Following our example, this means our routes would become: ` The current view ID is always available in the environment?.viewportID property, should you ever need it. Using UserContext and Environment Everywhere The UserContext and Environment are so useful and so commonly used that it makes sense to store them in React context. Otherwise, you'd need to pass them down from your view component all the way to the components that need them. This is a technique known as "prop drilling" in the React world. First, define your context object: ` Now, simply wrap your view component in a context provider, and initialize it with the userContext and environment that are passed to your view: ` Now, you can use the useContext hook to retrieve properties from userContext and environment anywhere in your component tree. For example, this is how we would retrieve the payment object if we were on the payments page: ` Updating Title and Description Dynamically title and description are properties on the view level, but more often than not, you might want to update them dynamically as they are very context-dependant. In one of the above examples, we might want to display the FAQ answer in the title as we navigate to individual FAQ entries. In such cases, we can use React context again. Let's extend our GlobalContext with title and description properties, as well as setters for them. ` The DefaultView component now becomes: ` Now, any child component can update title and/or description, using the following snippet: ` Layout Options Before starting to model different type of layouts, it is highly recommended to learn the concept of "stacks". Stacks are Stripe's way of modelling flexbox-like layouts. It's not exactly the same as flexbox, but once you get the hang of it, you will find it to be very similar. You can stack up elements horizontally (on the "x" axis) or vertically (on the "y" axis). Furthermore, elements can be distributed in a different way along the axis, or aligned in a certain way, giving you the ability to make any kind of layout you can imagine. You can even stack up elements on top of each other. Whenever you want to align your content in any way, you need to use a stack. Key-Value Two-Column Layout Let's assume that you want to show key-value pairs in a two-column layout. This can be accomplished easily using stacks: ` The above code is for one row in the two-column layout. To add more, just add more blocks like these (or better, extract the above component in its own component). This is how it would be displayed in the app: The above code snippet has several things of note: - Whenever you use a width on a Box, make sure its parent has a gap defined. The reason is the way how the width is calculated - it takes gap into calculation, and if the gap is 0, the calculation will produce invalid width (which will just fit the content). - To align content of a Box (we used alignX: 'end'), the Box should be a stack. - In addition to using margin, you can also use marginTop, marginBottom, marginLeft, and marginRight to specify margin only on one (or more sides). - Vertically Aligning Elements Stacks are not only helpful for horizontal alignments. You can align vertically too, and this can come in handy if you want to nicely align an icon with a text label, or if you want to have form elements displayed in a straight line. For example: ` Wizards Whenever you need to have confirmation modals, or wizard-like flows, it's best to use the FocusView component. A focus view slides from the right and allows the user to have a dedicated space to perform a specific task, such as entering details to create a new entry in a database, or going through a wizard. A wizard can be implemented using two or more FocusView components. Here is an example of having a wizard with two components: ` Conclusion Stripe really did fantastic work to provide an amazing UI Extensions for developing custom Stripe apps. We hope this blog post provided you with some guidance on how to set solid foundations if you want to make more complex UIs using the Stripe UI Extension SDK. Stripe is continuously upgrading the UI Extension SDK, and you can definitely expect some new widgets to play with in the future!...

Using XState Actors to Model Async Workflows Safely cover image

Using XState Actors to Model Async Workflows Safely

In my previous post I discussed the challenges of writing async workflows in React that correctly deal with all possible edge cases. Even for a simple case of a client with two dependencies with no error handling, we ended up with this code: ` This tangle of highly imperative code functions, but it will prove hard to read and change in the future. What we need is a way to express the stateful nature of the various pieces of this workflow and how they interact with each other in a way in which we can easily see if we've missed something or make changes in the future. This is where state machines and the actor model can come in handy. State machines? Actors? These are programming patterns that you may or may not have heard of before. I will explain them in a brief and simplified way but you should know that there is a great deal of theoretical and practical background in this area that we will be leveraging even though we won't go over it explicitly. 1. A state machine is an entity consisting of state and a series of rules to be followed to determine the next state from a combination of its previous state and external events it receives. Even though you might rarely think about them, state machines are everywhere. For example, a Promise is a state machine going from _pending_ to _resolved_ state when it receives a value from the asynchronous computation it is wrapping. 2. The actor model is a computing architecture that models asynchronous workflows as the interplay of self-contained units called actors. These units communicate with each other by sending and receiving events, they encapsulate state and they exist in a hierarchical relationship, where parent actors spawn child actors, thus linking their lifecycles. It's common to combine both patterns so that a single entity is both an actor and a state machine, so that child actors are spawned and messages are sent based on which state the entity is in. I'll be using XState, a Javascript library which allows us to create actors and state machines in an easy declarative style. This won't be a complete introductory tutorial to XState, though. So if you're unfamiliar with the tool and need context for the syntax I'll be using, head to their website to read through the docs. Setting the stage The first step is to break down our workflow into the distinct states it can be in. Not every step in a process is a state. Rather, states represent moments in the process where the workflow is waiting for something to happen, whether that is user input or the completion of some external process. In our case we can break our workflow down coarsely into three states: 1. When the workflow is first created, we can immediately start creating the connection, and fetching the auth token. But, we have to wait until those are finished before creating the client. We'll call this state "preparing". 2. Then, we've started the process of creating the client, but we can't use it until the client creation returns it to us. We'll call this state "creatingClient". 3. Finally, everything is ready, and the client can be used. The machine is waiting only for the exit signal so it can release its resources and destroy itself. We'll call this state "clientReady". This can be represented visually like so (all visualizations produced with Stately): And in code like so ` However, this is a bit overly simplistic. When we're in our "preparing" state there are actually two separate and independent processes happening, and both of them must complete before we can start creating the client. Fortunately, this is easily represented with parallel child state nodes. Think of parallel state nodes like Promise.all: they advance independently but the parent that invoked them gets notified when they all finish. In XState, "finishing" is defined as reaching a state marked "final", like so ` Leaving us with the final shape of our state chart: Casting call So far, we only have a single actor: the root actor implicitly created by declaring our state machine. To unlock the real advantages of using actors we need to model all of our disposable resources as actors. We could write them as full state machines using XState but instead let's take advantage of a short and sweet way of defining actors that interact with non-XState code: functions with callbacks. Here is what our connection actor might look like, creating and disposing of a WebSocket: ` And here is one for the client, which demonstrates the use of promises inside a callback actor. You can spawn promises as actors directly but they provide no mechanism for responding to events, cleaning up after themselves, or sending any events other than "done" and "error", so they are a poor choice in most cases. It's better to invoke your promise-creating function inside a callback actor, and use the Promise methods like .then() to control async responses. ` Actors are spawned with the spawn action creator from XState, but we also need to save the reference to the running actor somewhere, so spawn is usually combined with assign to create an actor, and save it into the parent's context. ` And then it becomes an easy task to trigger these actions when certain states are entered: ` Putting on the performance XState provides hooks that simplify the process of using state machines in React, making this the equivalent to our async hook at the start: ` Of course, combined with the machine definition, the action definitions and the actor code are hardly less code, or even simpler code. The advantage of breaking a workflow down like this include: 1. Each part can be tested independently. You can verify that the machine follows the logic set out without invoking the actors, and you can verify that the actors clean up after themselves without running the whole machine. 2. The parts can be shuffled around, and added to without having to rewrite them. We could easily add an extra step between connecting and creating the client, or introduce error handling and error states. 3. We can read and visualize every state and transition of the workflow to make sure we've accounted for all of them. This is a particular improvement over long async/await chains where every await implicitly creates a new state and two transitions — success and error — and the precise placement of catch blocks can drastically change the shape of the state chart. You won't need to break out these patterns very often in an application. Maybe once or twice, or maybe never. After all, many applications never have to worry about complex workflows and disposable resources. However, having these ideas in your back pocket can get you out of some jams, particularly if you're already using state machines to model UI behaviour — something you should definitely consider doing if you're not already. A complete code example with everything discussed above using Typescript, and with mock actors and services, that actually run in the visualizer, can be found here....

Understanding Sourcemaps: From Development to Production cover image

Understanding Sourcemaps: From Development to Production

What Are Sourcemaps? Modern web development involves transforming your source code before deploying it. We minify JavaScript to reduce file sizes, bundle multiple files together, transpile TypeScript to JavaScript, and convert modern syntax into browser-compatible code. These optimizations are essential for performance, but they create a significant problem: the code running in production does not look like the original code you wrote. Here's a simple example. Your original code might look like this: ` After minification, it becomes something like this: ` Now imagine trying to debug an error in that minified code. Which line threw the exception? What was the value of variable d? This is where sourcemaps come in. A sourcemap is a JSON file that contains a mapping between your transformed code and your original source files. When you open browser DevTools, the browser reads these mappings and reconstructs your original code, allowing you to debug with variable names, comments, and proper formatting intact. How Sourcemaps Work When you build your application with tools like Webpack, Vite, or Rollup, they can generate sourcemap files alongside your production bundles. A minified file references its sourcemap using a special comment at the end: ` The sourcemap file itself contains a JSON structure with several key fields: ` The mappings field uses an encoding format called VLQ (Variable Length Quantity) to map each position in the minified code back to its original location. The browser's DevTools use this information to show you the original code while you're debugging. Types of Sourcemaps Build tools support several variations of sourcemaps, each with different trade-offs: Inline sourcemaps: The entire mapping is embedded directly in your JavaScript file as a base64 encoded data URL. This increases file size significantly but simplifies deployment during development. ` External sourcemaps: A separate .map file that's referenced by the JavaScript bundle. This is the most common approach, as it keeps your production bundles lean since sourcemaps are only downloaded when DevTools is open. Hidden sourcemaps: External sourcemap files without any reference in the JavaScript bundle. These are useful when you want sourcemaps available for error tracking services like Sentry, but don't want to expose them to end users. Why Sourcemaps During development, sourcemaps are absolutely critical. They will help avoid having to guess where errors occur, making debugging much easier. Most modern build tools enable sourcemaps by default in development mode. Sourcemaps in Production Should you ship sourcemaps to production? It depends. While security by making your code more difficult to read is not real security, there's a legitimate argument that exposing your source code makes it easier for attackers to understand your application's internals. Sourcemaps can reveal internal API endpoints and routing logic, business logic, and algorithmic implementations, code comments that might contain developer notes or TODO items. Anyone with basic developer tools can reconstruct your entire codebase when sourcemaps are publicly accessible. While the Apple leak contained no credentials or secrets, it did expose their component architecture and implementation patterns. Additionally, code comments can inadvertently contain internal URLs, developer names, or company-specific information that could potentially be exploited by attackers. But that’s not all of it. On the other hand, services like Sentry can provide much more actionable error reports when they have access to sourcemaps. So you can understand exactly where errors happened. If a customer reports an issue, being able to see the actual error with proper context makes diagnosis significantly faster. If your security depends on keeping your frontend code secret, you have bigger problems. Any determined attacker can reverse engineer minified JavaScript. It just takes more time. Sourcemaps are only downloaded when DevTools is open, so shipping them to production doesn't affect load times or performance for end users. How to manage sourcemaps in production You don't have to choose between no sourcemaps and publicly accessible ones. For example, you can restrict access to sourcemaps with server configuration. You can make .map accessible from specific IP addresses. Additionally, tools like Sentry allow you to upload sourcemaps during your build process without making them publicly accessible. Then configure your build to generate sourcemaps without the reference comment, or use hidden sourcemaps. Sentry gets the mapping information it needs, but end users can't access the files. Learning from Apple's Incident Apple's sourcemap incident is a valuable reminder that even the largest tech companies can make deployment oversights. But it also highlights something important: the presence of sourcemaps wasn't actually a security vulnerability. This can be achieved by following good security practices. Never include sensitive data in client code. Developers got an interesting look at how Apple structures its Svelte codebase. The lesson is that you must be intentional about your deployment configuration. If you're going to include sourcemaps in production, make that decision deliberately after considering the trade-offs. And if you decide against using public sourcemaps, verify that your build process actually removes them. In this case, the public repo was quickly removed after Apple filed a DMCA takedown. (https://github.com/github/dmca/blob/master/2025/11/2025-11-05-apple.md) Making the Right Choice So what should you do with sourcemaps in your projects? For development: Always enable them. Use fast options, such as eval-source-map in Webpack or the default configuration in Vite. The debugging benefits far outweigh any downsides. For production: Consider your specific situation. But most importantly, make sure your sourcemaps don't accidentally expose secrets. Review your build output, check for hardcoded credentials, and ensure sensitive configurations stay on the backend where they belong. Conclusion Sourcemaps are powerful development tools that bridge the gap between the optimized code your users download and the readable code you write. They're essential for debugging and make error tracking more effective. The question of whether to include them in production doesn't have a unique answer. Whatever you decide, make it a deliberate choice. Review your build configuration. Verify that sourcemaps are handled the way you expect. And remember that proper frontend security doesn't come from hiding your code. Useful Resources * Source map specification - https://tc39.es/ecma426/ * What are sourcemaps - https://web.dev/articles/source-maps * VLQ implementation - https://github.com/Rich-Harris/vlq * Sentry sourcemaps - https://docs.sentry.io/platforms/javascript/sourcemaps/ * Apple DMCA takedown - https://github.com/github/dmca/blob/master/2025/11/2025-11-05-apple.md...

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co