Launching July 6, 2026 Get early access →
APEX

Docs · Technical Reference

Architecture, API, and security posture.

For the engineers who have to sign off before APEX goes into your stack. Stack, auth, tenant isolation, the REST API, plugins, deployment, audit trail format.

1. Stack

BackendNode.js 24 LTS · Express 4 · raw pg driver (no ORM at the query layer, Drizzle for schema definitions only)
DatabasePostgreSQL 16 · single shared deployment · per-tenant row isolation via tenant_id
FrontendVue 3 SFC · Pinia · Naive UI · Vite IIFE bundle · ECharts for charts
AuthJWT RS256 (RSA keypair) · TOTP 2FA · HTTP-only cookies for refresh · CSRF double-submit on mutating routes
DeploymentLinux/Windows · nginx front · Node behind on a private port · Postgres on its own service · TLS terminated at the edge
StorageFile attachments on local disk; S3-compatible bucket (BYO for data-residency) · on the roadmap

2. Keeping every customer's data separate

Tenant isolation is enforced at the database layer, so the application code never has to remember it. Every tenant-scoped table carries a tenant_id integer column. Every authenticated request lands on a route protected by the requireTenant middleware, which reads req.user.tenantId from the JWT and sets req.tenantId. From there, Postgres Row-Level Security does the enforcing: an rlsContext middleware runs SET LOCAL app.tenant_id at the start of each request's transaction, and a FORCE RLS policy on every tenant table makes the database itself filter every read and write to that tenant. A query that forgets to scope returns nothing, never another tenant's rows.

Cross-tenant access returns 404 (never 403) so the tenant id space can't be enumerated. Partner users have an additional scope: requireVendor middleware pins requests to their vendor_id as well, so a partner user authenticated to tenant A cannot see tenant B's data even if both tenants share that partner.

Tenant-scoped tables (partial list)

tenants, users, projects, equipment_items, fieldops, fieldopnotes, vendor_credentials, platform_licenses, discussion_threads, thread_messages, thread_status_changes, thread_mentions, thread_reads, room_status, room_schedules, vendor_invite_tokens, project_vendor_messages, cisco_devices, cisco_locations, cisco_workspaces, ...

3. How logins and 2FA work

APEX issues JWT access tokens signed with an RSA private key (RS256). The public verification key is loaded from disk at boot. Tokens carry userId, email, name, role, tenantId, and (for partner users) vendorId.

Both the access and refresh tokens are moving to httpOnly cookies (apex_access / apex_refresh), so the token is not readable by JavaScript; a legacy bearer header is still accepted during the cookie migration. CSRF protection uses a double-submit pattern: a non-HTTP-only apex_csrf cookie is read by the SPA and echoed back in the X-CSRF-Token header on mutating requests.

TOTP 2FA is built into the auth layer and recommended for owner / admin roles; self-serve enrollment is rolling out. The secret is encrypted at rest with an AES-256-GCM key loaded from an environment variable that fails-fast at boot.

Auth endpoints

  • POST /api/auth/login
  • POST /api/auth/refresh
  • POST /api/auth/logout
  • POST /api/auth/tenant-signup
  • POST /api/auth/2fa/enroll
  • POST /api/auth/2fa/verify-enroll
  • POST /api/auth/2fa/disable
  • POST /api/auth/verify-totp
  • POST /api/auth/forgot-password
  • POST /api/auth/reset-password

4. Who can do what: six roles

Each role sees and touches only what the job requires. The exact reach:

Role Can do Cannot do
ownerEverything in the tenant including user + role management, tenant settings, billingTouch other tenants
adminAll project / partner / license / report ops; invite users; manage non-owner rolesTouch owner role, delete tenant, modify billing
project_managerManage projects + tasks + visits + threads on projects they have membership inUser mgmt, license mgmt, tenant settings
field_opsRead all projects, update fieldops visits they're assigned to, post notesMutate projects, manage partners, see admin pages
auditorWorkspace-wide read-only, including the full audit log, built for compliance reviewersAny write operation; tenant config
viewerRead-only across all projects + reportsAny write operation

