Developers

Project structure

A Turborepo with pnpm: one Next.js app and five packages. Here's where things live and the habits the code follows.

The monorepo

Layout
apps/app                    the Next.js app: marketplace, stores, seller app, API, docs, MCP
packages/db                 Drizzle schema, migrations (drizzle/), client, migrate helpers
packages/email              React Email templates (emails/) and render helpers
packages/mcp                the MCP servers: tools, API client, stdio CLI
packages/ui                 the design system: tokens (styles.css), components, icons
packages/eslint-config      shared ESLint configs
packages/typescript-config  shared tsconfig files
render.yaml                 the Render Blueprint
docker-compose.yml          local Postgres 17 + pgvector on port 5433

Packages are imported by name: @repo/db, @repo/email, @repo/mcp, and design system pieces as @repo/ui/button, @repo/ui/icons and so on. They export their TypeScript source, so there's no build step between changing a package and seeing it in the app. Only @repo/mcp has a build, for its stand-alone dist/stdio.js.

The app's routes

Inside apps/app/app, route groups (the folders in brackets) share a layout without adding to the URL.

FolderWhat it is
(marketing)The landing page at /
(flow)Sign-up and onboarding (/welcome, /welcome/start), no app shell
(app)The seller app inside the shell: home, inbox, listings, offers, sales, shops, stats, tools, me. The layout calls requireUser()
(workspace)The listing workspace, /list/[id]/research → details → photos → words → publish, with the agent chat beside it
(market)The public marketplace on the root domain: discover, stores, agent, account, messages
(checkout)/checkout/[listing] and /offer/[listing]
store/[store]One seller's store. proxy.ts rewrites maya.resell.store/x onto /store/maya/x
docsThis site (docs.resell.store, or /docs)
mockThe front-end prototype of every designed screen, served at the real URLs in mock mode
design-systemEvery foundation and component from @repo/ui on one page
screensAn index of every designed screen with live and mock links
api/v1The public API; the routes themselves are in lib/server/api
api/mcpThe hosted MCP servers
api/authBetter Auth's handler
api/paypalCheckout and onboarding returns, and the webhook
api/*Also: cron/sweep, files/[id], listings/[id]/research and chat, session handoff, track (views), dev/magic-link

Server actions

app/actions/*.ts are the "use server" entry points the screens call: listings, listing-media, listing-words, listing-publish, commerce (buying, offers, shipping), after-sale (cancellations, problems, refunds), paypal, messages, shops, profile, account, follows, likes, reviews, sidekick, developer (keys, agent links, webhooks) and market. They check who's signed in, validate input with zod, call lib/server, and return { ok: false, error } for anything a person can fix instead of throwing.

Server modules

apps/app/lib/server is where the work happens. Actions, API routes, pages and the workflow all call the same functions, so there is one place to change a rule.

AreaFiles
Auth and peopleauth.ts, session.ts, dev-links.ts, viewer.ts, account-visit.ts
Shops and listingsshops.ts, listings.ts, listing-chat.ts, words.ts, files.ts
Research and agentsai.ts, research.ts, channel3.ts, comps.ts, kernel.ts, sidekick.ts, store-agent.ts, negotiator.ts
Marketplacemarket.ts (browse and search), embeddings.ts, follows.ts, likes.ts, activity.ts, stats.ts
Moneycommerce.ts, payout-policy.ts, paypal.ts, paypal-sellers.ts, paypal-webhook.ts, disputes.ts, reviews.ts
Messages and emailmessages.ts, notify.ts, notify-after-sale.ts, digests.ts, email.ts
Timingsweeps.ts (the timed jobs), later.ts (run after the response)
Share imagesshare.ts, og-images.tsx, og-render.tsx, og-photos.ts, og-fonts.ts
Public APIapi/: router.ts, routes/*, keys.ts, http.ts, serialize.ts, webhooks.ts, log.ts

market-logins.ts (signed-in marketplace accounts through Kernel) exists with its table, but nothing calls it yet. Outside lib/server: lib/urls.ts builds every cross-zone URL, lib/money.ts converts dollars and cents, lib/docs holds this site's nav, and lib/mock*.ts the prototype's data.

Components

apps/app/components is grouped by screen or zone: market/ (header, store, listing, discover, checkout, buyer account, agent), listing/ and workspace/ (the listing flow), shell/ (the app shell), home/, inbox/, offers/, shops/, tools/, after-sale/, reviews/, me/, welcome/, landing/, empty/ and loading/, plus docs/ for this site. Generic, reusable pieces belong in @repo/ui instead.

Live and mock

The mock pages under app/mock and the live pages render the same components. Live pages pass real data and server actions as props; mock pages pass nothing and the component falls back to the prototype's behaviour and the data in lib/mock*.ts. So when you change a shared component, keep its live props optional and check both versions.

Where the live version needed a different shape, it has its own folder next to the original: listing-live beside listing-later, inbox-live beside inbox, tools-live beside tools, and seller-live for sales and offers. lib/mock-mode.ts lists which paths have a mock.

Conventions

  • Every lib/server file starts with import "server-only", so it can't end up in a browser bundle.
  • Pages check auth themselves. requireUser() sends signed-out people to /welcome and people who haven't onboarded to /welcome/start; getCurrentUser() returns null instead. The proxy never checks auth.
  • Money is whole cents in the database (price_cents, total_cents…) and on the server. Screens show and type dollars (toCents, formatCents in lib/money.ts), and the public API takes and returns dollars, never cents.
  • Import Drizzle helpers (eq, and, sql…) from @repo/db, not drizzle-orm.
  • Links between zones use SiteLink / StoreLink or siteUrl() / storeUrl(), never a hard-coded host. See Domains and routing.
  • Slow or optional work (agents, emails) runs after the response with after() or runAfterResponse(), and never fails the action that started it.
  • Tests sit next to the file they test, as *.test.ts.
  • Comments explain why, in plain words, and screen codes from the design file (C2, P4, B3) mark which screen a piece belongs to.
Every outside service is optional and checked with a flag like aiConfigured, channel3Configured or paypalEnabled(). New code that calls a service should do the same and give the screen something sensible without it.