Developers
Run it locally
Before you start
- Node 24 or newer (the root
package.jsonsays"node": ">=24"). - pnpm through Corepack:
corepack enablepicks up the version pinned inpackageManager. - Docker, for Postgres 17 with pgvector (
pgvector/pgvector:pg17, the same major version as Render).
Get it running
From the repo root:
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| Open | What you get |
|---|---|
http://localhost:5689 | The landing page and marketplace |
http://claspandcarry.localhost:5689 | A seeded store. Any store is {slug}.localhost:5689 |
http://localhost:5689/docs | These docs |
http://localhost:5689/api/v1 | The public API (api.resell.store/v1 in production) |
http://localhost:5689/api/mcp | The hosted MCP servers |
http://localhost:5689/design-system | Every design system component |
http://localhost:5689/screens | Every 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.
| Without | What happens |
|---|---|
RESEND_API_KEY | Emails are printed in the dev server's terminal, and the sign-in link shows on the page |
ANTHROPIC_API_KEY | Research 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_KEY | Research skips the catalog ("Add CHANNEL3_API_KEY to search the catalog") and prices from comps and the model |
KERNEL_API_KEY | No live comps step in research, and the Shopping sidekick shows Coming soon |
JINA_EMBEDDING_MODEL_KEY | Search 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_KEYis empty; - always for
@example.comaddresses, which never get real email. All the seeded people use them, so you can sign in as a store owner (for examplepriya.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):
curl -X POST http://localhost:5689/api/cron/sweepOr 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:
cd apps/app
set -a; source .env.local; set +a
render workflows dev -- pnpm workflows
# in another terminal
render workflows tasks start offerSweep --localTo 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
pnpm email # React Email preview at http://localhost:5690The 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:
- In the PayPal developer dashboard, create a Platform app. Put its client id in
PAYPAL_CLIENT_IDandNEXT_PUBLIC_PAYPAL_CLIENT_ID, and the secret inPAYPAL_CLIENT_SECRET. - 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 toPAYPAL_BN_CODE. - 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/_PASSWORDandPAYPAL_DEMO_BUYER_EMAIL/_PASSWORD. These are shown in the app on purpose, so people trying the demo can use them. - Connect the demo seller once. The script prints a PayPal link the first time:
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=...- Put the printed id in
PAYPAL_DEMO_SELLER_IDand restart. Sellers who haven't connected PayPal are now paid there, and Use demo PayPal on Connections links an account to it. - 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/webhookand setPAYPAL_WEBHOOK_ID.
Gotchas
- Use port 5689.
NEXT_PUBLIC_ROOT_DOMAINandBETTER_AUTH_URLsaylocalhost:5689, and every absolute link, store URL and auth origin is built from them.pnpm devalready uses that port. - Store subdomains get their own copy of your session. Chrome won't send a
Domain=localhostcookie 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 (thers_handoffcookie): 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 fromdrizzle-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.
import "server-only", which throws outside Next. Load workflows/server-only.mjs with --import, like the seed does. See Testing.