Providers
This page shows which external services you can swap, which ones Litestore calls directly, and which ones the app needs before it will start.
Litestore talks to several outside services for payments, email, storage and more. Read this page before you replace one of them or add a new one, because some services sit behind an interface and others are called directly.
An interface means a second implementation could take the place of the first. Litestore calls the other services directly, because an interface there would add a layer to read through without giving you a real alternative. "Swappable" also means more for some services than for others, and the sections below explain the difference.
Behind an interface
| Concern | What ships today | Where the seam is |
|---|---|---|
| Payments | Stripe | PaymentProvider in types/payments.ts covers checkout, refund and webhook verification |
| Tax | Flat per-channel rate, or Stripe Tax | TaxProvider in services/tax/provider.ts has resolve, with optional commit/reverse |
| AI | Google Gemini, Anthropic, OpenAI via the AI SDK | AI_PROVIDER chooses one, and lib/ai/model.ts is the only model resolver |
| Newsletter | Klaviyo, Mailchimp, ConvertKit, Buttondown, Loops, Beehiiv, or a plain webhook | NewsletterProvider in lib/newsletter/providers.ts |
| Social posting | Bluesky, Twitter/X | SOCIAL_PLATFORMS in lib/social-media/platforms.ts, with one client per platform |
| Merchant feeds | Google Merchant, Meta Commerce | The syncHandlers map in services/integrations/platforms/index.ts |
| Imports | Shopify, CSV | ImportAdapter in lib/import/adapters/types.ts, where async generators feed one resumable runtime |
Every one of these follows the same rule: the adapter moves the data, and the record stays in your database.
For example, when a payment is captured, Litestore writes a row to your own append-only PaymentTransaction ledger, whichever provider moved the money. Swapping the provider therefore never rewrites your reporting.
Two of these seams are thinner than "swappable" suggests, and you should know which ones.
Payments is a branch
getPaymentProvider(name) is a simple branch, not a registry.
If you pass an unknown name, it throws an error instead of falling back to Stripe.
Newsletter is an order, not a setting
getNewsletterProvider() returns the first provider whose environment variables are set. It checks them in a fixed order:
Klaviyo → Mailchimp → ConvertKit → Buttondown → Loops → Beehiiv → webhookNo setting lets you pick one.
Tax has the most real branching
ENABLE_TAXES turns tax on or off as a whole. When it is off, getTaxProvider returns the zero-tax provider, which matches what the taxes settings page tells the operator.
When it is on, the channel's stored taxProvider value decides what happens:
manualcalculates the flat channel rate.stripe_taxcalls Stripe's calculation API. On any error, or when the destination is missing, it falls back to the flat rate.- Any other stored value logs a warning and falls back instead of pretending to work.
Stripe Tax's commit and reverse are not wired up. These feed Stripe's own tax reporting and are separate from the customer refund. To support them, the order would need to store the calculation id.
Adding a payment provider
- Implement
PaymentProvider. Uselib/providers/stripe.tsas the example.createCheckoutis required, whilerefundPayment,verifyWebhook,getCheckoutSessionandfindCouponare optional. - Add a branch for its name. The branch goes in
getPaymentProviderinlib/payments/payment-providers.ts.
There is no registry or priority logic, because there is only one provider today. No admin control selects a provider either.
Settings → Payments shows which providers are configured and checks the Stripe connection. The configuration itself comes from STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in the environment.
Direct integrations
Litestore calls some services directly, because an interface would add complexity without adding flexibility.
Resend
All email goes out through lib/email.ts. It has two entry points. sendEmail sends right away and is used when an admin triggers a send. queueEmail sends transactional mail through Inngest.
Litestore first looks for an active EmailTemplate row. If there is none, it uses the React components in emails/. Every send writes an EmailLog row.
To replace Resend, you edit this one file.
The S3 API
The S3 API already works as a shared standard, because AWS, R2 and MinIO all support it.
The client reads S3_ENDPOINT and uses path-style addressing. You can point it at any of these services by changing configuration, without changing code.
Search
Postgres finds the matching products, and it needs no extension to do it.
identifyCandidates in lib/search/candidates.ts works in three phases. It first expands synonyms from a static map. It then runs ILIKE over the name, description, tags and category. Finally, it adds near-misses using trigram similarity that it computes in JavaScript. It deliberately does not use pg_trgm.
The fuzzy and synonym phases are fail-safe. If they error, the ILIKE results still come back.
You don't need to run a separate search cluster.
Optional by default
Some services are optional, but that doesn't mean every one of them can be left out. env.ts makes the split clear.
The app does not boot without these variables:
DATABASE_URL
BETTER_AUTH_SECRET
BETTER_AUTH_URL
REDIS_REST_URL
REDIS_REST_TOKEN
RESEND_API_KEY
RESEND_SENDER_EMAIL
NEXT_PUBLIC_SITE_URL
NEXT_PUBLIC_SITE_EMAILThe four S3_* credentials are required as well.
Resend is required because sign-in works by sending a magic link.
The following services are optional. When you leave them unset, the parts of the app that use them are hidden:
- Stripe
- Every AI provider key
- Plausible, PostHog, GA4, Meta and Sentry
- The Shopify app credentials
- All newsletter providers
- Google OAuth, which
lib/auth.tsregisters only when bothAUTH_GOOGLE_IDandAUTH_GOOGLE_SECRETare present
A store without an AI key shows no AI buttons. A store without a Stripe key still lets customers browse and build carts.
Redis fails open
The rate limiter runs during sign-in.
For that reason, the Upstash client retries only once, and quickly. When Redis is unreachable, the limiter gives up fast and lets the request through, instead of leaving the user waiting after they click "Send magic link".
The database
Litestore is built on PostgreSQL.
The platform's guarantees rely on Postgres features such as constraints, append-only ledgers and case-insensitive unique indexes.
PostgreSQL is a deliberate foundation, not a swappable setting
Committing to one database is what makes those guarantees possible.
Where a new dependency goes
Put a new dependency behind an interface when a second implementation already exists. Do the same when the operator should genuinely get to choose, as with the newsletter platform, the social network or the tax engine.
Call it directly when there is one implementation and no realistic second one. You can replace the S3 client, the Resend send path or the Postgres search by changing one file or one function. An interface over them would only add a layer to read through.
Whichever way you choose, one rule tells you whether you did it right:
The provider moves the data, and your database keeps the record. If swapping a provider would rewrite your history instead of just your integration, the seam is in the wrong place.