Role enforcement is in middleware (requireRole(['admin','owner']), denyVendor, requireVendor) ahead of every route. Routes that need finer access (e.g. a PM can only edit projects they're a member of) use getProjectAccess to look up membership. Partners (your AV vendors) sign in to a separate, scoped portal via requireVendor rather than holding a tenant role.

5. A complete record of who changed what

Every change is logged: who did what, and when. Each mutating endpoint runs through an auditLog(action, category, severity) middleware that writes a row into the auditlog table after the response is sent, capturing:

id, tenant_id, timestamp, user, action, resource,
details (jsonb), category, severity, ipAddress,
projectId, taskId, created_at

Audit logs are append-only (no UPDATE or DELETE policy on the table from the runtime apex_app role) and tenant-scoped. Today they're queryable via the admin API as JSON; full before/after diffs and CSV/SIEM export are on the roadmap.

Audit query endpoints

  • GET /api/admin-portal/administration/audit

6. Connect your own systems: the REST API

Anything you can do in the app you can do from your own tools. All endpoints are mounted under /api. JSON in, JSON out. Auth is an httpOnly cookie (apex_access) set at login and sent automatically by the browser, never exposed to JavaScript. The access token is an RS256 JWT, refreshed through a separate httpOnly cookie. (A legacy Authorization: Bearer header is still accepted during the cookie migration.)

Tenant admin

  • /api/projects, /api/projects/:id/...
  • /api/fieldops, /api/fieldops/:id/{approve,decline,counter-propose}
  • /api/equipment, /api/equipment/:id/attachments
  • /api/vendors, /api/vendors/:id/credentials
  • /api/licenses, /api/licenses/summary
  • /api/threads/:type/:itemId
  • /api/rooms, /api/room-status
  • /api/locations
  • /api/admin-portal/administration/users, /api/roles
  • /api/admin-portal/administration/audit
  • /api/reports
  • /api/cisco/...

Partner portal

  • /api/vendor/projects
  • /api/vendor/equipment/:id
  • /api/vendor/equipment/:id/service-events
  • /api/vendor/equipment/:id/attachments
  • /api/vendor/locations/:id/documents
  • /api/vendor/fieldops, /api/vendor/fieldops/:id/{mark-on-site,complete,reschedule,cancel,accept-counter}
  • /api/vendor/credentials
  • /api/vendor/licenses
  • /api/vendor/threads/:type/:itemId (same shape as tenant threads, partner-scoped)

Full per-route OpenAPI specification is available on request; the auto-generated reference is being built and will publish here when ready.

7. How the data is structured

The full schema lives in node/drizzle/schema.ts (~50 tables). The relationships that matter most if you're building against it:

  • projects has a JSONB tasks column holding the phased task list with prerequisites + subtasks. Equipment items reference project_id.
  • fieldops (onsite visits) reference project + partner; the row carries the full lifecycle state including outcome, structured reschedule reason, ticket numbers (JSONB array), and equipment items touched (JSONB array of equipment IDs).
  • discussion_threads are polymorphic on (work_item_type, work_item_id) with work_item_type ∈ {project, equipment, room, fieldop, sitevisit}. Thread messages carry author role (client / partner) and an is_internal_only flag.
  • vendor_credentials with nullable tenant_id (NULL = global to the partner, set = scoped to a specific tenant for their compliance requirements). A derived view vendor_credentials_with_status exposes valid / expiring_soon / expired.
  • platform_licenses with managing_vendor_id linking to the partner responsible for renewals. Derived view platform_licenses_with_status exposes active / renewal_upcoming / renewal_imminent / expired.
  • cisco_devices mirrors what Control Hub knows; rows are joined into project equipment when the asset tag / serial matches.

8. How APEX connects to your other platforms

APEX pulls live data from the manufacturer platforms you already run, so room health and inventory stay current without anyone re-keying it. Each connection ("plugin") runs in the Node backend, exposes only a bounded set of actions, and uses per-tenant OAuth credentials stored encrypted in the database. Sync jobs respect rate limits and record an audit entry for every external call.

Plugin Status Capabilities
Cisco Control HubSHIPPEDLive device sync, OAuth token auto-refresh, rate-limited polling, full audit trail emission
Microsoft Graph + Teams RoomsQ3 2026MTR pairing state, calendar bookings, peripheral inventory
Crestron XiO CloudFUTUREDevice monitoring, room scheduling, firmware deployment
Q-SYS ReflectFUTURECore health, design version sync, alerting
Logitech SyncFUTURERally / Tap fleet management, firmware push

Plugins are NOT free-form code execution. Each one exposes a typed capability surface (sync devices, sync rooms, fetch usage telemetry). Tenants opt in per-plugin; OAuth flows happen per-tenant; no credential is ever shared across tenants.

9. How and where APEX runs

The footprint is small and standard. Host it yourself or let us host it. A typical deployment runs three processes: nginx at the edge for TLS termination and static asset serving, Node.js on a private port for the API, and PostgreSQL on its own service for the database. File attachments go to local disk today; S3-compatible bucket storage is on the roadmap.

┌──────────────┐   ┌──────────────┐   ┌──────────────┐
│   Internet   │ → │    nginx     │ → │  Node API    │
│              │   │   :443 TLS   │   │  :3001       │
└──────────────┘   └──────────────┘   └──────┬───────┘
                          │                  │
                          │ static           │ pg
                          ▼                  ▼
                   ┌──────────────┐   ┌──────────────┐
                   │ Vue bundle   │   │  Postgres    │
                   │ + marketing  │   │  :5432       │
                   └──────────────┘   └──────────────┘
                                             │
                                             ▼
                                      ┌──────────────┐
                                      │ S3-compat    │
                                      │ (roadmap)    │
                                      └──────────────┘

Hosting model: APEX runs as a shared multi-tenant deployment (one Postgres database serving all tenants with row-level scoping). On-prem deployment is supported; managed cloud is the default. Each managed tenant gets its own subdomain and tenant_id scope, not its own VM.

10. Security posture

APEX is built against an internal APEX Security Review Baseline (ASRB). The major properties:

  • No hardcoded secrets. Every credential, key, and signing material lives in environment variables or external secret stores. Required env vars are validated at boot and the process fails-fast if any are missing.
  • No SQL injection vectors. All queries use parameterized statements via the pg driver. No template-string SQL construction. Audited as a static analysis step.
  • JWT signing keys fail-fast validated on boot. The RS256 keypair is loaded from disk; missing or malformed keys prevent server startup.
  • Database role separation. The runtime apex_app role has CRUD on tables but no DDL. Schema changes go through a separate apex_ddl role used by migrations.
  • CSRF protection via double-submit cookie pattern on all mutating endpoints. The non-HTTP-only apex_csrf cookie is echoed back in the X-CSRF-Token header.
  • Password handling. Bcrypt with cost 12 for the password hash. Forgotten-password flow uses single-use tokens with short TTL.
  • TLS terminated at nginx with HSTS. TLS 1.2+ only (1.0/1.1 disabled). HTTP requests redirect to HTTPS.
  • Per-tenant data isolation enforced at the database layer by Postgres Row-Level Security (FORCE RLS), not just in application code. A route that forgets to scope a query returns nothing rather than another tenant's rows. The database fails closed.
  • Audit trail append-only, tenant-scoped; CSV/SIEM export on the roadmap (see section 5).

Open questions / improvements on the roadmap: optional WebAuthn / passkey support (instead of TOTP only), per-row encryption at rest for license keys + credential numbers, full SOC 2 audit cycle (currently building toward Type 1 readiness).

Engineering review request?

We'll walk your engineering team through the actual code and answer specific questions about isolation, audit, or any of the security properties above.

Schedule a review

Build your own integration?

The REST API is stable and documented. We're happy to share the OpenAPI spec and walk through a custom plugin or integration with your existing systems.

Request API access