Skip to content

Sense API — Components (L3)

Inside the Sense API container.

Sense API Components

REST controllers

11 controllers under src/Axion.Sense.Api/Controllers/:

Controller Endpoints
TracksController POST /tracks/{id}/confirm, GET /tracks/{id}, search
FramesController GET /frames/{id}, search with spatial + time filters
RolesController CRUD for custom roles, permission inheritance
UsersController / UserManagementController CRUD, search with JSONB profile filters (profileData[key]=value), org membership
OrganizationsController Org management
HoldsController Hold pins on the map
AuditController Read-only audit log queries
ObjectsController Object state
ReferencesController Read-only catalogs (detector classes, categories)
Planner/* DetourTasks, WmsSources, ExternalGeodata
TestDataController Dev-only (gated by DataSeedEnabled)

gRPC services

5 services under src/Axion.Sense.Api/Services/:

Service RPCs Proto
RoadDataServiceImpl CreateTrack, UploadFrameMetadataBatch, CommitFrameBatch, ConfirmTrack, UploadTrackLogs road_data.proto
DetourTaskServiceImpl GetDetourTasks, UpdateDetourTaskStatus, GetPriorityLayers, GetDriverStats, GetCustomLayers tasks.proto
DriverLocationServiceImpl UpdateDriverPosition driver_location.proto
UserServiceImpl User profile sync user.proto
NetworkScanServiceImpl UploadWifiScans, UploadCellScans network_scan.proto

Cross-cutting components

Auth Middleware

Dual scheme — OIDC JWT bearer (primary) and X-Api-Key (service-to-service). A policy scheme decides which authenticator runs based on the request (presence of header, route prefix). All protected routes require an authenticated principal before the controller is hit.

IPermissionService

A thin wrapper around the OpenFGA C# SDK. Two key responsibilities:

  1. BatchCheck — collapses N permission lookups into a single OpenFGA call. Used everywhere a list endpoint needs per-row authorization.
  2. Model versioning — the FGA model is authored as DSL at src/Axion.Sense.Data/OpenFga/model.fga, serialized to JSON, and pinned by SHA-256 hash in the openfga_model_versions Postgres table. The migration runner detects model changes and writes a new authorization model version.

Operations layer

The IOperation<TParam, TResult> pattern. Every controller action is a thin shell that authn/authz, validates input, and hands off to an operation. The operation owns the transaction, the EF Core calls, the Kafka publish, and the audit emission. This is where the real logic lives.

SenseDbContext

EF Core 9 + Npgsql. Schemas: public, road_data, planner, external. Roughly 50+ entities, 100+ repositories (read/write split). NetTopologySuite types for PostGIS columns (Point, LineString).

IS3Client

Wraps AmazonS3Client. Two main use cases:

  • Presigned URL issuance for UploadFrameMetadataBatch — one URL per frame; default TTL 15 minutes; PUT-only.
  • Presigned download URLs for clients that need to read a frame back (admin tools, exports).

Kafka Producer

KafkaFlow producer registered for the topics the API publishes to:

  • axion.sense.track.metadata
  • axion.sense.audit.events

Per-track ordering is preserved by setting the Kafka key to TrackId.

AuditEventPublisher

Captures per-request audit events (actor, action, resource, org context) at the operation boundary and publishes them to axion.sense.audit.events. The Worker batches these into ClickHouse — see audit batching below.

Output Cache

ASP.NET Core's OutputCache middleware, scoped per tenant, applied to read-heavy idempotent endpoints (reference data, hot list endpoints). Cache key includes the org id from the JWT.

Configuration sections (IOptions)

Oidc, S3, Kafka, ClickHouse, Postgres, Hangfire (Worker), AppOptions, OpenTelemetry, Cors, OutputCache, Kestrel, UserProfileSync, FrameAttributeApi (Worker), CitylensSyncOptions, CustomLayerSyncOptions, CoverageSyncOptions, ApiKey.

Each is bound to a class with a const SectionName. Override any value with environment variables: Section__SubKey=value.

Audit batching

The API never writes to ClickHouse directly. Audit events go through Kafka:

Operation completes
  → AuditEventPublisher.Publish(event)
    → Kafka topic axion.sense.audit.events  (key = RequestId)
      → Worker consumes
        → Buffer in memory
          → Bulk INSERT into ClickHouse audit_log

The buffer is bounded by both message count and time. See Sense Worker components.