Litestore · Developer docs
Platform services

Providers

Some external services sit behind an interface in Litestore and others are called directly, which determines how you swap, replace or add one.

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 are behind an interface and Litestore calls others directly.

Postgres 15+Your data
RedisCache and rate limits
S3 storageImages
LitestoreOne Next.js app
StorefrontAdmin
ResendEmail and sign-in links
InngestBackground jobs
StripeOptional
Litestore is one Next.js app that connects to Postgres, Redis, S3, Resend, Inngest and, optionally, Stripe.

Where Litestore uses an interface, a different implementation replaces the current one. Litestore calls the other services directly, because an interface there would add an extra layer without offering a realistic alternative. Some interfaces are also easier to swap than others, as the sections below explain.

Service interfaces

ConcernWhat ships todayWhere the seam is
PaymentsStripePaymentProvider in platform/types/payments.ts covers checkout, refund and webhook verification
TaxFlat per-channel rate, or Stripe TaxTaxProvider in modules/checkout/tax/provider.ts has resolve, with optional commit/reverse
AIGoogle Gemini, Anthropic, OpenAI via the AI SDKAI_PROVIDER chooses one, and modules/assistant/model.ts is the only model resolver
NewsletterKlaviyo, Mailchimp, ConvertKit, Buttondown, Loops, Beehiiv, or a plain webhookNewsletterProvider in modules/customers/newsletter/providers.ts
Merchant feedsGoogle Merchant, Meta CommerceThe adapters in modules/integrations/merchant/, selected in modules/integrations/connections.ts
ImportsShopify, CSVThe resumable runtime in modules/integrations/import/runtime/, which the Shopify and file importers share

All of these adapters work the same way. The adapter talks to the external service, and Litestore stores the resulting records in the store's database. For example, when a payment is captured, Litestore writes a row to the append-only PaymentTransaction ledger, whichever provider processed the payment. Because of this, switching providers doesn't change existing reports.

Two of these interfaces are simpler than the table suggests, and the tax interface has more branching than the others.

Adding payment providers

PaymentProvider

Use platform/stripe-payment-provider.ts as the example. createCheckout is required, while refundPayment, verifyWebhook and getCheckoutSession are optional.

Provider branch

The branch goes in getPaymentProvider in modules/orders/payments/payment-providers.ts.

Litestore has 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, and the configuration itself comes from STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in the environment.

Direct integrations

Litestore calls some services directly, because there's no realistic alternative implementation that an interface would make room for.

Resend

All email goes out through modules/email/send.ts, which has two entry points. sendEmail sends right away, for sign-in links and broadcasts. queueEmail sends transactional mail through Inngest.

Every email is a React component in modules/email/templates/, and every send writes an EmailLog row. To replace Resend, you edit this file and the client in platform/resend.ts.

S3

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, so switching between these services is a configuration change, without changing code.

Product search runs in Postgres without any extension, so you don't need a separate search cluster.

identifyCandidates in modules/storefront/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, and it deliberately does not use pg_trgm.

The fuzzy and synonym phases are fail-safe, so if either one throws an error, Litestore still returns the ILIKE results.

Required services

env.ts defines which services are required and which are optional. The app does not boot without these variables:

DATABASE_URL
BETTER_AUTH_SECRET
BETTER_AUTH_URL
REDIS_REST_URL
REDIS_REST_TOKEN
NEXT_PUBLIC_SITE_URL
NEXT_PUBLIC_SITE_EMAIL

The four S3_* credentials are required as well. In production, RESEND_API_KEY and RESEND_SENDER_EMAIL are required too, because sign-in works by sending a magic link; outside production, email is only printed to the console.

When you leave an optional service unset, Litestore hides the parts of the app that use it. Stripe, every AI provider key, Plausible, PostHog, GA4, Meta and Sentry, the Shopify app credentials and all newsletter providers are optional. Google OAuth is optional too, and platform/auth/server.ts registers it only when both AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET are 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 failures

Because the rate limiter runs during sign-in, the Upstash client is configured to retry only once after a short delay. When Redis is unreachable, the limiter stops quickly and lets the request through, so the user isn't left waiting after they click "Send magic link".

Sign-in links requested this minute5 / limit 3
Limit
SentSentSentBlockedBlocked
The sign-in rate limiter lets the request through when Redis is unreachable.

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 can't be swapped out

Those guarantees depend on Postgres features, so Litestore doesn't support other databases.

Adding services

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 or the tax engine.

Call it directly when there is one implementation and no realistic second one. Replacing the S3 client, the Resend send path or the Postgres search means changing one file or one function, and an interface over them would only add a layer to read through.

In either case, the provider should only handle the external call, and the store's database should keep the record. If switching providers would change the stored history and not only the integration code, the interface is in the wrong place.

Next steps

On this page