-
Notifications
You must be signed in to change notification settings - Fork 0
Operator Guide
This chapter is for whoever installs, runs, and keeps this system alive — comfortable with a terminal, not assumed to know this repository already.
Docker and Docker Compose. That's the whole list — make dev brings up Postgres, the
API, the register app, and the back office in containers, hot-reloading against the
working tree, with nothing else installed on the host.
Note: a fully native path (no Docker at all — Postgres,
php artisan serve,npm run devrun directly) remains supported and is documented in the repo's rootCLAUDE.md. This chapter covers the container path, which is the front door.
Five steps, in order — the third is a paste, not a command:
-
cp .env.example .env— copies the documented shape of the root.envfile. Only needed the first time. -
make dev-key— mints a LaravelAPP_KEYusing nothing but Docker (no vendor install, no compose, no existing key required). It prints a line likebase64:xxxxxxxx.... - Paste that value into
.envasPOS_DEV_APP_KEY. -
make dev— brings up the full dev stack: database, API, register app, back office. -
make seed— runs a fresh migrate and seeds a believable business: two locations, staff at every role, a catalog covering both retail and food service.
make seed prints three tables, and you'll want all three the first time:
-
Development PINs — one row per person, with their PIN and role, e.g. Alice /
1111/ cashier @ Downtown. Use one of these to clock in at a register. -
Device tokens — one row per till, register name and device token (e.g.
DT / Till 1). Paste one into a register's Enroll this terminal screen (see Getting Started) to bring that till online. -
Back-office login — an email and password (
POST /api/v1/admin/login) for signing in to the back office.
Note:
make seedis destructive — it's a fresh migrate every time. Re-run it whenever you want a clean slate, but not against data you meant to keep.
Once it's up: the API is at http://127.0.0.1:8000, the register app at
http://127.0.0.1:5174, the back office at http://127.0.0.1:5175. make ps shows
the actual host ports if you've overridden any of them (see POS_DEV_*_PORT in
Troubleshooting).
make help lists every target. The ones you'll reach for:
| Target | Does |
|---|---|
help |
List available targets |
dev |
Bring up the full dev stack (db, api, register, back office) |
dev-down |
Stop the dev stack (volumes survive) |
logs |
Tail dev stack logs |
ps |
Dev stack status |
dev-key |
Mint an APP_KEY for the root .env — no vendor, no compose, no existing key needed |
seed |
Fresh migrate + seed (prints dev PINs and device tokens) |
migrate |
Run pending migrations |
e2e |
Reseed (twice — see comment above), run all three committed e2e proofs, THEN LEAVE THE DEV DB DIRTY with two seeds' + e2e-admin-day's data (re-run make seed after for a clean slate). Needs the api container reachable at http://127.0.0.1:8000 — the scripts hardcode it; override POS_DEV_API_PORT back to 8000 in root .env if something else is squatting on it. |
test |
All suites, in containers |
test-backend |
Pest against the compose db (creates pos_test if missing) |
test-web |
Register app vitest |
test-bo |
Back-office vitest |
typecheck |
tsgo on both frontend apps |
clean |
Dev stack down AND volumes destroyed (asks first) |
build |
Build all three production images |
prod-up |
Start the production stack (needs .env — see .env.prod.example) |
prod-down |
Stop the production stack |
prod-logs |
Tail production logs |
backup |
pg_dump -Fc the stack db -> backups/pos-.dump (COMPOSE=prod for prod) |
restore |
Restore FILE=backups/... into the running db (DESTRUCTIVE, asks first) |
restore-drill |
Prove the newest backup restores: throwaway db, row counts, teardown |
Production runs the same three images behind one edge (the API container is
FrankenPHP, which is also Caddy — it terminates TLS and reverse-proxies to the two
frontends by hostname). It's still one docker compose stack, just a different
compose file.
Before the first boot:
-
DNS. Point two hostnames at the host: one for the register app, one for the back office. Both need ports 80 and 443 reachable from the internet if you want real certificates (see TLS, below).
-
.env, copied from.env.prod.exampleand filled in, sitting besidecompose.prod.yml:Variable One line POS_DB_PASSWORDPostgres password for the prod db container — a real secret, not dev's throwaway default. POS_APP_KEYLaravel APP_KEY — mint with make dev-key, paste thebase64:...value.POS_REGISTER_DOMAINPublic hostname for the register app + API (needs DNS + reachable 80/443 for a real certificate). POS_ADMIN_DOMAINPublic hostname for the back-office app (same requirement). POS_CURRENCYISO 4217 currency code (e.g. USD) — required, the app refuses to boot without it.POS_BUSINESS_NAMELegal/trading name printed on receipts and the Z-report. POS_BUSINESS_ADDRESSOptional — printed on receipts if set. POS_BUSINESS_TAX_IDOptional — tax/VAT registration id printed on receipts if set. POS_TLS_ISSUEROptional, default acme— set tointernalfor a domainless local boot.POS_MIGRATE_ON_BOOTOptional, default 1— runsphp artisan migrate --forceon every api boot; set0to migrate by hand instead. -
make prod-up— builds and starts the production stack.
TLS is automatic — Caddy provisions and renews certificates for
POS_REGISTER_DOMAIN and POS_ADMIN_DOMAIN on its own once DNS and 80/443 are in
place. No certificate files to manage.
To smoke-test without real DNS, set POS_TLS_ISSUER=internal and use
register.localhost / admin.localhost as the two domains (both resolve to
127.0.0.1 with no /etc/hosts edit). Caddy issues from its own local CA instead of
a public one, so curl needs -k against it.
Note: the Compose project name
pos(onlycompose.prod.ymluses it —compose.dev.ymlispos-dev, a separate volume namespace) claims thepos_pgdatavolume outright. If this host ever ran the retiredinfra/docker-compose.yml(same default project name), it attaches to that same volume — a real database, not a fresh one. Tear that down with-vfirst, or boot this stack withCOMPOSE_PROJECT_NAMEset to something else.
make backup # pg_dump -Fc the stack db -> backups/pos-<utc>.dump
make restore FILE=backups/pos-....dump # DESTRUCTIVE, asks first
make restore-drill # prove the newest backup actually restores
Add COMPOSE=prod to any of the three to target the production stack instead of dev
(both stacks name their db service db and their database/user pos, so one set of
targets covers either).
make restore overwrites the live database — it asks you to type restore to
confirm. make restore-drill doesn't touch anything live: it restores the newest
dump into a throwaway Postgres container, prints row counts, and tears the container
down.
Run the drill after every backup you'd actually rely on. As the roadmap puts it: an untested backup is a rumor.
make test # all three suites (backend/web/back-office), in containers
make e2e # the three committed end-to-end proofs, against the running stack
Note:
make e2ereseeds the dev database twice and leaves it dirty on purpose — two seeds' worth of fixtures plus everythinge2e-admin-day.shwrote. Runmake seedagain afterward before using the dev stack for anything else.
Register drops back to "Enroll this terminal" on its own. Its device token was
rejected (the API returns invalid_device_token) — most often because it was
reissued, which invalidates the old token in the same action. This happens whenever a
manager replaces a lost terminal. Get a fresh token — in dev, make seed prints one
per till; otherwise a manager reissues one from the back office — and re-enroll the
till with it (see Getting Started).
A till's drawer won't reconcile at close. Read the Z-report for that shift first — it breaks down sales by tender and any cash movements (payouts, paid-ins) recorded during the shift. If the counted cash still doesn't match, the close records a variance that needs approval.
Note: variance approval must come from a different register at the same location than the one that just closed. Closing a shift revokes every staff session bound to that register, so a request from the just-closed till 401s — the check is on location, not the specific terminal, so a request from any other open till there succeeds. This version's register app doesn't have a screen for it, though: a till only ever shows its own currently open shift, never another till's, so there's no button to tap at that other till either. Approving today means calling the API directly —
POST /api/v1/shifts/{id}/approve-variance— with a staff session from that other till, the wayscripts/e2e-lunch-service.shdoes it (see the Supervisor Guide).
Port already in use. Override the host-side port in .env rather than stopping
whatever's already listening: POS_DEV_API_PORT, POS_DEV_WEB_PORT,
POS_DEV_BACKOFFICE_PORT, or POS_DEV_DB_PORT. The containers keep listening on
their usual internal ports either way.
Synced from docs/ at 49febb9 — edit in the repo, not here.
User Manual
Technical Documentation