This is how to run Kytelo on your own machine to build a theme, build a plugin, or just see the platform end to end.
The one thing that trips everyone up first: the storefront is routed by
hostname. http://localhost:3000 has no tenant subdomain, so it renders
a 404 ("No storefront here") by design. Use
http://demo.lvh.me:3000 instead — lvh.me is a public DNS name that
resolves every subdomain to 127.0.0.1, so {shop}.lvh.me behaves
exactly like {shop}.kytelo.com does in production.
TL;DR#
corepack enable # pnpm 11 (once)
pnpm install
supabase start # local Postgres/auth/storage (needs Docker)
supabase db reset # migrations + demo seed data
pnpm dev # storefront + admin + worker
| What | URL | Login |
| ------------------------- | ------------------------------------------------ | ------------------------------------------ |
| Demo storefront | http://demo.lvh.me:3000 | — (anonymous shopping) |
| Second tenant (isolation) | http://other.lvh.me:3000 | — |
| Admin | http://localhost:3001 | owner@demo.test / kytelo-dev-password |
| Admin (other tenant) | http://localhost:3001 | owner@other.test / kytelo-dev-password |
| Docs (this site) | http://localhost:3004 (pnpm nx run docs:serve) | — |
| Supabase Studio | http://127.0.0.1:55323 | — |
What the seed gives you#
Two tenants, each with its own storefront, catalog, and owner account:
- demo ("Demo Outfitters") — six products across the Kites and Accessories collections, all plugins installed and enabled, the zephyr theme active.
- other ("Other Shop") — two products; the search plugin is installed but disabled, which is deliberate — it proves per-tenant plugin enablement.
The seed also backfills the search index, so /search?q=kite works on the
demo storefront without the worker having processed any events yet.
Things to try#
- Open http://demo.lvh.me:3000 — hero + product grid (zephyr theme).
- Open a product, pick a variant, Add to cart — the drawer that opens
is the cart plugin; pricing ran through its
cart.pricingfilter (see plugin development). - Log into http://localhost:3001, create a product (status active),
then watch it appear on the storefront — and in
/searchonce the worker (running underpnpm dev) processes theproduct.createdevent. - Admin → Plugins → toggle the cart plugin off, reload the storefront: the cart UI disappears (enablement is per-tenant runtime data, cached briefly in dev). Toggle it back.
- Open http://other.lvh.me:3000 — a different shop from the same process.
Running pieces individually#
pnpm nx run storefront:serve # :3000
pnpm nx run admin:serve # :3001 (target `dev` also works)
pnpm nx run worker:serve # event fan-out + jobs (vite-node --watch)
pnpm nx run marketing:serve # :3002
pnpm nx run marketplace:serve # :3003 (stub)
pnpm nx run docs:serve # :3004 (this site)
Each app reads its own apps/<app>/.env (already populated with
local-stack values). The worker must be running for events →
search-indexing to work; the web apps run fine without it (event
publishing fails soft, by design).
Using the hosted Supabase project instead#
Local Docker is the default dev loop, but everything also runs against a hosted Supabase project. One-time setup:
supabase link --project-ref <your-project-ref>
supabase db push # apply supabase/migrations/
Then in the Supabase Dashboard:
- Auth hook (required — logins get no tenant claims without it):
Authentication → Hooks → Customize Access Token → select the Postgres
function
public.custom_access_token_hook. - Keys: Project Settings → API — copy the URL, publishable (anon) key, and secret (service-role) key.
DATABASE_URL: the Connect panel → Session pooler URI (pg-boss cannot use the transaction pooler).
supabase db push does not run supabase/seed.sql — run its
contents against the hosted database yourself (Dashboard → SQL Editor) to
get the demo tenants and products. It inserts demo users directly into
auth.users, which is fine for a scratch project and not something to do
against one with real customers.
Point the apps at the hosted project by editing each apps/<app>/.env
with the values above.
Troubleshooting#
- Connection refused at
demo.lvh.me:3000— either the dev server isn't running (pnpm dev, orpnpm nx run storefront:serve— the target isserve, notdev), or it's bound only to IPv6 ([::1]:3000) whilelvh.meresolves to IPv4. Restart the dev server; if it persists, try http://demo.localhost:3000 instead (*.localhostresolves to loopback without DNS in modern browsers), or add127.0.0.1 demo.lvh.me other.lvh.meto/etc/hosts. - "No storefront here" (404) — you're probably at
localhost:3000instead ofdemo.lvh.me:3000(host-based tenant routing), or the Supabase stack isn't running/seeded (supabase status, thensupabase db reset), or the subdomain doesn't match a seeded tenant slug (demoandotherexist). - Search returns nothing for a product you just created — the worker isn't running; start it and re-save the product (indexing is event-driven).
- Ports — local Supabase uses 55321 (API), 55322 (Postgres), 55323 (Studio); apps use 3000–3004.
- Emails (magic links etc.) — the local stack traps all outbound mail in Mailpit: http://127.0.0.1:55324.
What's next#
- Theme development and plugin development — now that the stack is running, build against it.