Integrating SvelteKit and Stripe Checkout
Turning the SvelteKit demo into a SAAS application.
This article is a description of the sveltekit-stripe repository, which can be found here. The SvelteKit demo application comes with a handy Counter feature. It has a number, and when a user clicks on a ’+’ button, the number increases. It also comes with a ’-’ button. That makes the number go down. It’s awesome! The one thing the SvelteKit team failed to do was monetize it. So that’s what we’ll do here, by turning it into a subscription service using Stripe Checkout.
Stripe Checkout allows you to send your users to Stripe hosted checkout pages to buy your products and services without having to code up the checkout form yourself. This can save a lot of time.
Stipe Checkout allows users to buy your products and services from checkout pages hosted by Stripe. This saves you the time and effort of building the forms yourself, and users know they can trust Stripe.
Stripe account and API keys
To use this repository you must have a Stripe account and API keys. The API keys are required for programatically interacting with your account. Anyone who has a private API key can perform any action on the Stripe account it is associated with. A malicious actor with your key could cause mayhem, so it is important to keep it safe. For that reason, the private key is only used on the server. In SvelteKit, endpoints always run on the server, so it is safe to use the Stripe private API key there. It’s still not a great idea to hardcode the key. Doing so would cause it to end up in your code repository. It’s best to keep your API keys in environment variables. We use a .env
file to store the environment variables, which is listed in .gitignore
so it isn’t promoted to the git repo. The stripe library is initialized in the src/routes/stripe/_stripe.ts
file. The filename is prefixed with an underscore so it is hidden from the SvelteKit router. Any files in the routes
directory become either pages or endpoints unless they are prefixed with an underscore. To initialize the Stripe library we give it the private API key and the apiVersion that we are using. We’ll export the initialized library so it can be used in the endpoints.
import Stripe from 'stripe';
import dotenv from 'dotenv';
dotenv.config();
const stripe = new Stripe(process.env["STRIPE_SECRET_KEY"], {
apiVersion: '2020-08-27'
});
export default stripe;
Creating the subscription plans.
To be able to create recurring payments, we need to create Product and Price entities on Stripe. This repo comes with a script that will create the entities for us and save them to a JSON file. Use the npm run stripe:init
command to run the script. We’ll load the JSON file to display product/price info to the client so they can choose what plan they want. It’s also possible to create these entities manually on the Stripe dashboard. There is more information about how to do that in this guide.
A SvelteKit endpoint, defined in src/routes/plans.json.ts
, is used to load the product/price information from the JSON file. This information is used to display the Price Cards and create the checkout session. We’ll use SvelteKit’s load function to get this information. load
is a function in the pages module context that can run either on the server or on the client. In the load function, we call the plans.json
endpoint to get the business model data and return it as a prop. Notice that the fetch function is passed as property to load. This is to ensure fetch works consistently between client and server. The return value has a props object containing the plans. These props are passed to the page component.
<script context="module" lang="ts">
export const prerender = true;
export async function load({ fetch }) {
const res = await fetch('/plans.json');
const plans = await res.json();
return {
status: 200,
props: {
plans
}
};
}
</script>
Creating the Checkout session.
A checkout session is where a user pays for the product. It is a page hosted by stripe. A checkout session is created by making a post request to the /stripe/checkout-session endpoint defined in src/routes/stripe/checkout-session.ts
. The endpoint is sent the id of the price item that the customer has chosen. Checkout sessions need to be created on the server, otherwise, the client could potentially make up their own price. Other information is also given here to configure the session. It is for a subscription and we accept card payments. We also provide URLs to redirect the user to when they complete their order or cancel the order. We’re using the host property of the request, so this works in test and production environments. The {CHECKOUT_SESSION_ID}
placeholder will be replaced with the actual checkout session-id and can be used to get information from stripe about the checkout session to display on the success page. On success, the endpoint returns the id of the newly created checkout session.
Back on the client, we now have the checkout session-id. This is used to redirect to the checkout session hosted on stripe. To do this we use the client-side Stripe.js library. For PCI (Payment Card Industry) compliance, the library must be loaded directly from https://js.stripe.com. You could load this with a script tag in the head of the document, but I chose to use @stripe/stripe-js
to load it. Its loadStripe
function just injects that script tag into your document, but I like it because it comes with the TypeScript types as well.
<svelte:head>
<script src="https://js.stripe.com/v3/"></script>
</svelte:head>
In src/lib/stripe/StripeProvider.svelte
we load and initialize Stripe.js. The public API key is injected into our app by Vite automatically. It needs to be prefixed with VITE_
so that Vite knows it is public. We set the library on a Svelte context to make it available to the rest of the application. In the src/lib/Pricing/index.svelte
component we get the initialized Stripe.js library. When the user chooses a plan we create the checkout session and then redirect them to the session by calling stripe.redirectToCheckout with the session-id. From this point, Stripe handles the rest of the checkout process.
async function choosePlan(plan) {
if (plan.price.id) {
const res = await fetch('/stripe/checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ priceId: plan.price.id })
});
const { sessionId } = await res.json();
stripe.redirectToCheckout({
sessionId
});
} else {
// If the user chooses the free plan we just send them directly to the counter.
goto('/counter');
}
}
When the user has paid for their subscription they will be redirected to the counter page in our application so they can get down to the important business of, uhh…, counting things.
Listening to Webhooks
If we have registered a webhook endpoint with Stripe, it will send events to that endpoint as they occur. This is how we will know a user has successfully paid for their services and we can allow them to access those services. In this repository, the webhook endpoint is defined at src/routes/stripe/webhook.ts
. Even though this endpoint is intended for Stripe to call, anybody who knows about it could call it if they so desired. To prevent malicious actors from sending us fake events, we need to verify that the event is coming from Stripe. Stripe signs the requests they make to our webhook so we can verify that they are who they say they are. Add a STRIPE_WEBHOOK_SECRET to the .env file will prompt the webhook to verify the signature before handling the event
if (WEBHOOK_SECRET) {
let event;
const signature = req.headers['stripe-signature'];
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
signature,
WEBHOOK_SECRET
)
data = event.data;
eventType = event.type
} catch (err) {
return {
status: 500,
body: {
error: err
}
}
}
}
At this point, you could save the user’s subscription status to your database so you know what levels of service to provide them. Check out the sveltekit-stripe repository to see how it all fits together. Feel free to get in touch with any questions or comments.