Architecture
Audience: engineers joining the project, ops teams evaluating Panorama for self-hosting. Kept deliberately code-focused; product/marketing lives in the README.
1. High-level
┌────────────────────────────────────────────────┐
│ Edge / CDN │
│ (Cloudflare / fronting nginx) │
└────────────────────────────────────────────────┘
│
┌────────────────────────────────┼─────────────────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌────────────────┐
│ apps/web │ │ apps/admin │ │ apps/mobile │
│ (Next.js 14) │ │ (Next.js 14) │ │ (Expo/RN) │
└──────┬───────┘ └──────┬───────┘ └────────┬───────┘
│ │ │
└─────── OIDC cookie + REST ──┴────── REST + webhooks ─────────────┘
│
┌────────────────┴─────────────────┐
│ apps/core-api │
│ (NestJS 10, Node 20) │
│ ┌───────────────────────────┐ │
│ │ Domain modules: │ │
│ │ auth, tenants, users, │ │
│ │ assets, bookings, │ │
│ │ inspections, maintenance,│ │
│ │ reports, notifications, │ │
│ │ audit, plugin-host │ │
│ └───────────────────────────┘ │
└────────────────┬─────────────────┘
│
┌──────────────┬─────────────────┬─┴──────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌───────────────┐ ┌──────────┐ ┌────────────┐
│ Postgres│ │ Redis │ │ S3-compat │ │ Search │ │ Event bus │
│ 16 │ │ (cache, │ │ object store │ │ Postgres │ │ NATS JS / │
│ +RLS │ │ BullMQ) │ │ (photos, │ │ FTS or │ │ Redpanda │
└─────────┘ └──────────┘ │ backups) │ │ OpenSearch│ └────────────┘
└───────────────┘ └──────────┘Request lifecycle
- A logged-in user opens the web app; Next.js middleware validates the OIDC session cookie, extracts
tenant_idand role, and forwards to the core-api withX-Tenant-Idand a short-lived JWT. - The core-api's
TenantMiddlewaresetsrequest.tenantIdand starts a Prisma transaction withSET LOCAL app.current_tenant = $1so RLS and Prisma middleware agree on the scope. - The controller resolves the use case, the domain module runs, writes go through Prisma (scoped by middleware), domain events are published to the event bus, jobs are queued in BullMQ.
- Audit log rows are written synchronously in the same transaction as the mutation — if the transaction rolls back, the audit log rolls back with it.
- Notifications fire after commit via an outbox pattern; a worker drains the outbox and retries on failure.
2. Data plane
Postgres 16 with:
pg_stat_statements,pgcrypto,uuid-ossppgvector(Enterprise-only for predictive maintenance embeddings)- RLS enabled on every tenant-owned table (see ADR-0003)
Connection pooling via PgBouncer (transaction mode) on self-host deployments, or AWS RDS Proxy / Cloud SQL PgBouncer on managed setups.
Schema lives in apps/core-api/prisma/schema.prisma. Migrations are Prisma migrations + optional hand-written SQL for RLS policies (Prisma doesn't model those natively).
3. Async work
All deferred work goes through BullMQ queues backed by Redis:
emails— outbound SMTP (Snipe-IT + FleetManager both had email queues; we keep the pattern, tuned for idempotent retry)webhooks— outbound HTTP webhooks with HMAC signatures + exponential backoffreports— long-running report generation (CSV, XLSX, PDF)sync— third-party data sync (SCIM pushes, import jobs)maintenance— scheduled checks (training-expiry, overdue-return, sync-staleness — the equivalent of FleetManager's CRON suite)
Jobs are idempotent; the worker tier is horizontally scalable.
4. Event bus
Domain events (panorama.asset.checked_out, panorama.reservation.approved, etc.) are published to NATS JetStream. Consumers include:
- Notification service (decides email / Teams / Slack / webhook)
- Audit log replicator (optional — streams to SIEM)
- Plugin host (delivers events to registered plugin subscribers)
Events are retained for 7 days by default; replay is supported for plugin bootstrapping. In Community edition, NATS is optional — if disabled, events are dispatched in-process synchronously and plugins still work (just without replay).
5. Auth / authz
- Session — encrypted cookie (iron-session), per-tenant session secret, 5-minute refresh.
- OIDC / SAML — handled by the
authmodule; OIDC viaoidc-provider, SAML vianode-saml. IdP group claims map to Panorama roles via a per-tenantgroup_role_mappingtable. - API tokens — short-lived (default 15 min) via OAuth2 client-credentials, plus long-lived (90 days max) personal access tokens that hash to sha256 in the DB.
- Authorisation — CASL 6. Every endpoint asks
Ability.can('update', asset). Abilities are built from the authenticated principal's role + plugin extensions. - 2FA / WebAuthn — TOTP always available in Community; WebAuthn + passkeys in Community; attestation-required WebAuthn (FIDO2 AAL-2) in Enterprise only.
6. Multi-tenancy
See ADR-0003. Short version:
tenant_id(company_idin UI) on every tenant-owned table- Prisma middleware injects the predicate
- Postgres RLS enforces the same at the DB layer
- Super Admin bypass is opt-in per request
7. i18n
packages/i18n/{en,pt-br,es}/*.json— translation bundles, keyed by feature- Frontend uses
next-intl; backend usesnestjs-i18nfor email / webhook templates - CI check: every EN key must exist in PT-BR and ES; missing keys fail the build. A
scripts/i18n-check.tsproduces a coverage report per PR. - Contributor flow: add keys in EN, open PR, CI shows missing translations, a Crowdin or Weblate integration fills them (decided in a later ADR).
8. Observability
- Structured logs — pino, JSON, correlation-id middleware
- Traces — OpenTelemetry → Jaeger / Tempo / any OTLP endpoint
- Metrics — Prometheus exposition at
/metricson the admin port - Error reporting — Sentry optional, configured via env
- Audit log — append-only, hash-chained (each row's hash depends on the previous row's hash) to make tampering detectable
9. Plugin host
See ADR-0006. Plugins live in plugins/ directory on the self-hosted install or are installed via panorama plugins install @vendor/foo. Each plugin has:
manifest.json— permissions, version range, events subscribedserver.ts— NestJS dynamic moduleclient.tsx— React slot components
Plugins run in-process in Community; Enterprise offers a sidecar mode for isolation.
10. Deployment
Single-node Docker Compose
infra/docker/compose.prod.yml spins up:
core-api(1 replica)worker(1 replica)web(1 replica)admin(1 replica)postgres,redis,minio,nats
For teams up to ~100 users / 10k assets. No external dependencies.
Kubernetes + Helm
infra/helm/panorama provides:
core-apiDeployment with HPA on CPU and custom metricrequests_in_flightworkerDeployment, HPA on queue depthweb,adminas separate Deployments behind a shared Ingress- Managed Postgres (not bundled — you plug in your RDS / Cloud SQL / Patroni)
- Redis / NATS via Bitnami charts or your own
ServiceMonitorfor Prometheus scraping
Terraform blueprints
infra/terraform/aws/ (GCP and Azure mirrors) provision:
- RDS Postgres (
db.t4g.mediumstarter, encrypted) - ElastiCache Redis
- S3 bucket with KMS + lifecycle rules
- VPC, subnets, security groups
- ECR repositories for images
- IAM roles for the worker tier
11. Migration from Snipe-IT / FleetManager
The @panorama/migrator package reads from:
- Snipe-IT API (no direct DB access; tokens authenticated per role)
- Optional SnipeScheduler-FleetManager MySQL dump for reservation history
It produces Panorama fixtures in JSON form, which a panorama import-fixtures command loads into a target Postgres instance inside a transaction. Dry-run mode prints a diff without touching the DB.
During the migration window (weeks, not minutes), a compatibility shim exposes Snipe-IT-style endpoints (/api/v1/hardware, /api/v1/users, etc.) so existing integrations keep working without rewrite. The shim is a read/write proxy that translates to the Panorama native API.
12. Versioning
- SemVer for the platform (major / minor / patch)
- Prisma schema versions match the platform major.minor
- API versioned via
Accept: application/vnd.panorama.v1+jsonheader, not URL path (/api/v1) - Plugin SDK has its own SemVer; compatible with a range of platform versions declared in each plugin's manifest