Sense API — Components (L3)¶
Inside the Sense API container.
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:
- BatchCheck — collapses N permission lookups into a single OpenFGA call. Used everywhere a list endpoint needs per-row authorization.
- 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 theopenfga_model_versionsPostgres 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.metadataaxion.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.