Narration / Transcript

Integrating Stripe with SvelteKit: Dynamic pricing and Payment Security

This is what the narrator says, not what the page shows: equations are read as sentences, code blocks are described, and citations are spoken as citations.

57 blocks · 2,544 spoken words · narrated Jul 26, 2026

  1. 0:00

    Introduction

  2. 0:02

    Integrating Stripe into a SvelteKit application, especially when dealing with dynamic product pricing and robust webhook validation, can be challenging. While building a client's business application using SvelteKit and PostgreSQL (with Drizzle ORM), I encountered the need for a streamlined integration with Stripe. The goal was to use minimal dependencies while supporting features such as dynamic pricing, product images, and secure payment verification via webhooks, all without requiring database updates. This article provides a comprehensive guide to this entire process, addressing a gap in existing resources. It should be noted that we cover the concepts here, and as a result, it's framework agnostic. You can use Next JS or Nuxt JS in place of Svelte Kit since they support server endpoints, form actions, etc.

  3. 0:56

    Live version

  4. 0:58

    Source code Sirneij/digibooks

  5. 1:01

    A simple demo app that integrates Stripe with SvelteKit Svelte TypeScript CSS JavaScript HTML

  6. 1:08

    System architecture and requirements

  7. 1:11

    To demonstrate this integration, we will build a minimalist digital book application. The application will allow users to browse books, add them to a cart, and purchase them using Stripe. Access to purchased content will be granted based on the email provided during checkout.

  8. 1:29

    Prerequisite

  9. 1:31

    You need to create a new SvelteKit application. If you do not have one yet, just follow these commands: The shell code below is 1 line.

  10. 1:40

    When prompted during npx sv create digibooks:

  11. 1:44

    Choose Svelte Kit minimal barebones scaffolding for your new app when asked Which Svelte app template?.

  12. 1:51

    Select your preferred options for other features. For this project, I opted for TypeScript syntax, Prettier, ESLint, Tailwind CSS, and Drizzle ORM (with lib SQL).

  13. 2:03

    Also, we will be using the stripe library, which should be installed: The shell code below is 1 line.

  14. 2:10

    Implementation

  15. 2:12

    Step 1: Create the database schema

  16. 2:15

    When you opt for drizzle ORM while creating the project with npx sv create, src slash lib slash server slash db slash index dot ts (with a dot env entry for DATABASE URL), src slash lib slash server slash db slash schema dot ts and drizzle dot config dot ts alongside db:push, db:migrate and db:studio entries in package dot json will be made available. Let's open src slash lib slash server slash db slash schema dot ts and populate it with: The TypeScript code below is 28 lines, from src slash lib slash server slash db slash schema dot ts.

  17. 2:58

    Just some very basic SQL tables to list books and record purchases linked to customer emails, which can then be used to grant access to the digital content.

  18. 3:09

    To effect this change, run: The shell code below is 1 line.

  19. 3:13

    and choose the Yes when prompted. Now, our database tables have been created. We will also add a simple "money" formatter that takes into consideration the cents -based pricing in src slash lib slash utils slash helpers dot ts: The TypeScript code below defines format Money and valid Amount, 19 lines.

  20. 3:34

    To support a Turso-hosted SQLite instance, we need to modify src slash lib slash server slash db slash index dot ts and drizzle dot config dot ts to avoid this error: The shell code below is 14 lines.

  21. 3:51

    Open up drizzle dot config dot ts and make it look like this: The TypeScript code below is 16 lines, from drizzle dot config dot ts.

  22. 4:01

    and src slash lib slash server slash db slash index dot ts: The TypeScript code below is 16 lines, from src slash lib slash server slash db slash index dot ts.

  23. 4:16

    We are conditionally (based on the DATABASE URL) changing the database dialect to either sqlite or turso. If turso and auth Token are required to successfully connect to the instance. The dialect in drizzle dot config dot ts is conditionally set to turso when a non-file DATABASE URL is used, which allows db Credentials to correctly utilize the auth Token. If you are not using a Turso SQLite instance, you may not need to worry about this.

  24. 4:42

    Seeding the database

  25. 4:44

    So that we can have enough data to work with, this article's GitHub repository contains two files: books dot json and seed dot ts. The former contains some dummy data in JSON, while the latter has the code to load (not optimized) this data into the DB. I also added an entry in the script section of package dot json where we utilize the experimental node's experimental transform types flag to run the TypeScript file. You can run these commands in succession (locally) after deploying or at the start of development of your app: The shell code below is 2 lines.

  26. 5:21

    Step 2: Cart and carting store

  27. 5:24

    This app will not deal with the intricacies of authentication, so there will not be "users". However, we still need a way to keep track of what the current user wants to buy. As a result, we need a carting system that won't need to store its data in the database. We will combine the browser's local Storage and Svelte 5 runes to achieve a reliable reactivity. Create a src slash lib slash states slash carts dot svelte dot ts and populate it with: The TypeScript code below defines load Cart From Storage, save Cart To Storage and create Cart State, 95 lines.

  28. 6:01

    This is a basic CRUD carting process while making items reactive using the power of Svelte 5 rune. We referenced a type here, and this is the definition: The TypeScript code below defines Cart Item, Session Metadata and Pagination Metadata, 24 lines, from src slash lib slash types slash cart dot ts.

  29. 6:22

    Just some additional types that will be used later. Now, we can add a simple utility function (not extremely necessary) for the addToCart functionality: The TypeScript code below defines add To Cart, 9 lines, from src slash lib slash utils slash helpers dot ts.

  30. 6:41

    Step 3: Data loading and frontend

  31. 6:44

    Though we will not delve into the intricacies of styles and tailwindcss stuff as those are not the focus of this article, beware that I made custom changes to the app's src slash app dot css, src slash routes slash plus layout dot svelte and of course, each of the pages: src slash routes slash plus page dot svelte, src slash routes slash id slash plus page dot svelte, src slash routes slash cart slash plus page dot svelte, and src slash routes slash purchases slash plus page dot svelte with some glasmorphism effects.

  32. 7:12

    Let's see what src slash routes slash plus page dot server dot ts looks like: The TypeScript code below defines offset, 73 lines. Svelte automatically generates dollar types:

  33. 7:25

    SvelteKit automatically generates TypeScript definitions for your routes, including the data shapes for load functions and page props. When you see import type Page Server Load from. slash dollar types, or use Page Data (often imported from. slash dollar types as well, or inferred), these types are derived from your load function's return signature and any route parameters. This provides excellent type safety between your server-side data loading and your client-side Svelte components. If you modify the data structure returned by a load function, SvelteKit's type generation will reflect these changes, helping you catch errors at build time.

  34. 8:02

    It simply fetches data from the DB with search, filtering, and pagination support. To prevent request waterfalls, we use Promise dot all to run the counting and data retrieval queries in parallel. This can improve performance. In src slash routes slash plus page dot svelte, we simply get this data via the data props and render them, excluding other features: The HTML code below is 43 lines.

  35. 8:28

    Refer to the repository for the full code and the Book Card and Pagination components. Same goes with src slash routes slash id slash plus page dot svelte where its load function is quite basic: The TypeScript code below is 20 lines, from src slash routes slash id slash +page dot server dot ts.

  36. 8:50

    Then comes the slash purchases route, which provides an interface for users to see the products they purchased. It requires that users input their email address. This is the page's plus page dot server dot ts: The TypeScript code below is 53 lines, from src slash routes slash purchases slash +page dot server dot ts.

  37. 9:13

    To reduce trips to the Database, we joined the purchases and books. Its frontend simply displays this data.

  38. 9:20

    In the next subsection, we will finally integrate Stripe!

  39. 9:24

    Step 4: Payment integration

  40. 9:27

    It's now time to integrate Stripe with our app so that users can securely pay for their books of choice. There are multiple ways to integrate Stripe or accept payments with Stripe:

  41. 9:38

    Prebuilt Checkout page: Stripe hosts the payment page. You redirect users to Stripe, and Stripe redirects them back to your site after payment. This is the quickest way to get started and ensures PCI compliance is handled by Stripe.

  42. 9:54

    Payment Element: Embeddable UI components that allow you to design a custom payment form on your site while still leveraging Stripe's infrastructure for processing and PCI compliance. Offers more customization than the prebuilt Checkout page.

  43. 10:10

    Custom payment flow: Build your entire payment UI from scratch and use Stripe APIs (like Stripe.js on the frontend and the Stripe SDK on the backend) to process payments. This offers maximum control over the user experience but also requires more effort to implement and manage PCI compliance.

  44. 10:31

    In this article, we'll go with the first option. However, there is a plan to extend to the other two in future articles (as well as integrate other payment processors such as Squareup and Braintree).

  45. 10:44

    To start, create a payments dot ts file in src slash lib slash server slash payments dot ts (you can use server in NextJS): The TypeScript code below defines create Checkout Session, 79 lines, from src slash lib slash server slash payments dot ts.

  46. 11:04

    To instantiate a Stripe instance, a STRIPE SECRET KEY is required — gotten from your Stripe developer dashboard. In this function, we only touched on some of the data accepted by a Stripe checkout session. You can provide much more data depending on your needs. In the code, we supplied the products (books) being paid for alongside their images and descriptions with the unit amount (in cents, stripe takes cents for USD and EUR. It takes whatever is the 1 over 100 of your supported currency. This is to avoid floating point issues). The mode is a one-time payment. You could use subscription for recurring payments or setup to charge customers later. We also provided the routes for successful and unsuccessful processing. We even made collecting customers' billing addresses mandatory. After a payment has been confirmed (via a webhook, to be implemented later), we need some data about the books and other things the user bought so we can fullfil them (here create purchases entry(ies)), that's a potential use case of Stripe dot Checkout dot Session Create Params dot metadata. It only takes primitives (string, number, null) as objects' values, though that's why the books property in Session Metadata is a string (JSON string of an array of book Id and quantity). This is just the tip of the iceberg. You can do much more. After supplying these data, Stripe internally sends requests to its API(s), and a checkout session identification is returned alongside other important details such as the section URL (where you should redirect users to for them to make payment). Here is a truncated example response: The JSON code below is 33 lines.

  47. 12:39

    With that, we can now implement the form action that uses this function: The TypeScript code below is 56 lines, from src slash routes slash cart slash +page dot server dot ts.

  48. 12:52

    We simply retrieve the needed data from the form, slightly process it, and send it to Stripe. Beware of putting a redirect in try...catch:

  49. 13:02

    Wrapping a redirect in try.. dot catch in SvelteKit form action almost always returns the `catch` block (with errors). You should handle errors using a different approach.

  50. 13:13

    Now, let's see the cart page (only shows the form and URL handling part): The HTML code below defines update Quantity, remove Item and clear Cart, 212 lines, from src slash routes slash cart slash +page dot svelte.

  51. 13:30

    Just some styles and progressive form enhancements.

  52. 13:34

    Step 5: Confirming purchases with webhook

  53. 13:37

    Currently, even after a successful payment, users cannot access what they paid for. This is unfair. While we strive to fulfill every payment made, we need to be careful, though, as people can game the payment flow, providing a fraudulent payment. This is why we didn't create any purchases entry at the point of making payments. One way we could fulfill an order is to manually create the purchase entries from our Stripe dashboard. This is "manual" and hence prone to errors and very tedious. There is an automatic way to do this with Stripe, and it is called webhook — a mechanism that allows one application to notify another application in real-time about specific events. Before we can fulfill orders, we want Stripe to notify us that payments have indeed been made (via its checkout dot session dot completed or checkout dot session dot async payment succeeded hooks). We need to patiently listen to this, and a good way is via an endpoint. To locally test this, kindly look into this stripe guide. You will need to create a destination (the url of your endpoint where you want stripe to send communications to, for this app, it'll be slash api slash webhooks slash stripe) and you will be prompted to select the hooks you want to listen to as well (I opted for only checkout dot session dot completed). After this, a webhook secret will be generated for you. Copy it and save it as STRIPE WEBHOOK SECRET in your environment variable. Locally, it will be generated when you first run stripe listen events, and so on (you have a lot of events to listen to!). To "trigger" an event locally, just run stripe trigger checkout dot session dot completed in another terminal while the listen command is running in another. Your app should also be running. Here is our endpoint code: The TypeScript code below defines POST, 90 lines, from src slash routes slash api slash webhooks slash stripe slash +server dot ts.

  54. 15:21

    To avoid fraudulent event notifications, we ensure the event is signed and verified. Unsigned and/or unverified events are outrightly rejected. After that, the event is constructed, and since we are only interested in checkout dot session dot completed (covers most successful one-time payments) and async payment succeeded (important for asynchronous payment methods (e.g., some bank transfers)) type, that's what we listen to. From there, we retrieve the details we sent previously during payment (from metadata) and from it create purchases entries. This is done in an idempotent way so that webhook retries by Stripe or anything won't create duplicates, which would result in losing money.

  55. 16:05

    That's it! Thank you for your time!

  56. 16:08

    Outro

  57. 16:10

    Enjoyed this article? I'm a Software Engineer and Technical Writer actively seeking new opportunities to impact and learn, particularly in areas related to web security, finance, healthcare, and education. If you think my expertise aligns with your team's needs, let's chat! You can find me on LinkedIn and X. I am also an email away.