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).