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#

  1. Open http://demo.lvh.me:3000 — hero + product grid (zephyr theme).
  2. Open a product, pick a variant, Add to cart — the drawer that opens is the cart plugin; pricing ran through its cart.pricing filter (see plugin development).
  3. Log into http://localhost:3001, create a product (status active), then watch it appear on the storefront — and in /search once the worker (running under pnpm dev) processes the product.created event.
  4. 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.
  5. 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:

  1. Auth hook (required — logins get no tenant claims without it): Authentication → Hooks → Customize Access Token → select the Postgres function public.custom_access_token_hook.
  2. Keys: Project Settings → API — copy the URL, publishable (anon) key, and secret (service-role) key.
  3. 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, or pnpm nx run storefront:serve — the target is serve, not dev), or it's bound only to IPv6 ([::1]:3000) while lvh.me resolves to IPv4. Restart the dev server; if it persists, try http://demo.localhost:3000 instead (*.localhost resolves to loopback without DNS in modern browsers), or add 127.0.0.1 demo.lvh.me other.lvh.me to /etc/hosts.
  • "No storefront here" (404) — you're probably at localhost:3000 instead of demo.lvh.me:3000 (host-based tenant routing), or the Supabase stack isn't running/seeded (supabase status, then supabase db reset), or the subdomain doesn't match a seeded tenant slug (demo and other exist).
  • 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#