Skip to content

Package Map

A navigation map of the Django apps in apps/ — what each is for, and which way dependencies flow. Use it to find where a concern lives and to gauge the blast radius of a change before making it.

This is a role map, not a strict layering. The import graph is a connected graph, not an acyclic stack of layers: foundational apps are imported almost everywhere, and a few apps import very widely. The direction below describes the dominant flow, not an invariant the code never crosses — that is why there is no import-linter contract enforcing it (see the note at the end).

Dependency direction

Dependencies flow downward: entry points depend on domain apps, which depend on the foundation. The foundation depends on nothing above it.

flowchart TD
    EP["Entry points<br/>api · web · admin · dashboard · prelogin · users · oauth · sso"]
    DOM["Domain & runtime<br/>experiments · chatbots · pipelines · custom_actions · service_providers<br/>chat · channels · participants · slack · documents · files"]
    FEAT["Evaluation, analytics & support<br/>evaluations · human_annotations · annotations · assessments<br/>trace · events · cost_tracking · usage_metrics · analysis · moderation · ocs_notifications · banners · help · filters · mcp_integrations"]
    FND["Foundation<br/>teams · utils · generics · web · audit · data_migrations"]

    EP --> DOM
    EP --> FEAT
    DOM --> FEAT
    FEAT --> DOM
    DOM --> FND
    FEAT --> FND
    EP --> FND

Tiers

Foundation

Cross-cutting infrastructure imported almost everywhere. Changes here have the widest blast radius.

App Responsibility
teams Multi-tenancy root — BaseTeamModel, team middleware, auth decorators/mixins. See multi-tenancy guide.
utils Shared base models (BaseModel), custom fields, factories, helpers.
generics CRUD view/table scaffolding (make_crud_urls, generic actions).
web Base views, shared page chrome, health checks.
audit Field-level audit logging.
data_migrations Long-running/managed data migrations.

Domain & runtime

The chatbot model and the machinery that runs conversations.

App Responsibility
experiments The Experiment (a.k.a. Chatbot) model, versioning, sessions. Imported by 27 apps.
chatbots Chatbot-facing UI layer over experiments.
pipelines DAG workflow definition and runtime (LLM/router/custom-action nodes).
assistants Retired OpenAI Assistants integration (#4254). Historical migrations only — no models, admin, views, URLs or runtime. Kept installed so Django owns the migration graph.
custom_actions HTTP API wrappers (OpenAPI schema) callable from pipelines.
service_providers Credentials + LlmService/MessagingService/ModerationService abstractions for LLM, messaging, voice, tracing, moderation.
chat Chat/message models and history.
channels Platform integration + webhook routing (Telegram, WhatsApp, Slack, API, web, email, Connect).
participants Participant identity across channels.
slack Slack platform app (events, install/OAuth).
documents Document/collection management for retrieval.
files File storage and access.

Evaluation, analytics & support

Features layered on top of the domain, plus supporting services.

App Responsibility
evaluations Evaluation configs, runs, and datasets.
human_annotations Reviewer annotation queues and consensus. See ADR-0015.
annotations Tags and comments attached to domain objects (imported widely).
assessments Lean Score value layer. See ADR-0012.
trace Trace/Span observability records for requests and pipeline steps.
events Event triggers and scheduled actions.
cost_tracking LLM usage and cost accounting.
usage_metrics Shared activity-metric read path (sessions, messages, participants) for the dashboard and usage API. Definitions in ADR-0051. Models-free.
analysis Analysis pipelines over conversation data.
moderation Abuse detection: the participant denylist (DeniedParticipant), the abuse rules, the moderate_message task that runs a message through the team's moderation provider, the services that record the result on the message and give strikes (blocking a participant automatically at the strike limit), the enforcement helpers, the message counts by moderation status, and the Moderation section of team settings. Moderation providers live in service_providers. Called from channels, api, experiments and participants.
ocs_notifications In-app notifications.
banners Site banners.
help In-app help / help agents.
filters Reusable list-filter definitions.
mcp_integrations Model Context Protocol integrations.

Entry points

Top of the stack — nothing imports these; they wire the domain to the outside world.

App Responsibility
api DRF REST API (/api/) + OpenAI-compatible endpoints. See API versioning ADRs.
web Server-rendered web UI (also foundational for shared chrome).
admin Staff-only admin area.
dashboard Analytics dashboard.
prelogin Anonymous landing page at /, the shared frame for the logged-out auth pages, and permanent redirects for the marketing URLs that moved to openchatstudio.dimagi.com.
users · oauth · sso Authentication and account management.

Where code goes within an app

Services

An operation that more than one entry point uses (web view, API view, Celery task, channel) lives in a function in the owning app's services module. Each entry point calls that function instead of reimplementing the operation. The function:

  • takes the team explicitly, or derives it from a team-scoped object it is given
  • owns its transaction and any task dispatch

Views and serializers only parse input and format output.

Start with a single services.py. When it grows large or covers several unrelated concerns, turn it into a services/ package with one module per concern, as apps/cost_tracking/services/ does (pricing.py, estimation.py, recorder.py, reporting.py). Do not let services.py collect every operation the app has.

Helpers

Before writing a helper, search for an existing one that does the same thing. To reuse a helper from another module, move it rather than importing it from where it is:

Callers Destination
All in one app That app's utils.py
Span several apps apps/utils/<domain>.py (e.g. time.py, json.py)
Helper needs domain models The owning app's services module — it is not a utility

apps/utils must not import from domain apps.

Before moving or deleting a helper, search the whole repo for its name to find every caller and which apps they are in. Include uses through aliases, re-exports in __init__.py, star imports, and string references (Celery task names, URL configs, templates).

Blast radius

These apps are dependency magnets — a large fraction of the codebase imports them, so changes ripple widely. Treat their public surfaces as contracts and lean on the guard tests when touching them:

App Imported by (# apps) Guarded by
teams ~35 apps/teams/tests/test_multitenancy_guards.py, test_view_auth_guard.py
utils ~29 —
experiments ~27 model versioning tests
service_providers ~23 —
chat, web, channels, annotations, generics ~17–18 —

Why no import-linter contract

An import-linter layering contract was considered but rejected: the tool's current release forces a downgrade of a runtime dependency (typer, pulled in by taskbadger and transformers) to resolve, which is not worth it for a dev-only check. The import graph is also genuinely cyclic between the domain and feature tiers, so a strict layering contract would not hold without extensive ignore_imports exceptions. The existing scripts/check_inline_imports.py guard already covers the one import rule that is mechanically enforced (no unjustified function-level imports).