Japanese export stock — cars, spare parts, machinery and bikes — presented as a port departure board, with a customer dashboard for order tracking and a separate management back office.
Next.js 16 · React 19 · Tailwind v4 · Postgres via Prisma 7.
| Surface | Route | Who |
|---|---|---|
| Public board | /, /cars, /spare-parts, /machinery, /bikes |
Anyone. Live stock, no account needed to enquire. |
| Customer dashboard | /dashboard |
Signed-in buyers. Order tracking and history. |
| Management | /manage |
Admins only. Stock, sailings, orders, buyer accounts. |
/manage is not linked from the public site and carries noindex, nofollow.
It shares the design system with the public site and nothing else — no public
header, no footer, no route back into the public navigation.
The Dashboard link appears in the public header only when a customer is
signed in. /dashboard itself stays reachable when signed out: it renders a
"Please log in" page rather than silently redirecting, so a bookmarked or shared
link explains itself.
Local development, no setup — Prisma ships a local Postgres:
npx prisma dev --name krn # leave this running; it prints two URLsPaste the DATABASE_URL and SHADOW_DATABASE_URL it prints into .env
(see .env.example). Keep that process alive — killing it drops the database
connection and every page will 500.
If it will not restart (ERROR Lock file is already being held, and pages
500 with ECONNREFUSED): a force-killed server leaves a stale lock. Delete it
and start again.
rm -rf "$LOCALAPPDATA/prisma-dev-nodejs/Data/durable-streams/krn/server.lock.lock"
npx prisma dev --name krnYour data survives this; it lives in …/prisma-dev-nodejs/Data/krn. Stop the
server with Ctrl-C rather than by killing the process and you will not hit it.
None of this applies to a real Postgres — prisma dev is PGlite, not a server
you would deploy.
Or point at your own Postgres:
DATABASE_URL="postgresql://USER:PASSWORD@localhost:5432/krn?schema=public"
npm install
npm run db:push # or: npm run db:migrate (prisma migrate deploy)
npm run db:seed
npm run devdb:push syncs the schema directly and is the quickest way to get going.
db:migrate applies the SQL in prisma/migrations/ and is what you want for a
real deployment.
The seed creates three accounts. All three share the password
krn-demo-2026. Change or delete them before this goes anywhere real.
| Role | Lands on | |
|---|---|---|
admin@krn-international.example |
Admin | /manage |
dealer@example.com |
Customer | /dashboard — orders in progress |
buyer@example.com |
Customer | /dashboard — one in progress, one completed |
This is the flow that connects the three surfaces:
- A unit sits on the public board while its status is
AVAILABLEorRESERVED. - In
/manage/stockyou press Mark sold and choose the buyer:- A buyer with an account → the order lands on their
/dashboardimmediately. - An external buyer (eBay, walk-in) → the order is tracked internally against a free-text label. External buyers have no dashboard.
- A buyer with an account → the order lands on their
- The unit's status becomes
SOLDin the same transaction, which removes it from every public board. One unit can only ever carry one order — the database enforces it with a unique constraint. - You record stages on the order page. Each writes a
TrackingEventthe buyer sees, with an optional note. CANCELLEDreleases the unit back onto the public board.
Stages: RESERVED → DEPOSIT_PAID → IN_YARD → LOADED → SAILED → ARRIVED → RELEASED, plus CANCELLED off-path. RELEASED and CANCELLED move an order
into the buyer's history.
The sailing schedule used to sit on the public home page. It now lives in
/manage/sailings, and the public board reads from it — every row takes its
destination, vessel, cut-off and ETA from the sailing its unit is booked onto.
Change a date there and every row that references it follows.
- Passwords hashed with bcrypt, cost 12.
- Sessions are a 256-bit random token in an httpOnly, SameSite=Lax cookie (Secure in production). The database stores only its SHA-256, so a database dump cannot be replayed as a live session.
- Two roles,
CUSTOMERandADMIN. Self-registration can only ever create aCUSTOMER; admins are made in/manage/buyers. - Every admin page and every admin server action calls
requireAdmin(). Server actions are separately addressable endpoints, so guarding the layout alone would leave them open. - Sign-in gives the same response whether or not an address is registered.
| Command | What |
|---|---|
npm run dev |
Dev server |
npm run build / npm start |
Production |
npm run db:push |
Sync schema to the database (no migration history) |
npm run db:migrate |
prisma migrate deploy — for real environments |
npm run db:seed |
Reset and reseed stock, sailings, demo accounts and orders |
npm run db:studio |
Prisma Studio |
npm run lint |
ESLint |
All stock, pricing, vessels and sailings are authored placeholder data.
REPLACE-ME.md lists every invented value and where it lives. The public footer
carries a standing notice saying so — remove it only when the data is real.
DESIGN.md is the system of record for the visual language ("The Departure
Board"), with .impeccable/design.json carrying shadows, motion and component
snippets. Read it before adding a surface.