What it is, how to run it, how configuration works, the module layout, the seven steps to add an endpoint, the standing surprises, and a Kubernetes cheat-sheet — each pointing at the detailed doc. Plus a backend-developer section in SCAN_TO_ORDER.md: file map, local try-out, tests, tuning knobs, and how to change the embedding model or add a provider. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.6 KiB
Fiesta backend (nearle)
The Go/Fiber API behind the Nearle Daily merchant console, the customer app,
the rider app and the in-store POS terminals. Postgres (nearledb) for
tenants, stores, products, stock and orders; a separate pgvector database for
the global product catalogue; Redis for POS presence; MQTT for the tills.
This page is the map. Each section says what a thing is, how to use it, and where the detail lives.
Run it
export PATH="$PATH:$HOME/go/bin" # Go 1.24 lives there on the dev Macs
docker compose -f docker-compose.local.yml up -d # postgres :5433, pgvector :5434, redis :6379
go run . # APP_ENV unset → .env.local, listens on :1122
go test ./...
An empty database is not enough — startup runs migrations that assume the
live schema. init/README.md explains loading a schema dump first.
Startup prints what it loaded and where it is pointed:
config: loaded .env.local
config: APP_ENV=local, listening on :1122, database nearle@localhost:5433/nearledb
scan: product search uses openai/all-minilm # or: EMBEDDING_PROVIDER not set, text-only
Configuration
Everything comes from environment variables, read once by config.Load().
| You want to… | Do this |
|---|---|
| Run locally | Nothing — .env.local is loaded by default |
| Run against production settings | APP_ENV=production go run . (⚠ every write is real) |
| See every variable and what it does | .env.example |
| Add a new setting | Add it to config.Config + Load() + .env.example, and to the cluster (nearle-config ConfigMap or app-secrets Secret in namespace nearle) — a variable in the file and not in the cluster is unset in production |
| Find out why it won't boot | Read the message — it lists every missing variable at once |
Precedence is real environment > .env.<APP_ENV> > .env. The container gets
no .env file at all (.dockerignore); the Dockerfile sets
APP_ENV=production and the values come from Kubernetes.
Full detail, including the committed-credentials situation:
docs/ENVIRONMENT.md.
Layout
main.go boot: config → databases → migrations → routes → MQTT → listen
config/ env-file loading, typed Config, validation
db/ Postgres (nearledb + catalogue), Redis, S3 image store
facade/ wires repositories → services → controllers (add new modules here)
routes/ one file per module; /live/api/v1/web/... (console) and /v1/mob/... (apps)
controllers/ HTTP in, HTTP out — parse, call the service, shape the envelope
services/ the rules; no SQL, no HTTP
repositories/ the SQL; nothing else
models/ request/response and table shapes
messaging/ MQTT ingest from tills, console live stream
utils/ small shared helpers (tokens, geo, embeddings, geocoding)
docs/ integration specs for the frontends and handoff notes
scratch/ one-off read/verify tools run with `go run ./scratch/<name>`
init/ schema/seed for the local database
Every response uses the same envelope:
{ "code": 200, "status": true, "message": "…", "details": … }.
Business outcomes ("out of stock", "not registered") are 200s with a reason
in the body; HTTP errors mean the request could not be served at all.
Adding an endpoint
- Model the request/response in
models/. - Repository method(s) in
repositories/— SQL only, take acontext.Context, returnerror. - Service in
services/— the rules, with sentinel errors (ErrXxxBadRequest,ErrXxxNotFound) the controller can map to statuses. - Controller in
controllers/—BodyParser/Query, call the service, map sentinel errors to 400/404/503, everything else to 500. - Routes file in
routes/, registered inroutes/routes.go. - Wire it in
facade/container.go. - Test the service with a fake repository (see
services/scan_test.gofor the pattern) — no database needed.
services/scanService.go + controllers/scanController.go are a complete,
current example of all seven.
Features with their own docs
| Feature | For | Doc |
|---|---|---|
| Environment & deployment | everyone | docs/ENVIRONMENT.md |
| Scan-to-order (camera → product → nearest store with stock) | mobile app | docs/SCAN_TO_ORDER.md |
| Catalogue import into a store | console | docs/CATALOGUE_IMPORT_INTEGRATION.md |
| POS terminal ingest, login, API | POS / tills | docs/POS_*.md |
| Access-control audit and what is still open | everyone | docs/SECURITY_HANDOFF.md |
Things to know before you get surprised
- There is no auth layer.
customerid/tenantidin a request are trusted.docs/SECURITY_HANDOFF.md§1 is the standing issue. - Login errors mean what they say.
409 Invalid Email= the query ran and matched nobody.500 Login is temporarily unavailable= the database could not answer (it used to be reported as Invalid Email; seeservices/userService.golookupLogin). - Stock is a ledger. Live stock is always
SUM(in) − SUM(out)ofproductstocksat an outlet, never a stored number. Filter on the same expression you display (services/productVisibility.goexplains why). - The catalogue is a different database and must never be reached
through the
nearledbhandle. Its per-brand tables are discovered frominformation_schema; brands appear and columns vary. - Catalogue ids are not stable across re-scrapes;
imageidis the durable key (models.Products.Imageid). - Migrations run on boot and are guarded by
IF NOT EXISTS/ schema checks, not a version table. Read the comments inmain.gobefore adding one — several have bitten before. - One MQTT client id per replica. A second connection with the same id
evicts the first. Never run a local process with the production
MQTT_URL. scratch/tools read production when run with.env.production. They are read-only by construction; keep them that way.
Operations cheat-sheet (Kubernetes, namespace nearle)
kubectl get pods -n nearle # fiesta-0/1/2 (StatefulSet)
kubectl logs -n nearle fiesta-0 | grep -E 'config:|scan:|pos:'
kubectl exec -n nearle fiesta-0 -- env | grep EMBEDDING_
kubectl set env statefulset/fiesta -n nearle KEY=value # adds a var and rolls the pods
kubectl rollout status statefulset/fiesta -n nearle
The embedding model behind scan-to-order is served by the cluster's Ollama
(ollama.krow.svc.cluster.local:11434); docs/SCAN_TO_ORDER.md has the
exact settings and why that model.