1. Setup
Documentation
  • Back to home
  • StripeKit
  • Installation
  • Getting Started
  • Choosing a Mode
  • Type Reference
  • Setup
    • Configuration
    • Timezones
    • Storage Adapter
    • Money and Currency
    • Error Handling
  • Stripe
    • Elements
  • Modules
    • Overview
    • Customers
      • Overview
      • List
      • Retrieve
      • Find or Create by Email
      • Create
      • Update
      • Sync
      • Delete
    • Payment Methods
      • Overview
      • Create Setup Intent
      • List
      • Attach
      • Detach
      • Set Default
      • Sync
    • Payments
      • Overview
      • Pay With Saved Method
      • Create
      • Retrieve
      • Confirm
      • Cancel
      • Sync
    • Checkout
      • Overview
      • Get
      • Create
      • Submit Fields
      • Apply Coupon
      • Mark Complete
    • Subscriptions
      • Overview
      • Create
      • Retrieve
      • Cancel
      • Resume
      • Toggle Collection Method
      • Update Fields
      • Apply Promotion Code
      • List By Customer
      • Find By Metadata
      • Sync
    • Invoices
      • Overview
      • Retrieve
      • List By Customer
      • List By Subscription
      • Pay With Saved Method
      • Void
      • Finalize
      • Sync
    • Coupons
      • Overview
      • Create
      • Validate
      • Apply To Subscription
      • List
      • Deactivate
    • Webhooks
      • Overview
      • Process
      • Events Reference
    • Sync
      • Overview
  1. Setup

Storage Adapter

This page explains, in plain terms, what a storage adapter actually is, why StripeKit works fine without one, and exactly what changes once you add one. If you've read this before and it still didn't click, start with The short version.

The short version#

Stripe is the source of truth for billing data. Your own app almost certainly also wants a copy of that data in its own database, so you can, for example, show a user's subscription status on a dashboard page without calling the Stripe API on every page load.
A storage adapter is just a small PHP class you write that tells StripeKit how to save a copy of that data into your own database. You write one method per thing you want saved (saveCustomer(), saveSubscription(), and so on). StripeKit calls these methods automatically at the right moments, you never call them yourself.
If you don't write a storage adapter, StripeKit still works completely normally, every method still creates, reads and updates real objects in Stripe. The only difference is that nothing gets copied into your own database, and two specific features (in-progress custom checkout sessions, and webhook duplicate-detection) fall back to being held only in your PHP process's memory, which is fine for local development but not for a real deployment. Both of these points are explained fully below.

Do you need one at all?#

