FloCafe
FloCafe is the free, local-first restaurant POS in the FloPOS ecosystem. Run orders, tables, kitchen display, and local data without a software subscription.
翻訳待ち
No updates detected yet. GitHub releases will appear here.
MAISON — real-time auction house. Six rounds, two clocks, server-authoritative bidding. Next.js 16 + Postgres.
翻訳待ち
Loading the full README.
Read the full README ↗Next.js 16 (App Router, Turbopack), React 19, Tailwind CSS 4, TypeScript, Postgres. Mongolian UI. The auction runs on the server: it owns the clocks, validates every bid under a row lock, and pushes state over SSE.
npm run db:up # Postgres 16 in Docker
npm run db:migrate # applies db/schema.sql (idempotent)
npm run db:seed # loads the 12 sample lots
npm run dev
.env.local already points at that container. Verification codes have no SMS
provider in development, so they are printed to the server log — look for
[sms:dev].
Six rounds, 2h45m total. Two clocks run at once, and that is the whole game:
| Round | Bid clock | Round length | Min raise | Late entry |
|---|---|---|---|---|
| 1 | 5 min | 25 min | 1 pt | — |
| 2 | 3 min | 25 min | 2 pt | 20 pt |
| 3 | 1 min | 25 min | 2 pt | 30 pt |
| 4 | 30 sec | 25 min | 2 pt | 40 pt |
| 5 | 15 sec | 25 min | 2 pt | 50 pt |
| 6 | 5 sec | 40 min | 2 pt | 60 pt |
round × 10 points above the standing price, from round 2 onward.All of this lives in src/lib/auction.ts as data, not as
scattered conditionals. It is the contract — the server must agree with these
numbers, and nothing else in the front-end hard-codes them.
Postgres, reached through src/lib/repo/*, with the whole of it inside this
Next.js app — no second service. Four pieces:
| Piece | Where | What it is |
|---|---|---|
| Reads | src/lib/api.ts → src/lib/repo/lots.ts | Called from Server Components. server-only, so a Client Component importing them is a build error. |
| Writes | src/app/actions/ | Server Functions ('use server'). Bids, auth, contact. |
| Room updates | /api/room/[lotId]/stream | SSE pushing RoomState, fanned out over Postgres LISTEN/NOTIFY. |
| Clocks | src/lib/auction-engine.ts + src/lib/ticker.ts | The server owns time. See below. |
npm run db:up # docker compose up -d db
npm run db:migrate
npm run db:seed
npm run dev # app on the host, database in Docker
Postgres runs in Docker; the app runs on the host. That split is deliberate —
a bind-mounted node_modules on Windows is slow and confusing, and hot reload
is worth more during development than parity with the deploy image.
| Command | |
|---|---|
npm run db:up / db:down | start / stop Postgres |
npm run db:reset | destroy the volume and start clean |
npm run db:psql | a psql shell in the container |
docker compose --profile app up --build | the real production image, on :3000 |
docker compose --profile tools up -d adminer | a DB browser on :8080 |
Compose also creates a second database, urlug_test, for the integration
tests. docker compose down -v destroys both.
| Route | |
|---|---|
/profile | balance, bid history, lots won |
/wallet | point packages, top-up and ledger history |
/admin | staff only — stats, lots, users, audit, ledger drift |
/terms, /privacy | ⚠ drafted, not reviewed by a lawyer |
Two things in there are worth knowing before editing them.
The terms page interpolates its numbers from auction.ts — round count,
total duration, the late-entry multiplier, the join fee. A terms page is a
promise about how the system behaves, and one typed out by hand drifts from the
code the first time a round length changes, leaving the house contractually
bound to a format it no longer runs.
Age is a date, not a checkbox. A checkbox records that someone clicked a
checkbox; a date of birth is a fact that can be re-checked later, which is what
matters when the question comes from a regulator rather than from us. Each
document accepted at registration gets its own consents row, so "which text
were they shown on the day they placed that bid" has an answer.
src/lib/repo/admin.ts checks no authorisation. requireAdmin() in
session.ts is the only gate, and it calls notFound() rather than returning a
403 — a 403 confirms that /admin exists.
Roles. bidder / staff / admin, changed from the users table on
/admin. Three guards, each for a specific way it goes wrong: nobody may change
their own role (an admin who demotes themselves cannot reach the panel to undo
it, because the route stops existing); the last active admin cannot be demoted
(the only way back would be hand-written SQL against production, and the count
is taken inside the transaction after a row lock, so two admins demoting each
other cannot both pass); and a demotion revokes that user's sessions, because a
staff session that keeps working is the role not really having been taken away.
The first admin is made on the machine that holds the database:
npm run db:make-admin -- --phone 99112233
npm run db:make-admin -- --phone 99112233 --create --password "…" --email [email protected]
This is the beginning of the README. Read the rest at the original. Read the full README ↗