Litestore · Developer guide
Getting started

Setup

This page takes you from a fresh clone to a running store with a seeded catalog and an admin you can sign in to.

This page gets Litestore running on your computer for the first time. It follows the quick start in the repository's README.md: you clone the code, install it, fill in .env.local, apply the schema, seed the catalog and start the server.

The steps run in this order so you can tell a setup problem apart from a configuration problem. If the schema applies but sign-in fails, you know a key is wrong and the install is fine.

Prerequisites

You need six things before you run the first command. This is the requirements list from README.md, with a note on what each one does.

Node.js 20 or newer. The production build targets this runtime.

Bun. Bun installs packages and runs the scripts. The packageManager field pins bun@1.2.2, and .github/workflows/verify.yml installs that same version.

PostgreSQL 15 or newer. You can install it locally or run it with the included compose.yml.

Redis or Upstash Redis. Every environment requires REDIS_REST_URL and REDIS_REST_TOKEN. In production Redis is a required service, because logins and magic links are rate-limited through it. In development, checkRateLimit in lib/rate-limit/presets.ts returns success immediately when isDev is true. Locally the two values only need to be set. They don't have to point at a live instance.

An S3-compatible bucket. You can use AWS S3 or Cloudflare R2, or run MinIO locally.

A Resend account. Litestore signs people in with a magic link sent by email. Without a working key you can't log in at all, and that includes the admin.

Stripe, analytics, Shopify, AI providers and newsletter providers are optional. Shoppers can browse the store and fill a cart without any of them.

Postgres 15+Your data
RedisCache and rate limits
S3 storageImages
LitestoreOne Next.js app
StorefrontAdmin
ResendEmail and sign-in links
InngestBackground jobs
StripeOptional
Litestore connects to Postgres, Redis, S3 storage, Resend and Inngest, and to Stripe if you add it.

Step 1: Clone and install

git clone https://github.com/litestore/litestore

This clones the repository.

cd litestore

You run every later command from the repository root.

bun install

This installs the dependencies and runs two hooks along the way.

The postinstall hook calls db:generate (prisma generate). That command writes the typed Prisma client the rest of the app imports.

The prepare hook calls lefthook install. That command turns on the pre-commit and pre-push checks.

bun install does not build the icons. The icon sprite and types/icons.d.ts are generated files that git ignores, so run this once after cloning:

bun run icons

Both typecheck and the sprite contract test read those files. On a fresh clone they fail until you have run this command.

Prisma model 'does not exist'?

That error means the generated client is stale, not that the schema is wrong. Run bun run db:generate and it will resolve.

Step 2: Start the database

If you already run Postgres locally, create a database named litestore and skip to the next step.

Otherwise, compose.yml defines a Postgres service and a PgBouncer service. Start both in the background:

docker compose up -d

Then check their state:

docker compose ps

Checking matters because the two services don't come up at the same time. Postgres publishes port 5432 and is the only service with a healthcheck (pg_isready). PgBouncer publishes port 6543 and starts once Postgres reports healthy.

For local development, connect straight to 5432. PgBouncer is there for connection pooling in production.

Both services read DB_USER, DB_PASSWORD and DB_NAME from your env file. If you leave them out, they default to postgres, postgres and litestore.

Step 3: Configure environment variables

Copy the example file:

cp .env.example .env.local

Most keys in .env.example are empty strings, and env.ts sets emptyStringAsUndefined. That means an empty value counts as unset. Copying the file does not fill it in for you.

This is the smallest set of values that passes validation:

.env.local
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
NEXT_PUBLIC_SITE_EMAIL="hello@example.com"
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/litestore"

BETTER_AUTH_URL="http://localhost:3000"
BETTER_AUTH_SECRET="replace-with-a-long-random-secret"

# Required to be SET even locally — validation fails closed without them.
# The rate limiter never calls Redis in dev, so placeholders are fine here.
REDIS_REST_URL="https://example.upstash.io"
REDIS_REST_TOKEN="..."

RESEND_API_KEY="re_..."
RESEND_SENDER_EMAIL="no-reply@example.com"

S3_REGION="us-east-1"
S3_BUCKET="litestore"
S3_ACCESS_KEY="..."
S3_SECRET_ACCESS_KEY="..."

Every route 500s with 'Invalid environment variables'?

A required variable is missing or empty. The validator runs when the module loads, so one absent value fails every request. People most often forget the two Redis variables and NEXT_PUBLIC_SITE_EMAIL. The error output names the exact keys.

Step 4: Apply the schema

bun run db:push

This creates the tables and applies every committed migration.

README.md names db:push, but in package.json that script runs prisma migrate dev. It is the same command db:migrate runs. Neither one is a schemaless push, and both write a migration file.

prisma/migrations/ contains two committed migrations:

20260726143000_baseline_with_db_guarantees
20260730090000_performance_indexes

The baseline migration creates the schema. It also adds the CHECK constraints and the case-insensitive unique indexes.

Step 5: Seed the catalog

bun prisma/seed.ts

This loads a working store. You get products with variants, inventory, media, pages, reviews, feeds and email templates.

README.md names bun run db:seed for this step. That script runs tsx prisma/seed.ts, but tsx is not a dependency in package.json, so it fails unless you have tsx installed globally. The command above runs the same file, and it is also what the prisma.seed hook uses.

When the seed finishes, it prints a summary. It creates two rows for each of 41 base models, 20 more products with over 80 variants between them, and 12 transactional email templates.

The seed does not create orders. You create those through checkout or in the admin.

Every write in the seed is an upsert. If you run it twice, it updates the existing rows instead of adding duplicates.

Step 6: Start the dev server

bun run dev

This runs next dev -p 3000 --turbopack.

Open http://localhost:3000 to see the storefront and http://localhost:3000/admin to see the admin. One process serves both.

Step 7: Sign in and verify

The seed creates an admin account for admin@example.com. Enter that address at /auth/login and follow the link in the email. Sign-in uses a magic link, so the email goes wherever your Resend key delivers it.

Inbox
Sign in to Your StoreTo ava@example.com
now
YYour sign-in linkClick the button below to sign in. No password needed.Sign inDidn't ask for this? You can ignore this email.
You sign in by clicking the link Litestore emails to you.

Then open Settings → Developer.

The Self-host readiness panel runs a live SELECT 1 against Postgres and a redis.ping(), each with a 3-second timeout. It also tells you whether the site URL uses https, and whether Resend, S3, Stripe and an AI provider are configured.

If you used placeholder Redis values, the panel reports Redis as down. That is expected locally, but it should not happen in production.

Troubleshooting

The magic link email never arrives. Look for the send in the Resend dashboard. If it isn't there at all, RESEND_API_KEY or RESEND_SENDER_EMAIL is wrong, and the readiness panel will show which.

Typecheck runs out of memory. bun run typecheck asks Node for a 12 GB heap. On a smaller machine, run it on its own instead of next to the dev server.

Logins work locally but are blocked once deployed. Env validation only checks that a value exists. The rate limiter reads Redis when someone signs in, but it is skipped in dev. A wrong REDIS_REST_URL or REDIS_REST_TOKEN therefore first shows up in production, as a login that never completes.

The readiness panel's redis.ping() catches this, and the same idea applies to the whole setup. A variable being present does not prove it is correct. The readiness panel is the only step that actually calls the services.

Next Steps

On this page