dash-app — design & apprunner lineage

How the Dash platform (repo falcon / dash-app) is put together, what it inherited from the legacy apprunner monorepo (and its appserver4 half-step), and where the two diverge — architecture, job runner, and API surface.

Dash-app design

One multi-tenant, Kafka-driven microservice platform for the Dori AI video-intelligence stack. Everything runs behind a single nginx gateway; each service is an independent FastAPI app sharing one Postgres schema and one common Python library.

Services behind the gateway (nginx.conf → docker-compose)

URL prefixServicePortResponsibility
/config/*config_app9410Auth (login/JWT), platform admin (/infra/*: company, user, mlc, mlc_config, service bundles, credentials), tenant ops (/ops/*: locations, devices, subscribers), app data (/app/*: parts, dynamic kits). Also serves the legacy-compat /admin/* paths.
/jobs/*, /live/*job_app9420Job lifecycle (start/stop/abort, batch, triggers), MLC dispatch + health polling, uploads, MLC status callbacks.
/analytics/*analytics_app9430Inference-event ingest (Kafka consumer) + dashboard queries (events, counts, snippets, images).
/cim/*cim9401Customer Interface Module — outbound comms (email/SMS), event/kit callbacks, plugin upload.
/dmf/*dmf9440Data Management Framework — media capture, image/label review, CVAT sync, dataset export.
/dim/*dim9460Device Interface Module — device notifications over WebSocket, inbound order parsing.
/monitor/*monitor9405MLC health + system events; pass-rate window alerting.
/warehouse/*warehouse_app9470WMS: pallet-scan results, acks, dashboard aggregations.
/sim/*edge_simulator9450Test-only producer of synthetic ML events + a mock MLC.
/icd/, /cod/fe-icd-react80One React build, two experiences: /icd/ = platform admin (DORI_ADMIN), /cod/ = customer operations (per-tenant).

Backbone

Data & state

  • PostgreSQL + TimescaleDB — single canonical dori_db schema; every tenant-owned row carries company_id.
  • SQLAlchemy async ORM — shared models in common/dori_model, migrations via alembic.
  • S3 — per-company credentials in d_auth_aws; storage provisioned atomically with company create.

Messaging & auth

  • Redpanda (Kafka API) — 5 topics wire the inference → incident → alert pipeline.
  • JWT + bcrypt — local password auth; token carries the tenant context, decoded on every request (dori_utils.tenant).
  • Role gates — require_tenant → require_admin → require_platform_admin dependency chain.

Kafka topology

edge / sim ──► dori.inference.events ──┬──► analytics_app ──► dori.incidents.created ──► cim ──► dori.alerts.dispatch
                                       └──► monitor (pass-rate window) ────────────────────────► dori.alerts.dispatch

job_app ──► dori.jobs.lifecycle          external ERP ──► dori.orders.inbound ──► job_app

Legacy lineage — what dash-app merged

dash-app is the third generation. The name is literal: it merges the apprunner Flask monorepo and the appserver4 FastAPI sub-services into one repo with a uniform stack. Note the naming trap inside apprunner: app_server, k9_runner, and app_runner are three different things.

apprunner gen 1 — Flask

  • app_server/ — the dashboard + admin API. Flask-RESTful resource classes in safespace_server.py (~6,200 lines, events/analytics) and safespace_admin.py (~3,500 lines, admin CRUD). Gunicorn, sync workers, port 5000.
  • k9_runner/ — the job runner: k9_job_runner.py handles /live/start_job, drives the MLC (machine-learning container) via doriappsdk, manages MLC capacity and status.
  • cmd_module/ — deployment controller (/v2/deploy).
  • app_runner/ — despite the name, not a job runner: small Flask apps for video/snippet annotation.
  • Storage: raw SQL on MySQL (db_handler.py) + ClickHouse for events (ch_handler.py). No Kafka; comms by direct Gmail/SMS.

appserver4 gen 2 — FastAPI islands

  • First decomposition attempt: cim/, dim/, monitor/ as FastAPI + uvicorn services with a shared common/ (dori_utils, dori_model).
  • Introduced Kafka consumption (dim parses inbound <ORDER> XML via confluent_kafka).
  • cim absorbed apprunner's email/SMS comms (sendgmail, message91).
  • But the big Flask monolith (app_server, k9_runner) stayed behind in apprunner — two stacks in production at once.

Where each legacy piece landed

Legacy componentdash-app homeNote
apprunner/app_server/safespace_admin.pyconfig_appAdmin CRUD → namespaced /config/infra|ops|app/*, with /config/admin/* compat shims.
apprunner/app_server/safespace_server.pyanalytics_appEvents/dashboard queries → /analytics/*; ClickHouse replaced by TimescaleDB.
apprunner/k9_runner (job runner)job_appSame MLC HTTP contract, re-implemented async — see the job runner.
apprunner/cmd_module, app_runner (annotation)dropped / absorbedDeploy flow retired; annotation concerns live in dmf (CVAT sync, labeling).
appserver4/{cim,dim,monitor}cim, dim, monitorCarried over nearly 1:1, now sharing dash-app's common library and gateway.
(no predecessor)dmf, warehouse_app, edge_simulatorNew services with no appserver counterpart.

Similarities with apprunner

Deliberate continuity — old clients, old MLC images, and old data keep working.

AspectShared behavior
Auth modelJWT bearer tokens (HS256 via pyjwt in apprunner, same token style in dori_utils.tenant). Login → token → Authorization header on every call.
Multi-tenancyApplication-level, keyed on the company id — apprunner threaded companyId through every query; dash-app scopes on company_id via the decoded TenantContext.
Domain modelSame entities: company, user, location, camera/device, MLC, subscription, credentials, jobs, events. ID minting (UUID company_id, location_id) matches dar_model's add() behavior exactly.
MLC contractThe three MLC calls are byte-compatible with what k9_runner sent (predict_batch, push_image, pull_image) — existing MLC images work unmodified, including their habit of POSTing status to /add_benchmark_result.
API compat shims/config/admin/*, /config/device, /live/* and the clove_dental/* tenant paths preserve the appserver URL scheme byte-for-byte for old clients.
MLC health semanticsSame rule as apprunner's __mlcHealthCheckStatus: poll {mlc}/api/v1/_health; any non-reader process not active ⇒ MLC NOT ACTIVE.
Reverse-proxy front doorapprunner shipped an app_server_router nginx sidecar; dash-app keeps nginx as the single gateway (now routing nine services instead of one).

Differences from apprunner

Aspectapprunner (legacy)dash-app
ShapeMonorepo of Flask processes; one giant app_server plus side-processes (k9_runner, cmd_module), 6,000-line route files.Nine small FastAPI services, one per domain, each independently built and deployed (doridocker/dash-app-* images).
Framework / concurrencyFlask-RESTful on gunicorn sync workers, 30-minute timeouts to survive long MLC calls.FastAPI + uvicorn, async end-to-end (httpx for MLC calls, async SQLAlchemy sessions, background tasks in service lifespans).
DatabasesMySQL (raw mysql.connector SQL) for config + ClickHouse for events; SQL strings hand-built per handler.One PostgreSQL + TimescaleDB schema for everything, typed SQLAlchemy models in common/dori_model, alembic migrations.
IdentityAWS Cognito flows (confirm_signup, refresh_token, forgot_password) alongside JWT.Cognito removed — local bcrypt hashes + short-lived JWT; the Cognito-only endpoints were dropped.
MessagingNo queue in the Python code — comms fired synchronously (Gmail/SMS) from request handlers.Kafka (Redpanda) backbone: 5 topics decouple ingest, incident creation, alerting, job lifecycle, and inbound orders.
Events pipelineMLC → HTTP into app_server → ClickHouse inserts.Edge → dori.inference.events topic → analytics_app consumer → Timescale hypertables; monitor computes pass-rate windows off the same stream.
Device modelSeparate camera and monitor resources (/admin/camera, /admin/monitor).Unified device row (alembic 003); capture-worker fields are nullable columns.
MLC configurationPer-camera config blobs (/mlc_camera_config, /post_process_code).First-class platform objects: /config/infra/mlc_config (+ company junction) and /config/infra/service_bundle.
Deploymenttools/build_release.sh + hand-rolled k8s manifests per module.docker compose on one EC2; per-service images tagged and rolled independently.
RetiredCognito flows, safety-index/incident/ticket UIs, floormap, VMS, ZoneMinder, runtime-debug endpoints — see the dropped list in the API comparison.

The job runner: k9_runner → job_app

In apprunner, running a job was k9_runner/k9_job_runner.py's business — a separate process from the API server. dash-app folds that role into job_app as mlc_dispatch.py + mlc_health.py, keeping the wire contract and changing the runtime model.

Kept identical (MLC wire contract)

Job typeMLC callSemantics
live / capturePOST {mlc}/api/v2/predict_batch (JSON)start: dataset.authCredentials.path = the device's RTSP source; stop: same requestId, path = "stop".
image_push (+type1/2)POST {mlc}/api/v2/push_image (form)data = job json with init: start|stop, cameraConfig = pp_camera_config.
image_pullPOST {mlc}/api/v2/pull_image (form)Same form shape as push.

Config resolution also mirrors apprunner's Utility.getconfig: the device's MLC supplies url/auth/callback, the service bundle supplies the model, the company's d_auth_aws row supplies S3 creds, and live jobs bump/release mlc.current_capacity exactly like update_current_capacity("INC"/"DEC").

Changed

k9_runnerjob_app
LifecycleSynchronous: the start request blocked on the MLC; the row went RUNNING or FAILED in one shot.Async two-phase: START_PENDING → (MLC callback) → RUNNING | FAILED; stops sit in STOP_PENDING until confirmed; abort_job force-clears a stuck row.
Health checksLazy — ran only when the MLC list was serialized for the dashboard.Background poll in job_app's lifespan; a dead MLC gets its RUNNING and PENDING jobs failed and its capacity reset even if nobody opens the dashboard.
Failure policyFixed: MLC error ⇒ job FAILED.Configurable mlc_dispatch_mode: strict (fail like apprunner), best_effort (record error, keep RUNNING), off (bookkeeping only).
Process modelSeparate runner process beside app_server.Same service as the jobs API — dispatch is a module, not a daemon.

API surface at a glance

Full endpoint-by-endpoint tables live in docs/api-comparison.html; this is the shape of the change.

~140
Endpoints in dash-app
~85
Endpoints in appserver
47
Mapped (both sides)
62
New in dash-app
33
Dropped

Representative renames

appserverdash-appNote
POST /admin/loginPOST /config/common/login+ compat /config/admin/login.
GET/POST/DEL /admin/company/config/infra/companyPlatform-admin namespace.
GET/POST/DEL /admin/camera, /admin/monitor/config/ops/deviceCamera + monitor unified.
POST /live/start_job, /live/stop_jobPOST /jobs/start_job, /jobs/stop_jobOld /live/* paths still routed.
POST /events, /get_last_eventsPOST /analytics/get_events, /analytics/get_last_eventsDedicated analytics service.
POST /event (appserver4 cim)POST /cim/event/Prefix from the shared gateway.

Wholly new surfaces

AreaExamples
Platform admin/config/infra/mlc_config (+ per-company junction), /config/infra/service_bundle, /config/infra/company_config.
Parts & kits/config/app/part, /config/app/dynamic_kit (+ job-side mirrors under /jobs/*).
JobsDELETE /jobs/v2/job/{request_id}, /jobs/getjobslist, batch jobs, uploads, /jobs/v2/add_benchmark_result.
DMF (entire service)Capture, image review/comments, labels, CVAT sync, dataset export, standalone labeling login.
Warehouse (entire service)/warehouse/pallet_result/*, dashboard stats/throughput/suppliers/docks.