Product/Web client

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.

next-intl

Every page in the user’s language, with localised addresses for search engines.

Recharts

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.

BullMQ

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.

Hosting and delivery

Where it runs and how it gets there.

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.

Quality

Tests, linting and the checks every change passes.

Vitest

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

Playwright

The site and the admin tested in real browsers.

Maestro

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.

Shared API contract

The site, the admin and the app live in repositories of their own and share one thing with the backend: the contract of its API.

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.