Skip to content

Architecture

raktim edited this page Aug 21, 2026 · 1 revision

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.

Component breakdown

┌─────────────────────┐      /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)    │
                                                 └───────────────────┘

backend/

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.go wires 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 the EMP-ADMIN-1 account from KINETIRX_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.

frontend/

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) — see src/types.ts's TabType for 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 in backend/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, including TabType (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.

deploy/

  • docker-compose.yml — the reference self-hosted deployment: postgres (16-alpine, not published to the host), backend (built from ../backend/Dockerfile, published on BACKEND_PORT, default 8080), and frontend (built from ../frontend/Dockerfile, an nginx image serving the built SPA and reverse-proxying /api/*, published on HTTP_PORT, default 3080).
  • 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 from ghcr.io/raktim94/kinetirx-{backend,frontend} rather than building from source (CasaOS installs by pulling published images, not by running docker compose --build against a checked-out repo) — see Deployment for why these are two separate files.

mcp-server/ (scaffold only)

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.

Data flow

  1. Browser loads the SPA from nginx (frontend/ container, or vite dev in local development).
  2. The SPA calls /api/* via src/lib/api.ts, attaching Authorization: Bearer <JWT> for every route except /api/health and /api/auth/login.
  3. nginx (in the Compose deployment) proxies /api/* to the backend service over the internal Compose network; in local dev, Vite talks directly to VITE_API_URL (default http://localhost:8080).
  4. 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/sql and returns the JSON envelope described in API Reference.
  5. List/detail responses flow back through api.ts into useSyncedResource-backed state in the relevant tab component.

Auth model: JWT + role/permissions

  • Login (POST /api/auth/login) accepts an employee id or name (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 (a TabType value, e.g. "inventory", "pos", "patients") in their permissions array, or hold role: "admin" (admins implicitly pass every permission check).
  • Employee mutation (POST/PUT/DELETE /api/employees) is gated specifically on role: "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.

Gemini AI OCR integration

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 optional data: 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-text prompt (optionally with medicineContext) 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.

Clone this wiki locally