Drinks on tap for your guests. Barkeep puts your bar's menu on everyone's phone, takes their orders, and keeps you on top of what to make next.
You run it yourself, on a machine at home. It is built for a party among friends, not for a shop: there are no accounts and nothing to pay. A guest scans a code, types their name, picks a drink, and waits.
You set up a bar and add the drinks you are pouring, each with a photo and a recipe. You print or show the QR code.
Guests scan it, give a name, and get the menu. They pick something. It lands in your queue.
You accept it, make it, and mark it ready. Their phone keeps up as you go, so nobody has to ask whether their drink is coming. When you run out of something, you mark it out of stock and it stops being offered.
- A menu on their phone, grouped by category or by base spirit
- Only what is actually available; anything out of stock is not offered
- Favourites, which move to the top of their menu
- One drink at a time, tracked from ordered to being made to ready
- The ability to cancel, up until you hand it over
- A "surprise me" button that picks something at random, with a try again
- A list of what they have had, and a way to order the same thing again
- The recipe, on the drinks you choose to share it for
- A queue of orders to accept, mark ready, and mark done, oldest first
- The option to accept everything automatically when the bar gets busy
- Drinks with a photo, a recipe, a base spirit, and a short description for guests
- A cropping tool, so a photo sits nicely on the card
- A choice, per drink, of whether guests see the recipe or only the description
- Out of stock as a toggle, so you do not have to delete anything
- Categories, for grouping the menu how you like
- A QR code to show or print
- Simple figures: orders in total and today, the most popular drinks, and when the rush was
- English and Danish, though some of the host's own screens are still English only
- Everything updates on its own as orders come and go, with nothing to refresh
- If a phone loses signal it reconnects by itself, and only says something is wrong if it stays that way
Barkeep is one container. The menu, the API, the live updates and the drink photos are all served on a single port.
services:
barkeep:
image: ghcr.io/mtaanquist/barkeep:latest
container_name: barkeep
restart: unless-stopped
init: true
ports:
- "127.0.0.1:3000:3000"
volumes:
- ./data:/app/data
- ./uploads:/app/uploadscompose.yaml is the same with a healthcheck and the optional
settings filled in.
docker compose up -dThe port is bound to 127.0.0.1, so only the machine itself can reach Barkeep.
That is on purpose: put a reverse proxy in front to open it up, as below.
Change the first number if something on the machine already uses 3000, and point the proxy at whatever you chose. The second number is inside the container and is better left alone.
Two folders need to survive an upgrade:
| Folder | Holds |
|---|---|
data/ |
the database, one SQLite file |
uploads/ |
drink photos |
Back both up together. The database refers to photos by filename, so they only make sense as a pair.
Open the address in a browser and create a bar. You pick two passwords: one for yourself, and one you give to guests. Those are the only thing between the two views, which is about the right amount of security for a party in a garden.
The QR code is in the settings. Show it on a screen, or print it and leave it on the bar.
| Tag | Built from |
|---|---|
latest |
the released version, and what to normally run |
staging |
the working branch, to try a change out first |
v1.2.3, v1.2 |
a specific release |
sha-abc1234 |
a single build, to pin to an exact commit |
Images are built for Intel and ARM, so a Raspberry Pi works as well as a NUC.
All optional. The defaults suit one container behind a proxy on the same machine.
| Variable | Default | What it does |
|---|---|---|
PORT |
3000 |
Port inside the container. |
DB_PATH |
/app/data/bar.db |
Where the database file lives. |
UPLOADS_DIR |
/app/uploads |
Where drink photos live. |
PUBLIC_URL |
taken from the request | The address to put in QR codes. Needed only if guests reach Barkeep on a different address than the one it sees. |
PUID/PGID |
unset, runs as root | Run as an ordinary user instead. Ownership of the two folders is fixed on start. |
TRUST_PROXY |
this machine and the local network | Which proxies may say what address a guest used. Widening this would let an outsider point your QR codes elsewhere. |
TZ |
UTC |
Which clock "today" is measured against, which decides what counts as today's orders. |
If the proxy runs on the same machine, point it at the published port:
bar.example.com {
reverse_proxy localhost:3000
}Caddy needs nothing else. It passes the live updates straight through.
If the proxy runs in its own container, share a network with it instead of publishing a port at all. Create the network once, outside either stack:
docker network create edgeThen drop the ports: block from compose.yaml and add:
services:
barkeep:
networks: [edge]
networks:
edge:
external: trueThe proxy joins the same network and points at barkeep:3000. Barkeep is then
not reachable from outside Docker at all.
The container reports its own health, so docker ps and Dockge show whether
Barkeep is actually answering rather than merely running. /api/health is the
same check if you want to watch it yourself.
Pull the new image and start it. Any changes to the database are made on the way up, inside the same container, so there is nothing to run by hand.
Each applied change is recorded with a fingerprint of the file that made it. If one of those files is later altered, Barkeep stops and names it rather than running on with the database and the code disagreeing.
Photos no drink refers to are cleared out at startup, but only once they are more than a day old, so one uploaded while you are still filling in the form is safe.
The two halves run separately while you work on them, with the menu proxying
/api and /uploads through to the server.
cd backend && npm install && npm run dev # http://localhost:3000
cd frontend && npm install && npm run dev # http://localhost:5173The server creates backend/data/bar.db and backend/uploads/ on first run.
To build and run the real image from a checkout:
docker compose -f compose.yaml -f compose.dev.yaml up --buildcd backend && npm test && npm run lint && npm run typecheck
cd frontend && npm test && npm run lint && npm run typecheck && npm run buildBoth halves are TypeScript. The shapes the API sends live in one file,
shared/types.d.ts, that both import, so changing one side without the other
is a build error. Both linters fail on a warning.
The tests for the pages run against a stand-in browser, so they need neither a server nor a real one.
Add a dated .sql file to backend/src/db/migrations/. It runs once, inside a
transaction, on the next start, oldest first. Never edit one that has already
run; add another instead.
CLAUDE.md has the rest of the working notes.
Browser -> :3000 -> /api/* the API
/api/events live updates, server to browser
/uploads/* drink photos from disk
/* the menu pages
SQLite, one file, in data/
One container on one address, which keeps the whole thing small enough to hold in your head and leaves nothing to configure between the parts.
Live updates travel one way, from the server to the browser, over a long-lived request. The browser handles reconnecting itself, which matters when a phone sleeps in someone's pocket halfway through the evening.
SQLite suits this well. A party is a handful of people ordering drinks over an evening, and one file is far easier to back up than a database server.