Configuration
Set the environment variables Litestore needs and choose which optional services to turn on.
You configure Litestore's connections to outside services through environment variables. The app needs some of them to start, and each optional group adds a feature.
env.ts defines which variables exist. It is a createEnv schema from @t3-oss/env-nextjs that validates every variable at startup. It also splits them into server-only values and NEXT_PUBLIC_* values that the browser can read.
Start your local file from .env.example. It lists every key that env.ts declares except VERCEL_URL, which Vercel injects. Most of its values are empty strings.
Environment variables only hold credentials and switches. The few business settings a store owner changes live in the admin under app/admin/settings/ and take effect without a deploy. Timings, thresholds and other tuning are typed constants in platform/config/, which you change in code.
Missing values
The schema turns on emptyStringAsUndefined, so Litestore treats KEY="" the same as not setting KEY at all. The empty values in .env.example don't satisfy a required variable.
Validation runs when the module loads. If one required value is absent, every request fails with Invalid environment variables, including requests to routes that never read that variable. The error message names the exact keys.
If you set SKIP_ENV_VALIDATION, Litestore skips the check entirely. The email script sets it, and vitest.config.ts sets it for the unit tests.
Required variables
The app won't start until you set every variable in this table.
| Variable | What it does |
|---|---|
NEXT_PUBLIC_SITE_URL | This is the public URL of the store, and it must be a valid URL. Litestore also uses it in emails, feeds, JSON-LD and checkout links. |
NEXT_PUBLIC_SITE_EMAIL | This is the public contact address, and it must be a valid email. |
DATABASE_URL | This is the Postgres connection string, and it must be a valid URL. |
BETTER_AUTH_URL | This is the base URL for sign-in. In most setups it is the same as the site URL. |
BETTER_AUTH_SECRET | Litestore signs sessions with this secret. |
REDIS_REST_URL | This is the Upstash Redis REST endpoint. |
REDIS_REST_TOKEN | This is the token for that endpoint. |
RESEND_API_KEY | Required only in production. Litestore signs people in with a magic link. Outside production no email is sent at all: every email, including the sign-in link, is printed in the terminal running bun run dev. |
RESEND_SENDER_EMAIL | Required only in production. This is the from-address for transactional email, and it must be a valid email. |
S3_REGION, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_ACCESS_KEY | These four values set up media storage, and any S3-compatible provider works. |
Local development
compose.yml runs Redis behind an Upstash-compatible REST proxy on port 8079,
and .env.example already points the two REDIS_REST_* values at it. The
rate limiter, the checkout payment lock and coupon reservations use it the
same way they use Upstash in production.
Defaults
These five variables have defaults, so they're optional.
| Variable | Default |
|---|---|
PORT | 8000 |
NODE_ENV | development |
VERCEL_ENV | development |
AI_PROVIDER | google |
ENABLE_TAXES | false |
VERCEL_URL is optional. env.ts adds https:// in front of whatever value Vercel injects.
.env.example also has DB_USER, DB_PASSWORD and DB_NAME. compose.yml reads those, and env.ts does not.
Payments and tax
STRIPE_SECRET_KEY lets checkout take payment at all. app/api/stripe/webhooks/route.ts uses STRIPE_WEBHOOK_SECRET to verify incoming events. Without Stripe, shoppers can still browse the store and fill a cart, and only checkout switches off.
Tax is controlled separately from payments. ENABLE_TAXES (default false) controls whether Litestore charges any tax at all. While it is off, getTaxProvider returns the zero-tax provider and Litestore charges no tax on any order.
When tax is on, each channel's stored taxProvider picks how Litestore calculates it.
taxProvider | What Litestore charges |
|---|---|
manual | The flat channel rate. |
stripe_tax | Real tax for the shopper's jurisdiction. If Stripe returns any error, Litestore falls back to the flat rate. |
| Any other value | Litestore logs a warning and charges the flat rate. |
AI
AI_PROVIDER picks google, anthropic or openai, and it defaults to google. The key you supply has to match that choice, and modules/assistant/model.ts maps each provider to its key name.
AI_PROVIDER | Key you set |
|---|---|
google | GOOGLE_GENERATIVE_AI_API_KEY |
anthropic | ANTHROPIC_API_KEY |
openai | OPENAI_API_KEY |
AI_MODEL replaces the default model, but only for the provider that AI_PROVIDER selects. It doesn't switch providers.
AI_FALLBACK_ENABLED takes "true" or "false". Litestore keeps fallback to another provider on unless the value is exactly "false", and modules/assistant/model.ts reads it from process.env.
If there is no key for the selected provider, isProviderConfigured returns false and the admin hides its AI features. You set how far the assistant may go (read only, or read and propose) under Settings → AI in the admin. Rate limits and the spend budget are constants in platform/config/ai.ts.
Sign-in with Google
AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET add Google as a sign-in option in platform/auth/server.ts. Litestore only adds it when both values are present. Without them, people can only sign in with a magic link.
Storage details
Two optional variables change how Litestore uses the S3 credentials. Set S3_ENDPOINT when you use R2 or MinIO, and leave it out for AWS. Set S3_PUBLIC_URL when the public address for your media is different from the bucket endpoint.
Analytics and tracking
The two sets of analytics variables do different jobs, and the browser side, the server side, both or neither can be configured.
The NEXT_PUBLIC_* values load tracking pixels in the browser. Each pixel only loads after the shopper accepts the matching consent category.
| Service | Variables |
|---|---|
| Plausible | NEXT_PUBLIC_PLAUSIBLE_DOMAIN, NEXT_PUBLIC_PLAUSIBLE_URL |
| PostHog | NEXT_PUBLIC_POSTHOG_API_KEY, NEXT_PUBLIC_POSTHOG_HOST |
| Google Analytics | NEXT_PUBLIC_GA_MEASUREMENT_ID |
| Meta | NEXT_PUBLIC_FACEBOOK_PIXEL |
| TikTok | NEXT_PUBLIC_TIKTOK_PIXEL |
| Currency rates | NEXT_PUBLIC_CURRENCY_API_KEY |
Newsletter providers
Configure one of these providers. If you configure more than one, Litestore uses the first one in this table.
| Provider | Variables |
|---|---|
| Klaviyo | KLAVIYO_API_KEY, KLAVIYO_LIST_ID |
| Mailchimp | MAILCHIMP_API_KEY, MAILCHIMP_LIST_ID |
| ConvertKit | CONVERTKIT_API_KEY, CONVERTKIT_FORM_ID |
| Buttondown | BUTTONDOWN_API_KEY |
| Loops | LOOPS_API_KEY |
| Beehiiv | BEEHIIV_API_KEY, BEEHIIV_PUBLICATION_ID |
To send subscribers to any other platform, set NEWSLETTER_WEBHOOK_URL. Litestore posts each new subscriber to that URL. None of this affects transactional email, which always goes through Resend.
Integrations and observability
| Variable | What it does |
|---|---|
SHOPIFY_APP_URL, SHOPIFY_API_KEY, SHOPIFY_API_SECRET, SHOPIFY_SCOPES, SHOPIFY_API_VERSION | You need all five to import a catalog from Shopify through Admin OAuth. |
SERVICE_API_KEY | A fallback service key for development and simple setups: a Bearer token equal to it is accepted with the read, write:orders and write:webhooks scopes. It must be at least 32 characters long. For real integrations, create scoped keys under Settings → Developer. |
ENCRYPTION_KEY | Litestore encrypts stored credentials and API keys with it. It must be exactly 64 characters long, and openssl rand -hex 32 generates one. |
RESEND_WEBHOOK_SECRET | This is the Svix signing secret for Resend delivery events. Without it, /api/webhooks/resend answers 503 and Litestore never records bounces or complaints. |
ADMIN_ALERT_EMAIL | This address receives operational store emails when the Admin notification email setting is empty. |
JINA_API_KEY | Litestore reads web pages through Jina Reader (for example when it imports a product from a URL); the key lifts Jina's anonymous rate limit. |
NEXT_PUBLIC_SENTRY_DSN, SENTRY_ORG, SENTRY_PROJECT, SENTRY_AUTH_TOKEN | Sentry error reporting uses these. next.config.ts only applies withSentryConfig on a production Vercel build, and it only uploads source maps in CI. |
Inngest variables
env.ts doesn't declare any Inngest variable. platform/inngest.ts creates the client.
The Inngest SDK reads INNGEST_EVENT_KEY and INNGEST_SIGNING_KEY from process.env on its own. If a key is missing, the error appears when Litestore sends an event rather than at startup.
Litestore handles a missing key on purpose. isInlineFallbackEnabled in platform/events/emitter.ts checks for a missing INNGEST_EVENT_KEY. In that case, Litestore runs the same domain-event handlers inline, once, instead of letting the events go nowhere. In production without a key it skips the send and goes straight to the inline path; in development it tries a key-less Inngest dev server first.
Verification
Open /api/health. The route in app/api/health/route.ts runs a live SELECT 1 against Postgres and a redis.ping(), each with a 3-second timeout. It answers ok, degraded (Redis down) or down (database down, with status 503), so an uptime monitor can poll it.
The admin home's setup guide checks six things against the store's real state: a first product, payments, shipping, taxes, store details and email sending.
Environment and admin settings
Put a value in the environment when it is a credential or an endpoint, because it identifies an outside service or authorises a call to it. Put it in Settings in the admin when a store owner would change it: the admin has settings pages for site, currency, payments, notifications, integrations, AI, roles and developer keys. Everything else, such as timings, thresholds, windows and copy defaults, is a typed constant in platform/config/.
Where a value goes depends on who changes it and how often, rather than how sensitive it is.
platform/config/ constant needs a deploy. Changing an admin setting does not.