Built with

Everything else

The rest of the stack: search, storage, email, sign-in, the MCP servers, the database layer and the UI kit. Each one is a single file or package, and most switch off cleanly without their keys.

Jina and pgvector

Search by meaning, so "yellow cast iron pot" finds Maya's Le Creuset even though she never wrote "cast iron".

  • Embedding. lib/server/embeddings.ts calls POST https://api.jina.ai/v1/embeddings with 1024 dimensions, normalized. A listing is embedded as a retrieval.passage (title, one-liner, category, details and description) when it's published and whenever its words change (refreshEmbedding); a search as a retrieval.query.
  • Storing. listing.embedding is vector(1024) with an HNSW cosine index. Migration 0000_enable-pgvector switches the extension on.
  • Searching. searchListings in lib/server/market.ts runs two rankings side by side, 200 candidates each: Postgres full text (websearch_to_tsquery, ranked by ts_rank_cd, with a boost when the shop name matches) and pgvector cosine distance, ignoring anything under 0.3 similarity so "boots" doesn't return mugs. They're merged with reciprocal rank fusion: each ranking adds 1 / (60 + rank).
  • Without it. No JINA_EMBEDDING_MODEL_KEY, or a failed call, and search quietly uses text alone.
JINA_EMBEDDING_MODEL_KEY=jina_…
JINA_EMBEDDING_MODEL=jina-embeddings-v5-text-small   # the default

Cloudflare R2

Photos and videos. lib/server/files.ts signs S3 requests with aws4fetch and stores uploads at uploads/{owner}/{id}.{ext}. Photos can be up to 10 MB, videos 40 MB. The rest of the app only sees file ids and /api/files/{id}, which redirects (308) to R2_PUBLIC_URL when there is one or streams from the bucket, cached forever since files never change.

Without R2 the bytes are kept in Postgres (file.data), which is fine locally. Env: R2_URL, R2_ACCESS_KEY, R2_SECRET_ACCESS_KEY, R2_BUCKET (default resell-store), R2_PUBLIC_URL (optional).

Resend

sendEmail() in lib/server/email.ts renders a React Email template to HTML and plain text and posts it to https://api.resend.com/emails. Without RESEND_API_KEY the text is logged to the console instead, and in development the sign-in link shows on the sign-in page. Notifications to example.com addresses are logged, never sent (lib/server/notify.ts), so seeded accounts stay quiet. Env: RESEND_API_KEY, EMAIL_FROM, and SUPPORT_EMAIL for problems that need a person.

Better Auth

  • lib/server/auth.ts, with the Drizzle adapter and the handler at /api/auth/[...all]. Sign-in is an email link (15 minutes). Sessions last 30 days.
  • Stores live on subdomains, so in production the session cookie is set on .resell.store and every store reads it. Browsers won't share a cookie across *.localhost, so in development a one-time token hands the same session to each store host (see Domains and routing).
  • Pages check auth themselves with requireUser() from lib/server/session.ts, which sends signed-out people to /welcome. Env: BETTER_AUTH_SECRET (required), BETTER_AUTH_URL.

React Email

packages/email holds the templates as React components with a shared theme, so emails look like the app. There are 10: sign-in, offer received, offer update, sold, shipped, order placed, paid out, new from a shop you follow, the evening agent summary, and a general notice. Preview them with pnpm email (port 5690).

MCP TypeScript SDK

packages/mcp builds both MCP servers on @modelcontextprotocol/server 2.3.0: McpServer and registerTool for the tools, createMcpHandler for the hosted HTTP endpoint, serveStdio for the local command, and elicitation for "Ask me first". How the package fits together is on API, docs and MCP.

Drizzle

packages/db (@repo/db) has the schema (src/schema.ts, 33 tables) and the SQL migrations (packages/db/drizzle, 0000 to 0015). Render runs pnpm db:migrate before each deploy.

CommandDoes
pnpm db:generateWrites a migration from schema changes
pnpm db:migrateApplies migrations
pnpm db:studioOpens Drizzle Studio

Import query helpers (eq, and, sql…) from @repo/db, never from drizzle-orm directly, so the app and the schema share one copy of Drizzle.

Base UI and Tailwind

packages/ui (@repo/ui) is the component kit: buttons, chips, fields, item and seller cards, the tab bar, dialogs, drawers, popovers, toasts, tooltips and the icon set. Interactive pieces are built on @base-ui/react for accessible behaviour, styled with Tailwind CSS v4 from the tokens in src/styles.css. Components import as @repo/ui/button, icons as @repo/ui/icons, and helpers like cn from @repo/ui/lib/utils.