Your situationDo you need a storage adapter?
You're prototyping locally, or Stripe's own dashboard is the only place you look at billing dataNo. Skip this entirely.
You want to show subscription/invoice/payment status inside your own app without calling Stripe on every page loadYes.
You're running your app on more than one server process, or behind a load balancer, or with more than one workerYes, at minimum implement saveCheckoutSession() / getCheckoutSession() and hasProcessedWebhookEvent() / markWebhookEventProcessed(), see Why multi-instance deployments need this below.
You want your database to auto-update itself when Stripe events happen (a customer's card gets updated, an invoice gets paid)Yes.

How StripeKit decides whether you "have" a storage adapter#

This is the part that trips people up, so it's worth being explicit: StorageAdapter is an abstract PHP class with empty, no-op methods, not an interface. Every method already exists and does nothing by default. When you write your own adapter, you extend StorageAdapter and only override the specific methods you actually care about.
Internally, StripeKit checks whether you actually override a given method before calling it, using PHP reflection ($adapter->overrides('methodName')). This matters for exactly two methods, saveCheckoutSession() / getCheckoutSession() and hasProcessedWebhookEvent() / markWebhookEventProcessed(), where calling the no-op default silently would hide a real problem. For those two pairs, if you haven't overridden them, StripeKit falls back to in-process memory instead and logs a warning, rather than pretending your data was saved. Every other method (saveCustomer(), saveSubscription(), savePayment(), saveInvoice(), savePaymentMethods(), saveCoupon(), markInvoiceDeleted()) is simply called if a storage adapter exists at all; if you didn't override it, it just quietly does nothing, which is the correct behavior for a save method you don't care about.

Writing your own adapter#

Override only what you need. A typical adapter backed by SQL looks like this (trimmed for clarity, a full PDO/Postgres example is in examples/storage-adapter-postgres.php):

Every method, and exactly when StripeKit calls it#

MethodCalled byWhat it's for
findUserByEmail(string $email)customers->findOrCreateByEmail(), checkout->create()Look up your own user record (and their stripeCustomerId, if you already have one) by email, before falling back to creating a new Stripe customer.
findUserById(string|int $id)checkout->create()Look up your own user record by your internal ID, to resolve their existing stripeCustomerId.
findUserByStripeCustomerId(string $customerId)Reserved for your own use; not currently called internally by any StripeKit moduleLook up your own user record by Stripe customer ID.
saveCustomer(array $record)customers->create(), update(), delete(), sync()Persist a KitCustomerRecord.
saveSubscription(array $record)subscriptions->create(), cancel(), resume(), toggleCollectionMethod(), updateFields(), applyPromotionCode(), sync(); also sync->everythingForCustomer()Persist a KitSubscriptionRecord.
savePayment(array $record)payments->confirm(), cancel(), payWithSavedMethod(), sync()Persist a KitPaymentRecord. Note: payments->create() in the api flow does not call this, see the note on that page.
saveInvoice(array $record)invoices->payWithSavedMethod(), voidInvoice(), finalize(), sync(); also sync->everythingForCustomer()Persist a KitInvoiceRecord.
savePaymentMethods(string|int $userId, array $records)paymentMethods->list() and sync(), only when a $userId argument is passedReplace (not merge) a user's full list of saved payment methods.
saveCoupon(array $record)coupons->create()Persist a KitCouponRecord.
markInvoiceDeleted(string $stripeInvoiceId)invoices->voidInvoice(); also the invoice.deleted webhook eventFlag an invoice as deleted/void in your own schema.
saveCheckoutSession(array $session)checkout->create(), applyCoupon(), markComplete()Persist in-progress checkout session state. See below.
getCheckoutSession(string $checkoutId)checkout->get(), submitFields(), applyCoupon()Read back in-progress checkout session state. See below.
hasProcessedWebhookEvent(string $eventId)webhooks->process()Check whether a webhook event was already handled. See below.
markWebhookEventProcessed(string $eventId, string $type)webhooks->process()Record that a webhook event has now been handled. See below.

Why checkout sessions need this#

A checkout session created by checkout->create() carries state that has nowhere to live in Stripe itself, most importantly any custom field values you collect with customFields. StripeKit has to keep that state somewhere between the moment you create the session and the moment the customer finishes paying.
If you implement saveCheckoutSession() / getCheckoutSession(), that state lives in your own database. If you don't, StripeKit keeps it in a plain PHP array in memory, for the lifetime of that one PHP process. This is completely fine for local development and single-process testing. It breaks in a real deployment for two reasons: the array is wiped on every restart or deploy, and if you're running more than one server process (which almost every production deployment does), a session created on one process is invisible to a request that lands on a different process a moment later, checkout->get() will throw NotFoundError even though the session genuinely exists.

Why webhook idempotency needs this#

Stripe may deliver the same webhook event more than once, this is expected and documented Stripe behavior, not a bug. webhooks->process() is built to handle that safely, but only if it can remember which event IDs it has already seen.
If you implement hasProcessedWebhookEvent() / markWebhookEventProcessed(), that memory lives in your own database, correctly shared across every server process, and survives restarts. If you don't, StripeKit falls back to a PHP array in process memory, exactly like the checkout session fallback above, with the same limitation: fine for a single local process, unsafe for anything running more than one instance, since a duplicate delivery landing on a different process won't be recognized as a duplicate, and your handler (and any automatic sync) will run twice.

Why multi-instance deployments need this#

Put together, the two fallbacks above are the entire reason the "do you need a storage adapter" table above singles out multi-instance deployments. If your app ever runs as more than one PHP process at the same time, whether that's multiple workers, containers, or servers behind a load balancer, implement at minimum these four methods:
Everything else (saveCustomer(), saveSubscription(), and so on) is about keeping your own database's copy of the data up to date, it's valuable, but skipping it never causes incorrect billing behavior the way skipping these four can.

Full method reference#

See src/Contracts/StorageAdapter.php in the package source for the exact PHP signatures and return types of every method, or the table above for a plain-English summary of each one. See also the Type reference for the exact shape of every record passed into a save...() method.
Previous
Timezones
Next
Money and Currency
Built with