Product
Technology stack
The product is three parts: a web client, a mobile client and the server they all talk to. One TypeScript stack runs through them — the React Native app, the NestJS backend, PostgreSQL and AWS the client asked for, and Next.js where search engines must read the catalogue — each part in its own repository, held together by one versioned API contract.
Web client
Two Next.js applications, each in its own repository: the public site that buyers and search engines read, and the admin where staff and the hub work. Both reach the server through a server side of their own, so the browser never holds a token.
What it does
The site: the catalogue, listings, seller profiles, checkout, chat and the seller’s area. The admin: moderation, disputes, refunds and payouts, and the hub’s inspection workspace — each person sees what their role allows.
How pages are made
The pages that must be found — home, categories, a listing, a seller — are rendered on the server and cached; the server refreshes them the moment a listing changes. The account, checkout and chat render in the browser and are kept out of search. The admin renders in the browser, on a domain of its own, never indexed.
How it talks to the server
Through its own server side: the sign-in lives in a secure cookie the page’s scripts cannot read, and the Next.js server calls the API with the typed client. Staff sign in with a second factor. Card details go straight to Stripe and never touch our servers.
How it is built
Route files only render a screen; every screen and component lives with its feature, shared pieces in one place. Styles come from this showcase’s tokens; each application keeps its own translations.
Technologies

Next.js
The site, rendered on the server so search engines index every listing, and the admin with the hub workspace.

React
The interface library under both applications.

Tailwind CSS
Styles drawn from this showcase’s design tokens.

shadcn/ui
Accessible building blocks of the admin, restyled to the brand.

TanStack Table
The admin’s registers and queues, with sorting and filters.

React Hook Form
The admin’s forms and their checks.
Every page in the user’s language, with localised addresses for search engines.
The charts of the admin’s reports and the sellers’ dashboards (SC-008).

Lucide
The base icons; the product’s own icons are drawn on the same grid.
Mobile client
One React Native app on Expo for iPhone and Android — the first surface of the product. It does what a buyer and a seller do on the site, with the phone’s own strengths: the camera, push notifications, links that open the app.
What it does
Browse and search, save, make an offer, buy, chat, list an item with photos, follow an order through inspection and delivery, leave a review — and delete the account from the app itself, as the stores require.
How screens are reached
The app’s paths are the site’s addresses, so a shared link or a push notification opens the same listing or order in the app, and in the browser when the app is not installed.
How it talks to the server
With a token kept in the phone’s secure storage. Data comes through the typed client and stays fresh on screen. The app sends its version with every request; below the oldest version the server serves, it asks the user to update.
What it uses on the phone
The camera and the gallery: photos are compressed on the phone and uploaded straight to storage by a signed link. Push through Apple’s and Google’s services. Payment in Stripe’s own sheet; card details never touch our servers.
How it reaches phones
Builds go to the App Store and Google Play through Expo’s build service; a fix to the app’s code can reach phones without a store release. The build also runs on our own machines, so nothing depends on one vendor.
Technologies

React Native
One app for iPhone and Android, written once.

Expo
Builds, store releases and over-the-air updates; Expo Router makes the app’s links the site’s links.

TanStack Query
Fresh data on every screen, cached between them.

i18next
Every screen in the user’s language.

Stripe React Native
The payment sheet inside the app.

Lucide
The same icons as on the web.
Server
One NestJS application — a modular monolith whose modules are the product’s fourteen modules — run as two processes from the same code: the API, which answers the clients, and the worker, which does everything that may wait.
The API
REST at /v1 for users and /v1/admin for staff, who need a second factor. The chat runs over a live connection, and every message is checked for contact details on the server before it is delivered. Providers’ calls back are accepted only with a valid signature.
The worker
The timers of offers and reservations, letters and pushes, photos (thumbnails made, location data stripped), payment and delivery events with their retries, exchange rates, exports, and the erasure of personal data on request.
Modules
Each module owns its tables; another module asks it through its public service, never reads them directly. A module reacts to another’s events through an outbox written in the same transaction, so no event is lost when an outside call fails. Audit, events, files, money and queues are platform modules under them all.
Data
Money is kept in whole minor units with its currency and counted only on the server. An item cannot be reserved twice. The history of orders, payments and decisions is never deleted, and every change says who made it and why. Search runs in PostgreSQL behind one interface, ready for a search engine when the catalogue grows.
Providers
Payments, authentication, shipping, email, push and exchange rates each sit behind an interface of the product’s own: a provider for another country is one more adapter, and the rest of the code does not change.
Technologies

Node.js22
Runs the API and the worker.

NestJS12
The backend: its modules are the modules of the product.

TypeScript
One strictly typed language in all four applications.

OpenAPI
The API described from the code itself; the clients’ package is generated from it.

Socket.IO
The chat between buyer and seller in real time.
The worker’s queues: jobs, timers and retries.

PostgreSQL18
Accounts, listings, offers, orders and money; the catalogue search and its filters.

Prisma7
The database schema, split by module, and its migrations.

Redis
The job queues, the request limits and the chat, shared by every server instance.

Amazon S3
Listing photos, served through the CDN; inspection evidence and identity documents in a private store, by short-lived links.

Stripe Connect
Payments held until the item is authenticated and delivered, then paid out to the seller.

Firebase Cloud Messaging
Push notifications to Android.

Apple Push Notification service
Push notifications to iPhone.

Amazon Web Services
The client’s cloud, in Frankfurt (eu-central-1) so the data stays in the EU; how each application is hosted is set by DevOps.

Docker
Every server application ships as an image; locally one command starts the database, Redis, file and mail stand-ins.

GitHub Actions
Each repository checks, builds and releases itself; the backend also publishes the API client.

Vitest
Unit and end-to-end tests of the backend, against a real PostgreSQL.

Playwright
The site and the admin tested in real browsers.
The app tested on iPhone and Android simulators.

Oxlint
Catches mistakes in the code before review.

Prettier
One code style in every repository, applied on commit.

Sentry
Errors from all four applications, reported the moment they happen.
One source
The backend describes its API in OpenAPI, generated from the code itself. On every release its CI builds a typed client from that description and publishes it as the package @lrm/api-client, with the release’s version.
A pinned version
The site, the admin and the app each name the version of the client they use and move to a new one on purpose, in a change of their own. A request or a status the backend does not have does not compile.
Only compatible changes
A field or an endpoint may be added, never removed or changed. The CI compares the new description with the last one and stops a breaking change; a change that must break goes to a new path, /v2, while /v1 lives on for the clients that use it.
Old apps on phones
An app stays on a phone for months. It sends its version with every request, and the backend names the oldest version it still serves, so the app can ask the user to update instead of failing.
The marks are their owners’ trademarks, shown to name each technology; the files come from Simple Icons and the projects’ own sites and are kept in this folder.