saveCustomer(), saveSubscription(), and so on). StripeKit calls these methods automatically at the right moments, you never call them yourself.| Your situation | Do you need a storage adapter? |
|---|---|
| You're prototyping locally, or Stripe's own dashboard is the only place you look at billing data | No. Skip this entirely. |
| You want to show subscription/invoice/payment status inside your own app without calling Stripe on every page load | Yes. |
| You're running your app on more than one server process, or behind a load balancer, or with more than one worker | Yes, 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. |
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.$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.examples/storage-adapter-postgres.php):| Method | Called by | What 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 module | Look 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 passed | Replace (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 event | Flag 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. |
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.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.webhooks->process() is built to handle that safely, but only if it can remember which event IDs it has already seen.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.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.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.