Developers

Run it locally

Postgres in Docker, a seeded marketplace with ten stores, and the app on port 5689. No API keys needed to start.

Before you start

  • Node 24 or newer (the root package.json says "node": ">=24").
  • pnpm through Corepack: corepack enable picks up the version pinned in packageManager.
  • Docker, for Postgres 17 with pgvector (pgvector/pgvector:pg17, the same major version as Render).

Get it running

From the repo root:

Terminal
corepack enable
cp apps/app/.env.example apps/app/.env.local
# set BETTER_AUTH_SECRET in .env.local: openssl rand -hex 32

pnpm install
pnpm db:up          # Postgres + pgvector in Docker, on port 5433
pnpm db:migrate     # runs packages/db/drizzle/*.sql
pnpm --filter app seed
pnpm dev            # http://localhost:5689
OpenWhat you get
http://localhost:5689The landing page and marketplace
http://claspandcarry.localhost:5689A seeded store. Any store is {slug}.localhost:5689
http://localhost:5689/docsThese docs
http://localhost:5689/api/v1The public API (api.resell.store/v1 in production)
http://localhost:5689/api/mcpThe hosted MCP servers
http://localhost:5689/design-systemEvery design system component
http://localhost:5689/screensEvery designed screen, with live and mock links

The seed makes ten stores, four buyers, about 70 items, some sales with reviews, questions in messages, follows, likes and a month of views for Stats. Everyone it makes has an id starting with seed_, so running it again replaces only its own rows. --reset removes them, --no-embed skips the Jina calls. Pictures come from apps/app/seed/images; pnpm --filter app seed:art renders them from the drawings in seed/art.

Which keys you need

None, to start. With an empty .env.local (apart from the secret) everything works, with these differences. Add keys as you need the feature, then restart pnpm dev. The full list is on Environment variables.

WithoutWhat happens
RESEND_API_KEYEmails are printed in the dev server's terminal, and the sign-in link shows on the page
ANTHROPIC_API_KEYResearch still runs and prices from catalog numbers; the listing chat, the words step, the store agent's answers and the Sidekick are off; the negotiator counters with a template
CHANNEL3_API_KEYResearch skips the catalog ("Add CHANNEL3_API_KEY to search the catalog") and prices from comps and the model
KERNEL_API_KEYNo live comps step in research, and the Shopping sidekick shows Coming soon
JINA_EMBEDDING_MODEL_KEYSearch is Postgres full text only, no semantic matches
R2_*Uploads are stored in Postgres (file.data) and served from /api/files/[id] the same way
PAYPAL_*Checkout is a test checkout: the order is real, no money moves, releases do nothing

Signing in

Go to /welcome, type an email and choose Email me a sign-in link. In development the link is kept in memory and an Open the sign-in link button appears on the page:

  • for every address, when RESEND_API_KEY is empty;
  • always for @example.com addresses, which never get real email. All the seeded people use them, so you can sign in as a store owner (for example priya.nair@example.com, who runs claspandcarry) or a buyer (ava.lindqvist@example.com).

Links last 15 minutes and work once. Sessions last 30 days.

Running the timed jobs

Offer expiry, reminders, cancellations and payouts don't run on their own locally. Run one pass of everything in-process (no secret needed in development):

Terminal
curl -X POST http://localhost:5689/api/cron/sweep

Or run the real Render Workflow locally with the Render CLI. The workflow script doesn't read .env.local by itself, so export it first, then start tasks from a second terminal:

Terminal
cd apps/app
set -a; source .env.local; set +a
render workflows dev -- pnpm workflows

# in another terminal
render workflows tasks start offerSweep --local

To watch a payout happen without waiting days, set PAYOUT_DEMO_MINUTES_PER_DAY=1 so every "day" in the money timers is a minute. See Background jobs.

Live and mock screens

Every designed screen was first built as a front-end prototype, and those versions still live under app/mock. In development a tab on the right edge flips the current URL between the live screen and its mock. ?view=mock and ?view=live do the same (they set or clear the rs_view cookie). /screens lists every screen with both links; signed in, the live links point at your own shop and listings.

Email previews

Terminal
pnpm email   # React Email preview at http://localhost:5690

The ten templates are in packages/email/emails. Images in sent emails come from apps/app/public/email; the preview serves its own copies from emails/static.

PayPal sandbox

PayPal is sandbox only for this project. To try real (sandbox) checkout, payouts and refunds:

  1. In the PayPal developer dashboard, create a Platform app. Put its client id in PAYPAL_CLIENT_ID and NEXT_PUBLIC_PAYPAL_CLIENT_ID, and the secret in PAYPAL_CLIENT_SECRET.
  2. Find the platform's sandbox business account (Testing Tools, Sandbox Accounts) and put its Account ID in PAYPAL_PARTNER_MERCHANT_ID. Add your BN code to PAYPAL_BN_CODE.
  3. Create two sandbox personal accounts, one to act as the shared demo seller and one as the demo buyer, and put their logins in PAYPAL_DEMO_SELLER_EMAIL / _PASSWORD and PAYPAL_DEMO_BUYER_EMAIL / _PASSWORD. These are shown in the app on purpose, so people trying the demo can use them.
  4. Connect the demo seller once. The script prints a PayPal link the first time:
Terminal
cd apps/app
node --env-file=.env.local scripts/paypal-demo-seller.mjs
# open the link in a private window, sign in as the demo seller, allow everything
node --env-file=.env.local scripts/paypal-demo-seller.mjs
# Connected. PAYPAL_DEMO_SELLER_ID=...
  1. Put the printed id in PAYPAL_DEMO_SELLER_ID and restart. Sellers who haven't connected PayPal are now paid there, and Use demo PayPal on Connections links an account to it.
  2. Webhooks need a public URL, so locally you can skip them: the app reads capture and payout results from PayPal's responses. To test them, tunnel to /api/paypal/webhook and set PAYPAL_WEBHOOK_ID.

Gotchas

  • Use port 5689. NEXT_PUBLIC_ROOT_DOMAIN and BETTER_AUTH_URL say localhost:5689, and every absolute link, store URL and auth origin is built from them. pnpm dev already uses that port.
  • Store subdomains get their own copy of your session. Chrome won't send a Domain=localhost cookie to *.localhost, so the first page load on a store bounces through the marketplace to copy it over. It only tries once every 10 minutes (the rs_handoff cookie): if you signed in after opening a store, clear that cookie or wait. See Domains and routing.
  • Import Drizzle helpers from @repo/db (eq, and, sql…), never from drizzle-orm. pnpm can hand the app a second copy of drizzle-orm and the types stop matching.
  • NEXT_PUBLIC_* values are baked in when Next builds. In dev, restart after changing them.
  • Postgres is on 5433, not 5432, so it doesn't clash with one you already run.
Running scripts outside Next? Server modules start with import "server-only", which throws outside Next. Load workflows/server-only.mjs with --import, like the seed does. See Testing.