Skip to content
SENS Platform Docs

platform-api

services/platform-api is the only HTTP trust boundary for product data. It owns local authentication, current authorization decisions, tenant and organization administration, user and membership management, device inventory, telemetry-flow administration, audit queries, the platform-admin raw-data view, and the temporary manual IOTA snapshot migration. Its internal telemetry module serves bounded Device history without adding another deployable service.

For a protected /v1 request, the service:

  1. accepts only the route’s documented parameters and body;
  2. authenticates the bearer access token and loads authoritative session and user state from PostgreSQL;
  3. resolves a numeric Tenant collection or the owning Tenant UUID of a flat aggregate route through a fixed, typed locator;
  4. derives current platform, tenant, and organization grants from the database;
  5. checks the requested action and scope;
  6. calls a domain service, which still supplies the explicit Tenant UUID to a repository or bounded SQL module;
  7. persists security-relevant audit information with the operation;
  8. returns the shared external error or success contract with correlation context.

Protected business operations fail closed when their required audit write cannot be persisted. UI visibility never replaces this lifecycle.

Tenant collections, creates, projections, and singletons use /v1/tenants/{tenantNumber}/…. Existing globally unique aggregate roots use flat member routes such as /v1/devices/{deviceId}; their dependent resources remain below that root. There are no UUID-path aliases and no Tenant header or session-derived API scope. TenantId remains the internal UUID in persistence, events, audits, and authorization.

ModuleRoutes and responsibility
authLogin, refresh, logout, current identity, sessions, and password change.
authorizationCurrent database-backed platform, tenant, and organization checks.
tenancyNumeric Tenant and flat aggregate ownership resolution.
tenants, organizationsCustomer boundaries and organization trees.
user-management, membershipsGlobal identities and scoped grants.
devices, device-typesResource inventory, field contracts, current values, and lifecycle.
telemetryBounded Device history with current and historical scope checks.
flowsTenant-scoped flow versions, validation, release, and activation.
auditFail-closed request/business audit and bounded queries.
raw-ingestionPlatform-admin-only raw metadata and detail queries.
iota-importTemporary bounded IOTA source adapter and atomic create-only migration.

Cross-cutting implementation is kept in small internal modules rather than copied between route families. http owns request context extraction, cursor-page construction, cursor parsing, error mapping, and response cache policy. administration owns change detection and lifecycle audit action selection. The auth module separates session-token creation and login throttling from the main authentication workflow. These are internal boundaries, not additional deployables or public APIs.

The Tenant, Device Type, Device, Flow, global User, Tenant User, and Membership collections use route-specific counted cursor contracts. One SQL statement per request derives both the page and totalCount from the same filtered, authorization-scoped relation and database snapshot. Their ascending orders use createdAt followed by the collection’s unique aggregate ID. Each opaque cursor includes a digest of the caller’s current identity, grants, filters, and page size; invalid, stale, or incompatible cursor state fails validation rather than silently changing the result set. These administration tables accept only 25, 50, or 100 rows and default to 25.

Flow-version pages are uncounted histories ordered by createdAt and flowVersionId in descending order. Their cursors preserve PostgreSQL microsecond precision and bind the continuation to the caller, Tenant, Flow, and page size. The singleton Flow route restores an exact authorized selector value without scanning earlier pages.

Raw, Audit, and Device assignment history instead use uncounted progressive pages of 25 records. Their descending orders combine the event timestamp with a unique UUID. The first page fixes an upper time anchor; the opaque cursor binds that anchor, filters, and scope to each continuation. Refresh starts without a cursor. These timelines never calculate or expose an exact total.

The Membership page projection joins only the addressed Tenant’s User and Organization labels into each list item. The Device page projection likewise joins its Device Type and current Organization labels. This keeps table rendering at one request per cursor page without exposing global identity fields or requiring per-row detail calls.

These response changes are deployed only with a web-console image that understands the counted contracts. Roll back both images to their previous compatible pair, or forward-fix the unmatched image when a paired rollback is unavailable.

The collection navigation inventory records every user-facing page, timeline, hierarchy and bounded detail collection with its console consumer and test evidence.

The authoritative external surface is platform-api.v1.yaml. Route code must not introduce an endpoint without updating that contract. Request and response DTO runtime schemas live beside their TypeScript contracts in @sens-platform/contracts. Mutating API routes validate bodies with those schemas, and the web-console uses the same schemas to validate API responses.

The Device-history route requires telemetry:read, a mandatory time window of at most 30 days, and a page limit of at most 5,000 measurements. It first uses the inventory’s current Device visibility and then filters stored historical organization identifiers against the actor’s currently effective grants. Its stable descending cursor uses observedAt plus measurementId; current replay revision heads remain visible, tombstones do not, and legacy rows without a revision head remain queryable. There is no route-specific V1 rate limiter; authentication and these query bounds are the documented initial load boundary.

Passwords are verified with Argon2id. Short-lived HS256 access JWTs contain identity, not authoritative permissions. Refresh tokens are rotating opaque credentials stored as hashes in PostgreSQL and sent in a host-only HttpOnly cookie. CORS uses an exact origin allowlist with credentials; wildcards are not valid for this flow. Browser preflights explicitly allow the API’s supported GET, HEAD, POST, PUT, PATCH, and DELETE methods plus OPTIONS; the allowlist includes concurrency headers such as If-Match without bypassing route authentication or authorization.

All long-lived correctness state is in PostgreSQL/TimescaleDB. The API keeps no in-memory session or authorization state, so multiple replicas can serve requests safely. Organization grant unions are evaluated with bounded, tenant-scoped repository queries instead of one database round trip per tree node. Password hashing and verification share one concurrency limiter per API replica across login, password change, user creation, and password reset to control aggregate Argon2 memory pressure. Capacity exhaustion returns a non-cacheable 429 response with Retry-After.

  • /healthz reports process health.
  • /readyz checks validated configuration and database connectivity.
  • /metrics exposes HTTP, authentication, audit, latency, and dependency signals through the shared operability server.
  • /version identifies the immutable release; /test is a smoke path.

Readiness failure normally means PostgreSQL is unreachable or startup configuration is invalid. Authentication or audit failures can occur while the process remains healthy, so inspect status-specific metrics, safe structured logs, and the request correlation ID.

IOTA is optional and request-scoped. Its availability never affects health or readiness. Import runs expose bounded run, duration, and entity counters; source credentials, URLs, DevEUIs, and upstream responses are absent from logs and metric labels.

The API attaches the platform release to every structured log, logs bounded HTTP completion fields using route templates, and captures only sanitized top-level exceptions through the optional ErrorReporter adapter. Reporter failure never changes the HTTP result. When product analytics is enabled, GET /v1/auth/me derives domain-separated user and tenant HMAC pseudonyms from SENS_PRODUCT_ANALYTICS_ID_HMAC_KEY; the API remains the sole identity source.