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.
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.
| URL prefix | Service | Port | Responsibility |
|---|---|---|---|
/config/* | config_app | 9410 | Auth (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_app | 9420 | Job lifecycle (start/stop/abort, batch, triggers), MLC dispatch + health polling, uploads, MLC status callbacks. |
/analytics/* | analytics_app | 9430 | Inference-event ingest (Kafka consumer) + dashboard queries (events, counts, snippets, images). |
/cim/* | cim | 9401 | Customer Interface Module — outbound comms (email/SMS), event/kit callbacks, plugin upload. |
/dmf/* | dmf | 9440 | Data Management Framework — media capture, image/label review, CVAT sync, dataset export. |
/dim/* | dim | 9460 | Device Interface Module — device notifications over WebSocket, inbound order parsing. |
/monitor/* | monitor | 9405 | MLC health + system events; pass-rate window alerting. |
/warehouse/* | warehouse_app | 9470 | WMS: pallet-scan results, acks, dashboard aggregations. |
/sim/* | edge_simulator | 9450 | Test-only producer of synthetic ML events + a mock MLC. |
/icd/, /cod/ | fe-icd-react | 80 | One React build, two experiences: /icd/ = platform admin (DORI_ADMIN), /cod/ = customer operations (per-tenant). |
dori_db schema; every tenant-owned row carries company_id.common/dori_model, migrations via alembic.d_auth_aws; storage provisioned atomically with company create.dori_utils.tenant).require_tenant → require_admin → require_platform_admin dependency chain.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
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.
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.db_handler.py) + ClickHouse for events (ch_handler.py). No Kafka; comms by direct Gmail/SMS.cim/, dim/, monitor/ as FastAPI + uvicorn services with a shared common/ (dori_utils, dori_model).dim parses inbound <ORDER> XML via confluent_kafka).sendgmail, message91).| Legacy component | dash-app home | Note |
|---|---|---|
apprunner/app_server/safespace_admin.py | config_app | Admin CRUD → namespaced /config/infra|ops|app/*, with /config/admin/* compat shims. |
apprunner/app_server/safespace_server.py | analytics_app | Events/dashboard queries → /analytics/*; ClickHouse replaced by TimescaleDB. |
apprunner/k9_runner (job runner) | job_app | Same MLC HTTP contract, re-implemented async — see the job runner. |
apprunner/cmd_module, app_runner (annotation) | dropped / absorbed | Deploy flow retired; annotation concerns live in dmf (CVAT sync, labeling). |
appserver4/{cim,dim,monitor} | cim, dim, monitor | Carried over nearly 1:1, now sharing dash-app's common library and gateway. |
| (no predecessor) | dmf, warehouse_app, edge_simulator | New services with no appserver counterpart. |
Deliberate continuity — old clients, old MLC images, and old data keep working.
| Aspect | Shared behavior |
|---|---|
| Auth model | JWT bearer tokens (HS256 via pyjwt in apprunner, same token style in dori_utils.tenant). Login → token → Authorization header on every call. |
| Multi-tenancy | Application-level, keyed on the company id — apprunner threaded companyId through every query; dash-app scopes on company_id via the decoded TenantContext. |
| Domain model | Same 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 contract | The 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 semantics | Same rule as apprunner's __mlcHealthCheckStatus: poll {mlc}/api/v1/_health; any non-reader process not active ⇒ MLC NOT ACTIVE. |
| Reverse-proxy front door | apprunner shipped an app_server_router nginx sidecar; dash-app keeps nginx as the single gateway (now routing nine services instead of one). |
| Aspect | apprunner (legacy) | dash-app |
|---|---|---|
| Shape | Monorepo 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 / concurrency | Flask-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). |
| Databases | MySQL (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. |
| Identity | AWS Cognito flows (confirm_signup, refresh_token, forgot_password) alongside JWT. | Cognito removed — local bcrypt hashes + short-lived JWT; the Cognito-only endpoints were dropped. |
| Messaging | No 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 pipeline | MLC → 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 model | Separate camera and monitor resources (/admin/camera, /admin/monitor). | Unified device row (alembic 003); capture-worker fields are nullable columns. |
| MLC configuration | Per-camera config blobs (/mlc_camera_config, /post_process_code). | First-class platform objects: /config/infra/mlc_config (+ company junction) and /config/infra/service_bundle. |
| Deployment | tools/build_release.sh + hand-rolled k8s manifests per module. | docker compose on one EC2; per-service images tagged and rolled independently. |
| Retired | Cognito flows, safety-index/incident/ticket UIs, floormap, VMS, ZoneMinder, runtime-debug endpoints — see the dropped list in the API comparison. | |
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.
| Job type | MLC call | Semantics |
|---|---|---|
| live / capture | POST {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_pull | POST {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").
| k9_runner | job_app | |
|---|---|---|
| Lifecycle | Synchronous: 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 checks | Lazy — 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 policy | Fixed: MLC error ⇒ job FAILED. | Configurable mlc_dispatch_mode: strict (fail like apprunner), best_effort (record error, keep RUNNING), off (bookkeeping only). |
| Process model | Separate runner process beside app_server. | Same service as the jobs API — dispatch is a module, not a daemon. |
Full endpoint-by-endpoint tables live in docs/api-comparison.html;
this is the shape of the change.
| appserver | dash-app | Note |
|---|---|---|
POST /admin/login | POST /config/common/login | + compat /config/admin/login. |
GET/POST/DEL /admin/company | /config/infra/company | Platform-admin namespace. |
GET/POST/DEL /admin/camera, /admin/monitor | /config/ops/device | Camera + monitor unified. |
POST /live/start_job, /live/stop_job | POST /jobs/start_job, /jobs/stop_job | Old /live/* paths still routed. |
POST /events, /get_last_events | POST /analytics/get_events, /analytics/get_last_events | Dedicated analytics service. |
POST /event (appserver4 cim) | POST /cim/event/ | Prefix from the shared gateway. |
| Area | Examples |
|---|---|
| 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/*). |
| Jobs | DELETE /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. |