abhishek 8e1549764b Nearle Buddy answers a typed question
Phase 2: the loop and the model gateway. The composer in the console has
said "Not connected yet" since it was built, because there was no
assistant endpoint anywhere. There is one now.

- utils/chat.go   the gateway, a sibling of embedding.go: one small
                  interface, a provider switch, the shared postJSON, no
                  framework. Agents name a TIER (fast/balanced/deep) and
                  config maps tier to model, so changing provider does not
                  touch an agent.
- services/assistantService.go  one loop for every agent. An agent is a
                  name, a tier, a prompt and an allow-list — data, not a
                  class — so a sixth is config rather than a subclass.
- the endpoint under /v1/web, inheriting middleware.WebAuth along with
  every other console route. The assistant reads the same data the console
  does and must read it as the same person.

What the model does not get to decide:

  whose data      the caller is built from the verified session in the
                  controller, never from the request body — there is no
                  tenant field to fill in. A test scripts the model calling
                  a tool with {"tenantid": 916} and asserts it ran for 1147.
  which tools     the registry enforces the agent's allow-list; a test
                  scripts a call to a tool the agent lacks and asserts the
                  handler never ran.
  when to stop    steps and tool calls are counted here. A model that keeps
                  calling tools is stopped by arithmetic, not by being
                  asked nicely.

Two quiet failures have tests of their own. A finish_reason of "length"
means the provider cut the reply off mid-sentence, which reads exactly
like a complete answer unless it is flagged. And a truncated tool result
reaches the model in words it will repeat — otherwise it describes a
capped list and an empty one identically.

A refused tool goes back as a message, not an error: a model told "that
tool needs a tenant" can explain it, where a model handed nothing says
"something went wrong".

Optional, like the embedder. Without ASSISTANT_PROVIDER the endpoint
answers "not switched on here", the composer stays disabled, and the tools
still work — they are ordinary Go functions, and only turning a sentence
into a tool call needs a model.

14 tests, against a scripted model rather than a live provider: these are
about what the loop refuses to let a model do, and that has to hold for
any model, including one behaving badly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 13:13:20 +05:30
2026-09-16 17:09:04 +05:30
env
2026-09-15 12:49:34 +05:30
2026-09-02 10:49:46 +05:30
2026-09-23 13:13:20 +05:30
2026-07-09 18:03:44 +05:30
2026-09-16 17:09:04 +05:30

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

  1. Model the request/response in models/.
  2. Repository method(s) in repositories/ — SQL only, take a context.Context, return error.
  3. Service in services/ — the rules, with sentinel errors (ErrXxxBadRequest, ErrXxxNotFound) the controller can map to statuses.
  4. Controller in controllers/ — BodyParser/Query, call the service, map sentinel errors to 400/404/503, everything else to 500.
  5. Routes file in routes/, registered in routes/routes.go.
  6. Wire it in facade/container.go.
  7. Test the service with a fake repository (see services/scan_test.go for 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 / tenantid in 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; see services/userService.go lookupLogin).
  • Stock is a ledger. Live stock is always SUM(in) − SUM(out) of productstocks at an outlet, never a stored number. Filter on the same expression you display (services/productVisibility.go explains why).
  • The catalogue is a different database and must never be reached through the nearledb handle. Its per-brand tables are discovered from information_schema; brands appear and columns vary.
  • Catalogue ids are not stable across re-scrapes; imageid is 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 in main.go before 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, and most are read-only. Two are not — termbackfill and cataloguefactsbackfill repair rows that no endpoint can reach. Both default to a dry run that prints every change and write only when passed apply, and both print the SQL to undo themselves afterwards. A new tool that writes follows that shape or it does not write.

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.

Description
No description provided
Readme 20 MiB
Languages
Go 99%
PLpgSQL 0.6%
JavaScript 0.2%
Dockerfile 0.2%