A Next.js application for managing fictional train lines, viewing timetables, and displaying departure boards. Data is stored in MongoDB; the network is edited through the built-in admin UI.
pnpm install
cp .env.example .env.local # then fill in your MongoDB Atlas connection string
pnpm devOpen http://localhost:3000 in your browser. Manage the network under /admin.
Data lives in MongoDB Atlas, which lets the app be edited live when deployed to a serverless host like Vercel (where the filesystem is read-only).
The data layer lives in lib/db/:
lib/db/mongodb.ts— connection management (cached across serverless invocations and dev HMR).lib/db/repository.ts— a generic, type-safeRepository<T>over a collection of documents keyed by a stringid.lib/db/indexes.ts— collection names and index definitions. Indexes are created automatically on first use, so a freshly provisioned database needs no setup step.lib/db/collections.ts— the typed repository per entity.
Domain access functions in lib/data/* (e.g. getStations, createLine)
build on those repositories. The document shape of each collection is defined by
the TypeScript interfaces in types/index.ts — that file is
the source of truth for the data model (Station, Line, Variant,
Timetable, RouteCorridor, OperatingPattern, LineSchedule).
All routes are dynamically rendered, so changes made through the admin UI appear immediately without a redeploy.
Set these environment variables (locally in .env.local, on Vercel under
Project → Settings → Environment Variables):
| Variable | Required | Description |
|---|---|---|
MONGODB_URI |
yes | Atlas connection string (mongodb+srv://...). |
MONGODB_DB |
no | Database name within the cluster (defaults to vrt). |
APP_PASSWORD |
no | Password protecting /admin. Unset = admin open (see below). |
The public site (timetables, departure boards, line/station views) is always
open. The /admin pages and the /api/admin/* mutation endpoints are gated by
APP_PASSWORD:
- Unset (typical for local dev): the admin area is open, no login required.
- Set (recommended in production): visiting
/adminredirects to/login; after entering the password a signed session is stored and you stay logged in for 7 days per device.
How it works (lib/serverAuth.ts, proxy.ts, app/api/auth/route.ts):
- The password is never stored — the server only compares against
APP_PASSWORD(constant-time) and discards the input. - Sessions are stateless HMAC tokens in an httpOnly cookie. The signing key
is derived from the password, so changing
APP_PASSWORDinstantly invalidates every existing session. proxy.tsverifies the session on every/adminand/api/admin/*request, so the gate can't be bypassed by calling the API directly.- Wrong-password guesses get a small, escalating delay.
Use a long, random APP_PASSWORD and always serve over HTTPS (Vercel does).
vrt/
├── app/ # Next.js pages and API routes
│ ├── admin/ # Admin UI (password-gated when APP_PASSWORD is set)
│ ├── api/admin/ # Admin mutation endpoints (gated)
│ ├── api/auth/ # Login / logout endpoint
│ ├── login/ # Password prompt
│ ├── lines/ # Train line views
│ ├── stations/ # Station views
│ └── departures/ # Departure boards
├── components/ # React components
├── lib/
│ ├── data/ # Domain data access (built on lib/db)
│ └── db/ # MongoDB connection, repository, indexes
├── proxy.ts # Admin password gate (middleware)
└── types/ # TypeScript interfaces (the data model)