Providers
Learn which external services sit behind a swappable interface, which are integrated directly, and which environment variables the app refuses to boot without.
Some external services in Litestore sit behind an interface a second implementation could fill.
Others are integrated directly, because a seam would add indirection without adding a second implementation.
This page names which is which, and what "swappable" actually means in each case. The word does more work in some rows below than in others.
Behind an interface
| Concern | What ships today | Where the seam is |
|---|---|---|
| Payments | Stripe | PaymentProvider in types/payments.ts — checkout, refund, webhook verification |
| Tax | Flat per-channel rate, or Stripe Tax | TaxProvider in services/tax/provider.ts — resolve, with optional commit/reverse |
| AI | Google Gemini, Anthropic, OpenAI via the AI SDK | AI_PROVIDER selects; lib/ai/model.ts is the one 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, 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 — async generators over one resumable runtime |
The pattern is the same everywhere: the adapter moves data, the record stays in your database.
A payment capture lands as a row in your own append-only PaymentTransaction ledger whichever provider moved the money, so swapping a provider never rewrites your reporting.
Two of these are thinner than "swappable" suggests, and it is worth knowing which.
Payments is a branch
getPaymentProvider(name) is a branch, not a registry.
An unknown name throws rather than falling back to Stripe.
Newsletter is an order, not a setting
getNewsletterProvider() returns the first provider whose environment variables are set, in a fixed order:
Klaviyo → Mailchimp → ConvertKit → Buttondown → Loops → Beehiiv → webhookThere is no setting that picks one.
Tax has the most real branching
ENABLE_TAXES gates it entirely. Off, and getTaxProvider returns the zero-tax provider — which is what the taxes settings page already tells the operator.
On, the channel's stored taxProvider selects:
manualcomputes the flat channel ratestripe_taxcalls Stripe's calculation API, and falls back to the flat rate on any error or missing destination- Any other stored value logs a warning and falls back rather than pretending to work
Stripe Tax's commit and reverse — Stripe's own tax reporting, distinct from the customer refund — are not wired. They need the calculation id persisted on the order.
Adding a payment provider
Implement PaymentProvider, then add a branch for its name in getPaymentProvider in lib/payments/payment-providers.ts.
See lib/providers/stripe.ts for the shape. createCheckout is required; refundPayment, verifyWebhook, getCheckoutSession and findCoupon are optional.
There is no registry or priority machinery over a one-element set, and no admin control that selects a provider.
Settings → Payments reports which providers are configured and verifies the Stripe connection, but the configuration itself is STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in the environment.
Direct integrations
Some dependencies are integrated directly, because a seam would add complexity without adding flexibility.
Resend
lib/email.ts is the one send path, with two entry points: sendEmail for immediate admin-initiated sends, and queueEmail for transactional mail through Inngest.
Templates resolve from an active EmailTemplate row first, then fall back to the React components in emails/. Every send writes an EmailLog row.
Replacing Resend means editing one file.
The S3 API
Already a de-facto interface — AWS, R2 and MinIO all speak it.
The client reads S3_ENDPOINT and uses path-style addressing, so pointing it at any of them is configuration, not code.
Search
Postgres does the identification, with no extension required.
identifyCandidates (lib/search/candidates.ts) expands synonyms from a static map, runs ILIKE over name, description, tags and category, then adds near-misses using trigram similarity computed in JavaScript — deliberately not pg_trgm.
The fuzzy and synonym phases are fail-safe: an error there never stops the ILIKE results from returning.
There is no separate search cluster to run.
Optional by default
Optional is not the same as unconfigured everywhere. env.ts splits them plainly.
Required, or the app does not boot:
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 too.
Resend is required because sign-in sends a magic link.
Optional, and their surfaces hide when unset:
- Stripe
- Every AI provider key
- Plausible, PostHog, GA4, Meta, Sentry
- The Shopify app credentials
- All newsletter providers
- Google OAuth — the provider is registered in
lib/auth.tsonly when bothAUTH_GOOGLE_IDandAUTH_GOOGLE_SECRETare present
A store with no AI key shows no AI buttons. A store with no Stripe key still browses and builds carts.
Redis fails open
The rate limiter runs inside the sign-in path.
So the Upstash client is configured with a single quick retry: an unreachable Redis becomes a fast fail-open rather than a long hang on "Send magic link".
The database
Litestore is built on PostgreSQL.
Constraints, append-only ledgers, and case-insensitive unique indexes are Postgres features the platform's guarantees are written against.
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, or when the choice is genuinely the operator's — which newsletter platform, which social network, which tax engine.
Integrate it directly when there is one implementation and no realistic second. The S3 client, the Resend send path and the Postgres search are all one file or one function away from being replaced, and an interface over them would only add a layer to read through.
Whichever way it goes, the same rule decides whether it was done correctly:
The provider moves the data; your database keeps the record. If swapping a provider would rewrite your history rather than just your integration, the seam is in the wrong place.
Next Steps
Configuration
Learn which environment variables env.ts requires, which ones are optional, and what each optional group configures.
Guarantees
Learn which money, refund, stock, identity, visibility and cache promises Postgres enforces, which ones a harness check enforces, and which ones are explicitly not guaranteed.