Unstripe yourself: How to replace Stripe Elements with Basis Theory
Elements provide modern building blocks for collecting sensitive information in your UI, allowing developers to build immersive forms that match the look and feel of your website. While these powerful components keep systems out of compliance scope, they also couple applications and websites to the payment processors (most notably, Stripe) that offer them.
Using Basis Theory Elements and our PCI-compliant environment, developers can decouple their card data from their payment processors and secure it in a CDE in just a couple of hours.
Let’s look at how to insert Basis Theory in front of Stripe without changing your Stripe integration or downstream services.
Application Overview
Every application will have a different architecture, and every implementation will be slightly unique. For this guide, I have chosen to use the with-stripe-typescript
example project from NextJS. To start a new project, execute npx create-next-app folder-name –example with-stripe-typescript
. This will set up the project in the folder you specify.
This NextJS project comes with three different examples, all highlighting a different way to use Stripe. Two of the examples (“Donate with Checkout” and “Use Shopping Cart”) use features that are hosted on stripe.com and do not use Elements.
For our example, we will upgrade the “Donate with Elements” page to Basis Theory Elements.
There are three things to notice about the new project. First is the .env.local.example
file, which contains environment variables that are injected into the project at build time. This is where the Stripe and Basis Theory API keys will be set. There is also a components
folder that contains several components used across the app, and the ElementsForm.tsx
component is the one we will be upgrading. Finally, there is the pages
folder, which contains the backend API code and the pages that consume the components.
The NextJS team has a great introductory guide if you aren’t familiar with Next. You do not need to know all the ins and outs of NextJS to follow this guide, but a basic understanding of how NextJS works will be helpful!
Environment Variables
The first step is configuring your environment variables. We need five keys for this project:
- A Private Basis Theory Application, with the following permissions:
token:pci:create
andtoken:pci:use:reactor
. The API will use this to use the token to create a charge with Stripe. - A Public Basis Theory Application, with the following permissions:
token:pci:create
. This will be used for the frontend Elements. Public Applications only have write permissions, allowing you to use the key in your frontend code without worrying about malicious actors using it to read back sensitive data. - A Reactor ID to securely send our card data to Stripe without consuming the data in our app.
- A Stripe publishable key
- A Stripe secret key
If you need help creating the Applications, you can refer to our complete guide to Applications. Once you have the API keys for both Applications, you will need to put the keys into the .env.local.example
file. The Public API key variable name will need to start with NEXT_PUBLIC_
- this ensures that NextJS makes the variable available to your frontend application. Next, you will need your Stripe publishable and secret keys. Enter the publishable key into the .env.local.example
file.
The last piece of the puzzle is the “Stripe - Create Payment Method for a Card” Reactor. Inside the Basis Theory Portal, select Reactors from the menu, then “Create Reactor”. Next, search for “Stripe” and select the “Stripe - Create Payment Method for a Card” formula, then “Use This Formula”. Give the Reactor a name and enter your Stripe secret key, then click “Create Reactor.” When you create the Reactor, you need to copy the Reactor ID and store it in the .env.local.example
file also.
Make sure to review our complete guide to Reactors to learn more.
Basis Theory Elements Provider
Basis Theory’s React package uses Context to ensure the Basis Theory service is available throughout your application. Stripe’s Elements uses a similar provider to wrap and help initialize the fields. Therefore, we first need to remove the Stripe provider and replace it with the Basis Theory provider.
Open the pages/donate-with-elements.tsx
file. We can replace the entire useState
and useEffect
at the top of the component with a simple call to initial the Basis Theory service:
const { bt } = useBasisTheory(process.env.NEXT_PUBLIC_BT_ELEMENTS_API_KEY!, {
elements: true,
});
Next, we can further simplify the component by replacing the entire Elements
provider usage with the much simpler BasisTheoryProvider
:
{bt ? (
<BasisTheoryProvider bt={bt}> <ElementsForm /> </BasisTheoryProvider>
) : (
<p>Loading...</p>
)}
We also no longer need to pass the Payment Intent from Stripe into the ElementsForm
component. Already our code is looking much simpler and easier to understand! (Don’t forget to import useBasisTheory
and BasisTheoryProvider
if your IDE didn’t do it automatically!)
View the changes to the file on GitHub.
The Payment Form
Now that we’ve configured the Basis Theory Provider, we can update the ElementsForm.tsx
component to use Basis Theory Elements instead of Stripe. First, we need to initialize a ref that will be used to reference the secure data, as well as get an instance of the Basis Theory service from the provider:
const { bt } = useBasisTheory();
const cardRef = useRef(null);
Then we can replace the PaymentElement
from Stripe with the CardElement
from Basis Theory:
<CardElement id="card-element" ref={cardRef}/>
The rest of the front-end changes happen in the handleSubmit
function. When using Basis Theory with Stripe, you do not need a Payment Intent until you are ready to charge the card. Therefore, we can remove all the calls to Stripe in this function, and instead create a Basis Theory Token using the bt
service and cardRef
we initialized earlier.
let token: Token | undefined;
try {
token = await bt.tokens.create({
type: 'card',
data: cardRef.current,
});
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'Unknown Error');
// eslint-disable-next-line no-console
console.error(error);
}
Then, instead of attempting to confirm a Payment Intent with Stripe from the front end, we will send the token to our Basis Theory API to process the payment with Stripe. Moving the processing to the backend allows you to store the Token in your database if needed and gives you full control over how the payment is processed without worrying about client-side interference.
// Charge the card using our API
const response = await fetchPostJSON('/api/charge_with_reactor', {
token,
amount: input.customDonation
})
View the changes to the form on GitHub.
Updating the API
In the last step, we called the charge_with_reactor
endpoint, which doesn’t yet exist! NextJS uses a file-based routing convention: inside the api
folder, create a new folder called charge_with_reactor
and create an index.ts
file within it. This will hold our new API endpoint. Some boilerplate is needed to handle a request in NextJS, which I won’t cover here. (You can view the complete endpoint here for reference.) Let’s jump right into the body of the code:
const { raw } = await bt.reactors.react(process.env.BT_REACTOR_ID as string, {
args: {
card: `}`,
},
});
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: CURRENCY,
payment_method: raw.id as string,
confirm: true,
});
So what’s going on here, and what’s happening behind the scenes with that Reactor? When our updated <span class="code">ElementsForm</span>
calls this new endpoint, we send the Basis Theory Token and the amount to charge. The first thing we do is pass the Token ID to the Reactor, making sure that the value is enclosed in double curly braces (<span class="code"></span>
). When the Reactor starts, the Token is automatically detokenized, and the Reactor has the complete PAN, CVC, and expiration. The Reactor we are invoking creates a Payment Method within Stripe for us to charge with their SDK, and it returns the value in the <span class="code">raw</span>
property of the response.
Using the Payment Method ID we just received, we then pass that data to Stripe, ensuring we set confirm: true
to capture the payment immediately. And that’s it! You have successfully decoupled your application from Stripe by capturing and storing the card data with Basis Theory, then charged the card from your API without your application ever seeing the plaintext card data!
What’s Next
What’s next depends on your application and needs. Here are some other things to consider:
- Migrating your Stripe payment methods to Basis Theory Tokens. Stripe provides a way to export all your PAN data. You can transform the export into a CSV and upload it securely to Basis Theory to bulk import and tokenize all of your data.
- Secure PII with Basis Theory. Elements can help you secure your entire application by tokenizing more than just card data. Basis Theory Elements provides a flexible
`TextElement`
that you can use to capture any input (such as social security numbers, bank accounts, addresses, and more). - Implement other payment processes. Basis Theory Reactors support multiple payment processors. With very little change to your API logic, you can process payments with a processor other than Stripe, giving you the flexibility to fully control your payment flow.
Have feedback or questions? Join us in our slack community.