← All docs
Architecture
The LeadFella ecosystem is two independent systems that share a brand but not a codebase or a database.
┌───────────────────────────────────────────┐
│ Visitors │
└───────────────────┬─────────────────────────┘
│ HTTPS
┌─────────────────────────────────┼─────────────────────────────────┐
│ │ │
leadfella.com api.leadfella.com app.leadfella.com
┌──────────────────┐ ┌────────────────────┐ ┌──────────────────────┐
│ Marketing site │ /api/* │ Website API │ │ Product app (SPA) │
│ Next.js 15 │─────────────▶│ Express + TS │ │ React + Vite │
│ (App Router) │ │ │ │ │ /api │
│ + /admin panel │ │ waitlist/contact/ │ │ ▼ │
└──────────────────┘ │ admin │ │ Spring Boot monolith│
└─────────┬──────────┘ │ (lead, scraper, │
│ │ intelligence, ...) │
▼ │ + Playwright-Java │
┌───────────┐ └──────────┬───────────┘
│ PostgreSQL│ │
│ (website) │ ▼
└───────────┘ ┌───────────┐
│ MongoDB │
│ (product) │
└───────────┘
Two systems
Product app (the SPA you sell)
- Frontend: React + Vite + TypeScript + Tailwind + shadcn/ui, a pure SPA.
- Backend: a single Java Spring Boot modular monolith (Java 21), packaged by
feature:
lead,scraper,intelligence,scoring,crm,outreach. It does everything, including Google Maps scraping via Playwright-Java in-process. - Database: MongoDB, owned entirely by the backend.
- Served: the built SPA is bundled into the backend's static resources and
runs as a single jar on a single port. In production at
app.leadfella.com. - Lives in
../../apps/weband../../backend. Not part of this website project.
Marketing website (this project)
- Frontend (
../frontend): Next.js 15 (App Router) + React 19 + TypeScript + Tailwind + shadcn/ui + Framer Motion. Statically pre-rendered marketing pages plus an/adminarea (client-rendered,noindex). Served atleadfella.com. - Backend (
../backend): a small Express + TypeScript API for the waitlist, contact form, and admin panel. Served atapi.leadfella.com. - Database: its own PostgreSQL instance (tables
waitlist_leads,contact_messages,schema_migrations). Completely separate from the product's MongoDB.
Why decouple the website
The product app is a Vite SPA served by Spring Boot. The marketing site has different needs - SEO, static rendering, fast first paint, and a tiny lead-capture API - so it is intentionally a separate Next.js frontend plus a small Express API that can be deployed and scaled on their own.
Request flow
- Marketing pages are static; the browser loads them directly from the Next.js deployment.
- Forms (early access, contact)
POSTto the Express API over/api. The frontend talks to the API through a typed client (src/lib/api.ts) and reads the API base fromNEXT_PUBLIC_API_URL. - Admin panel (
/admin) authenticates against the Express API (POST /api/admin/login→ JWT), stores the token inlocalStorage, and calls the admin endpoints with aBearertoken. A401clears the token and signs the operator out.
Boundaries (hard rules)
- The website never touches the product's MongoDB, and the product never touches the website's PostgreSQL.
- The website frontend talks only to the website API over
/api- no direct database access from the browser. - Website users (early-access leads, the single admin) are not product users. The two auth systems are independent.
Website backend internals
backend/src/
config/ env (zod-validated) + logger (pino)
db/ pool, migration runner, repositories
middleware/ error handling, rate limiting
models/ row/DTO types + zod input schemas
auth/ admin JWT + requireAdmin middleware
routes/ health, waitlist, contact, admin
lib/ shared helpers (CSV)
scripts/ migrate, seed:admin, maintenance CLIs
Controllers stay thin, logic lives in services/repositories, and persistence is
parameterized SQL through a single pg pool. See
configuration.md and api.md for specifics.