platform-api
Responsibility
Section titled “Responsibility”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.
Request lifecycle
Section titled “Request lifecycle”For a protected /v1 request, the service:
- accepts only the route’s documented parameters and body;
- authenticates the bearer access token and loads authoritative session and user state from PostgreSQL;
- resolves a numeric Tenant collection or the owning Tenant UUID of a flat aggregate route through a fixed, typed locator;
- derives current platform, tenant, and organization grants from the database;
- checks the requested action and scope;
- calls a domain service, which still supplies the explicit Tenant UUID to a repository or bounded SQL module;
- persists security-relevant audit information with the operation;
- 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.
Modules
Section titled “Modules”| Module | Routes and responsibility |
|---|---|
auth | Login, refresh, logout, current identity, sessions, and password change. |
authorization | Current database-backed platform, tenant, and organization checks. |
tenancy | Numeric Tenant and flat aggregate ownership resolution. |
tenants, organizations | Customer boundaries and organization trees. |
user-management, memberships | Global identities and scoped grants. |
devices, device-types | Resource inventory, field contracts, current values, and lifecycle. |
telemetry | Bounded Device history with current and historical scope checks. |
flows | Tenant-scoped flow versions, validation, release, and activation. |
audit | Fail-closed request/business audit and bounded queries. |
raw-ingestion | Platform-admin-only raw metadata and detail queries. |
iota-import | Temporary 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.
Authentication and browser boundary
Section titled “Authentication and browser 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.
Persistence and scaling
Section titled “Persistence and scaling”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.
Health and signals
Section titled “Health and signals”/healthzreports process health./readyzchecks validated configuration and database connectivity./metricsexposes HTTP, authentication, audit, latency, and dependency signals through the shared operability server./versionidentifies the immutable release;/testis 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.
Observability additions
Section titled “Observability additions”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.