-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
KinetiRx is a three-tier system: a React SPA, a Go REST API, and Postgres — plus an optional, currently-scaffolded MCP server component. This page covers component boundaries, data flow, the auth model, and the AI OCR integration.
┌─────────────────────┐ /api/* ┌──────────────────────┐
│ frontend/ (SPA) │ ────────────────► │ backend/ (Go/Gin) │
│ React 19 + Vite + │ JWT bearer │ REST API, JWT auth, │
│ TypeScript + Tailwind│ token │ bcrypt, migrations │
└─────────┬────────────┘ └───────────┬──────────┘
│ served by nginx (frontend/nginx.conf) │ database/sql
│ reverse-proxies /api/* to backend ▼
│ ┌───────────────────┐
▼ │ PostgreSQL 16 │
browser (end user) │ (deploy/docker- │
│ compose.yml │
│ volume-backed) │
└───────────────────┘
Go + Gin, organized under internal/:
-
internal/handlers/— one file per resource (medicines, patients, due-khata, sales, expenses, needed-meds, opd, distributors, campaigns, tasks, employees, daily-register, invoice-config, ai/gemini/ocr, health).router.gowires routes to handlers and middleware. -
internal/auth/— JWT issuing/verification (jwt.go) and bcrypt password hashing (password.go). -
internal/middleware/— auth + permission enforcement applied per-route. -
internal/models/— Go structs mirroring the API's JSON object shapes (see API Reference). -
internal/db/— Postgres connection setup. -
internal/seed/— first-boot seeding of theEMP-ADMIN-1account fromKINETIRX_ADMIN_PASSWORD. -
internal/config/— environment variable loading. -
internal/httpx/— shared JSON response/error envelope helpers. -
migrations/— SQL migrations (0001_init.up/down.sql), run automatically on backend startup. -
cmd/server/main.go— entrypoint.
React 19 + Vite + TypeScript + Tailwind CSS v4, structured as:
-
src/components/tabs/— one component per top-level app section (Dashboard, DailySales, POS, DueKhata, MedicineOrders, Inventory, InwardOCR, OPD, Patients, Expenses, BusinessDev, EmployeeMgmt, InvoiceSettings, SystemReset) — seesrc/types.ts'sTabTypefor the authoritative list. -
src/components/modals/— dialogs (add/edit patient, stock, expense, OPD visit, employee, distributor, invoice print, AI finder, login, etc.). -
src/lib/api.ts— the single typed fetch client for the backend; every network call in the app goes through this file, using the JSON error envelope and JWT bearer auth documented inbackend/API.md. -
src/hooks/useTheme.ts— dark/light mode. -
src/hooks/useSyncedResource.ts— generic data-fetching/sync hook used by the tabs to load and mutate backend resources. -
src/types.ts— shared TypeScript types, includingTabType(doubles as the app's permission-key vocabulary — see Auth model below).
The frontend is a pure static SPA: no server-side secrets are ever bundled
into it. VITE_API_URL is a build-time constant; when unset it defaults to
http://localhost:8080 in code, and in the Docker Compose deployment it's
deliberately left empty so requests are same-origin (/api/...) and nginx
proxies them to the backend container.
-
docker-compose.yml— the reference self-hosted deployment:postgres(16-alpine, not published to the host),backend(built from../backend/Dockerfile, published onBACKEND_PORT, default8080), andfrontend(built from../frontend/Dockerfile, an nginx image serving the built SPA and reverse-proxying/api/*, published onHTTP_PORT, default3080). -
casaos-manifest.yml— a separate x-casaos v2 compose-extension manifest for CasaOS App Store submission. It's structurally similar but pulls pre-built images fromghcr.io/raktim94/kinetirx-{backend,frontend}rather than building from source (CasaOS installs by pulling published images, not by runningdocker compose --buildagainst a checked-out repo) — see Deployment for why these are two separate files.
A Go module (kinetirx/mcp-server, depends on
github.com/modelcontextprotocol/go-sdk) exists but currently contains only
go.mod/go.sum — no server source code yet. See MCP Server
for current status.
- Browser loads the SPA from nginx (
frontend/container, orvite devin local development). - The SPA calls
/api/*viasrc/lib/api.ts, attachingAuthorization: Bearer <JWT>for every route except/api/healthand/api/auth/login. - nginx (in the Compose deployment) proxies
/api/*to thebackendservice over the internal Compose network; in local dev, Vite talks directly toVITE_API_URL(defaulthttp://localhost:8080). - The Go backend's middleware verifies the JWT, re-checks the caller's
permissions against the target route, then the handler reads/writes
Postgres via
database/sqland returns the JSON envelope described in API Reference. - List/detail responses flow back through
api.tsintouseSyncedResource-backed state in the relevant tab component.
-
Login (
POST /api/auth/login) accepts an employeeidorname(case-insensitive) plus password, verifies against the bcrypt hash stored server-side, and issues a JWT access token valid for 12 hours. There is no refresh-token endpoint — the client re-authenticates after expiry. - Every other route (except health check) requires that JWT as a bearer token.
-
Authorization is enforced server-side, in
internal/middleware/, per route: most routes require the caller to hold a specific permission string (aTabTypevalue, e.g."inventory","pos","patients") in theirpermissionsarray, or holdrole: "admin"(admins implicitly pass every permission check). -
Employee mutation (
POST/PUT/DELETE /api/employees) is gated specifically onrole: "admin"— a permission array entry alone cannot grant the ability to create/edit/delete other employees. -
Safety rails: the API refuses (
409 conflict) to delete or demote the last remaining admin account, and refuses to let an admin delete their own currently-authenticated account. - Password/PIN hashes are marked
json:"-"in the Go models, so they can never leak through any response, by construction rather than by convention.
Two backend endpoints call the Gemini REST API's generateContent directly
over net/http (no SDK dependency), using GEMINI_API_KEY from the
environment:
-
POST /api/ocr/parse-bill(permission:inward-ocr) — accepts either a base64 image (imageBase64, with an optionaldata:URL prefix that's stripped automatically) or raw OCR/plain text (textContent) of a distributor purchase bill, and asks Gemini (gemini-3.7-flash) to extract a structured invoice: distributor details, invoice number/date/total, and a line-item array (medicine name, company, salt, pack, HSN, batch, expiry, qty, rate, MRP, scheme, discount, GST). If Gemini's reply isn't valid JSON, the raw text is returned instead (rawText,data: null) so nothing is silently dropped. -
POST /api/ai/ask— any authenticated employee can send a free-textprompt(optionally withmedicineContext) and get back a clinical-pharmacist-styled answer.
Fallback behavior: when GEMINI_API_KEY is unset, both endpoints return
{"success": false, "fallback": true, ...} with a canned offline message
instead of erroring — the frontend's Inward OCR tab and AI assistant modal
degrade gracefully rather than breaking when no key is configured. A genuine
Gemini call failure (key present but the request fails) returns 500 with
type: "ocr_extraction_failed" or "ai_request_failed".
No image or prompt data is stored by KinetiRx beyond what the user explicitly saves into inventory/records after reviewing the OCR result — the OCR call itself is stateless from the backend's perspective.