Asset Manager (AMS) is a Postgres-backed platform for tracking physical and digital assets. The UI is a React 19 single-page app (Vite 7); authNexus (OIDC) handles sign-in; a FastAPI 0.136 backend serves /api/v1/* (plus GET /api/health and GET /observability/logs) with RS256 JWT validation when auth is enabled, role-based access control, and full audit-trail mutations. Optional email notifications POST to an external microservice. A local Grafana OSS stack (under Observability/) aggregates logs, metrics, and traces.
| Folder | What it contains |
|---|---|
Client/ |
React + Vite + TypeScript SPA (Client/package.json, dev port 5174) |
Server/ |
FastAPI app (Server/main.py), asyncpg pool, versioned API routers |
DB/ |
init.sql full schema dump + pgAdmin bootstrap files |
Observability/ |
Loki, Tempo, Prometheus, Grafana, Alloy configs; env template — services run via root docker-compose.yml |
logs/ |
Runtime log files tailed by Alloy (ams_server.log, telemetry_server.log) |
Notes/ |
Product requirements and authNexus API reference docs |
email.service.implementation.readme.md |
Spec for the external email notification microservice (not shipped in Server/) |
- The browser calls the FastAPI server for all application data. Base URL:
VITE_API_URL(defaulthttp://localhost:8000). Integration layers:Client/src/api.ts,Client/src/api/apiClient.ts,Client/src/utils/authNexus.api.ts,Client/src/services/*,Client/src/queries/*. - Sign-in uses OIDC via
oidc-client-ts(Client/src/utils/authService.ts, callback atClient/src/components/pages/AuthCallback.tsx). The server validates RS256 access tokens via JWKS whenAUTH_ENABLED=true(Server/core/auth_middleware.py,Server/core/authnexus.py).POST /api/auth/refresh(Server/routers/api_auth.py) forwards refresh using the HttpOnlynexus_refresh_tokencookie so the SPA can obtain new access tokens without silent OIDC renewal. - Postgres is the system of record. The asyncpg pool is created from
DATABASE_URLor individualPOSTGRES_*vars (Server/core/postgres.py). Business rules live inServer/repositories/andServer/services/. - Database schema:
DB/init.sqlis a fullpg_dumpof the schema including tables, views, functions, and seed categories. It is the authoritative schema reference. - Email notifications: the AMS server POSTs fire-and-forget events to an external email microservice when
NOTIFICATIONS_ENABLED=true(Server/services/notifications/). - Observability:
RequestIdMiddlewarelogs everyrequest_completedevent with path, status, and elapsed ms. The React client sends OpenTelemetry browser traces whenVITE_OTEL_GRAFANA_ENABLED=true(Client/src/otel-telemetry.ts). Loki is queried server-side atGET /observability/logs(IT Ops role only) — the browser never calls Loki directly.
- Roles:
employee,admin,it_ops— enforced inServer/core/authz.py. The client treats any role other thanemployeeas privileged (RequirePrivilegedinClient/src/App.tsx). - Asset status enum:
in_stock,assigned,in_repair,retired,lost,disposed(defined inDB/init.sql). - Asset tags: auto-generated as
AST-#####byfn_next_asset_tag()in Postgres when not supplied by the client. - Flags:
employees.is_active(employment status) andemployees.erp_activeare separate columns with distinct semantics. - Soft delete / Recycle Bin: assets and employees are soft-deleted (
is_deleted = true) and tracked inrecycle_bin_entries. The HTTP API for the recycle bin is/api/v1/recycle-bin— not nested under/assets. - Public scan:
/scan/:id(no auth) and/assets/scan/:id(authenticated) both useClient/src/components/pages/ScanPage.tsx. The server endpoint isGET /api/v1/assets/public-scan/{asset_tag}for anonymous access.
Client/CLIENT_README.md— client routes, env vars, component map, dependenciesServer/SERVER_README.md— API endpoints, env vars, local run, middleware stackServer/services/README.md— service-layer modulesServer/services/notifications/README.md— email adapter and orchestratorDB/README.md— database schema, setup, and seed dataObservability/OBSERVABILITY_TELEMETRY.md— local observability stack setup and telemetry guideDOCKER_DEPLOYMENT.md— Docker Compose: ports, env, single-file stackemail.service.implementation.readme.md— external email microservice contract (Supabase audit, templates)CHANGELOG.md— release notes
Provision a Postgres 15 instance and apply the schema:
psql -U postgres -c "CREATE DATABASE assetmanager_db;"
psql -U postgres -d assetmanager_db -f DB/init.sqlSee DB/README.md for full details.
cd Server
cp .env.example .env # fill in POSTGRES_*, AUTH_*, FRONTEND_URL
pip install -r requirements.txt
uvicorn main:app --reload --host 0.0.0.0 --port 8000Server/app.py re-exports main.app for older uvicorn app:app invocations.
cd Client
cp .env.example .env # fill in VITE_API_URL, VITE_AUTH_AUTHORITY, VITE_CLIENT_ID, etc.
npm install
npm run dev # starts on http://localhost:5174Configuration lives under Observability/ (Observability/.env from .env.observability.example). With local Docker, observability services start together with the app from the repo root compose file (see step 5).
Grafana (typical published port 11200): http://localhost:11200 · Prometheus: http://localhost:9090 · Loki: http://localhost:3100
From assetmanager/, root docker-compose.yml starts observability, Postgres, FastAPI, nginx client, and pgAdmin on one network.
cd assetmanager
cp Observability/.env.observability.example Observability/.env # Grafana / Alloy vars
# edit .env with POSTGRES_*, VITE_* , AUTH_*, FRONTEND_URL, ALLOWED_ORIGINS, ports
docker compose up --build -dSee DOCKER_DEPLOYMENT.md for port mapping and rebuild notes.
- Provision Postgres and apply
DB/init.sql. - Configure Server
.env— database connection,AUTH_ENABLED,AUTH_JWKS_URL,AUTH_ISSUERandAUTH_AUDIENCE(must match access-tokeniss/audfrom authNexus),AUTH_PROJECT_ID,AUTH_AUTHORITY(required forPOST /api/auth/refresh),FRONTEND_URL,ALLOWED_ORIGINS. - Configure Client
.env—VITE_API_URL, authNexus OIDC vars, optional telemetry. - Start the server, then the client.
- Provision at least one employee row with
role = 'it_ops'or'admin'to access privileged routes.
flowchart TD
subgraph client["Client — SPA"]
A([User starts sign-in]) --> B[Redirect to authNexus OIDC]
B --> C([User authenticates])
C --> D[Callback with authorization code]
D --> E[Exchange for access token]
end
subgraph backend["Server — FastAPI"]
F["GET /api/v1/employees/me\nAuthorization: Bearer …"]
F --> G[JWKS verify JWT when AUTH_ENABLED]
G --> H[Resolve employee from token / DB]
H --> I{Provisioned?}
I -- Yes --> J[Return employee context]
I -- No --> K([403 — not provisioned])
end
E --> F
J --> L([200 — session profile])
| Variable | Where | Purpose |
|---|---|---|
DATABASE_URL or POSTGRES_* |
Server | Postgres connection |
AUTH_ENABLED |
Server | Enable JWT validation (default false) |
AUTH_JWKS_URL |
Server | Required when AUTH_ENABLED=true |
AUTH_ISSUER / AUTH_AUDIENCE |
Server | Optional but recommended when validating JWTs; AUTH_AUDIENCE must match the access token aud or APIs return 401 (Audience doesn't match) |
AUTH_AUTHORITY |
Server | authNexus base URL; used by POST /api/auth/refresh |
AUTH_PROJECT_ID |
Server | JWT project scope check |
FRONTEND_URL |
Server | Base URL embedded in QR code PDFs |
ALLOWED_ORIGINS |
Server | CORS allowed origins (comma-separated) |
NOTIFICATIONS_ENABLED |
Server | Enable email microservice calls |
OTEL_GRAFANA_ENABLED |
Server | Expose /metrics for Prometheus |
LOKI_BASE_URL |
Server | Loki URL for GET /observability/logs |
VITE_API_URL |
Client | FastAPI server base URL |
VITE_AUTH_AUTHORITY |
Client | OIDC issuer URL |
VITE_CLIENT_ID |
Client | OIDC client ID |
VITE_PROJECT_ID |
Client | Must match server AUTH_PROJECT_ID |
VITE_OTEL_GRAFANA_ENABLED |
Client | Enable browser OTel traces |
To back up your data from the Docker container, use the following commands:
docker exec -t ams-postgres-docker pg_dump -U assetmanager_user -d assetmanager_db > database_dump.sqldocker exec -t ams-postgres-docker pg_dump -U assetmanager_user -d assetmanager_db -s > schema_only.sqlcat database_dump.sql | docker exec -i ams-postgres-docker psql -U assetmanager_user -d assetmanager_dbIf nginx uses a default_server that return 404 for unknown server_name, browsing by raw IP on port 80 can 404 by design. Use the configured hostname (for example ams.rokkalabs.com) so the server block that proxies to the client matches.
The access token’s aud claim must match server AUTH_AUDIENCE (and typically iss matches AUTH_ISSUER when set). If authNexus issues aud: default_client but the API expects a project API audience, align IdP application settings or set AUTH_AUDIENCE to the value your tokens actually carry.
| Context | Client UI | API | Grafana (host) |
|---|---|---|---|
| Local dev | 5174 (Vite) |
8000 |
11200 if compose publishes it |
Docker (see .env) |
11000 |
11100 |
11200 |
- QR link origin:
Client/src/utils/qr.tsbuilds scan URLs usingFRONTEND_URL→VITE_PUBLIC_APP_ORIGIN→ a hardcoded production fallback.getScanPageBaseUrlinClient/src/api.tsonly checksVITE_PUBLIC_APP_ORIGIN; keep both env values consistent. - CORS:
Server/main.pyreadssettings.ALLOWED_ORIGINSand always appendshttp://localhost:11000if not already present. For Vite on5174, addhttp://localhost:5174toALLOWED_ORIGINSwhen testing cross-origin tolocalhost:8000. - Request tracing: every response carries
x-request-id(set byRequestIdMiddleware). SendX-Request-Idon requests to correlate with server logs. - Prometheus metrics: exposed at
GET /metricsonly whenOTEL_GRAFANA_ENABLED=trueon the server. app.py: legacy compatibility shim — re-exportsmain.appsouvicorn app:appstill works.