Architecture¶
Baibu Community Edition is a Django monolith with Celery for background work. It renders pages on the server (Django templates styled with Tailwind CSS); there is no single-page JavaScript application.
Services¶
flowchart LR
Browser --> Django["Django (web)"]
Django <--> Postgres[(PostgreSQL)]
Django <--> Valkey[(Valkey)]
Beat["Celery beat"] -- schedules --> Valkey
Valkey -- tasks --> Worker["Celery worker"]
Worker <--> Postgres
Worker <--> Valkey
| Service | Role | Licence |
|---|---|---|
| Django (Python 3.14) | Web application, admin, sign-in | BSD-3-Clause |
| PostgreSQL | All relational data | PostgreSQL Licence |
| Valkey | Cache, Celery broker. Any Redis-protocol server works. | BSD-3-Clause |
| Celery worker | Runs background tasks | BSD-3-Clause |
| Celery beat | Schedules periodic tasks (CELERY_BEAT_SCHEDULE) |
BSD-3-Clause |
Every service is open source and needs no account with a third party. Files live on the local filesystem or on any S3-compatible object store (File storage). Later stages add a language-model endpoint reached through LiteLLM, which the deployment chooses.
Django apps¶
| App | Responsibility |
|---|---|
baibu.core |
Platform settings exposed to templates (platform context), language configuration, the file storage interface, the /health/ endpoint, the worker heartbeat task, and keeping the Site record in line with the platform name and domain. |
baibu.users |
The user model (email sign-in, optional phone sign-in, one name field), profile completion, consent history, account deletion requests, the messaging provider interface for text messages, and the admin. |
baibu.submissions |
Text contributions: the submission form and list, the cleaning task and pluggable cleaners, and the admin used for review. See Submissions and cleaning. |
baibu.chat |
The assistant chat: conversations, messages, runs, prompt versions, model variants, voice messages and the audit trail; the Celery tasks that transcribe voice messages and produce replies. See Assistant chat. |
baibu.notifications |
The in-app inbox, the unread count in the navigation and the notify() service other apps call. See Notifications. |
baibu.staff |
The staff area: review queues for submissions and reported chat replies, and processing errors. No models of its own. See Staff review. |
baibu.localization |
Interface translations edited and published from the site: source string extraction, drafts, publishing with an audit trail, and loading published catalogues into running processes. See Translations. |
baibu.metrics |
Count-only usage rollups (DailyMetric): the nightly task and compute_metrics command that compute them, and the staff usage page with its CSV export. See Usage metrics. |
baibu.theme |
The Tailwind source. The built stylesheet is not committed. |
Sign-in, sign-up, email confirmation, password reset and two-factor
authentication come from django-allauth. The
templates in baibu/templates/allauth/ restyle its pages; they do not change
its behaviour. With PHONE_SIGN_IN_ENABLED, allauth also signs people in
with one-time codes sent to a verified phone number through the messaging
provider (MESSAGING_PROVIDER); see Phone sign-in.
Data model¶
erDiagram
User ||--o{ ConsentRecord : "has history"
User |o--o{ AccountDeletionRequest : "asks for"
User ||--o{ Submission : contributes
ConsentRecord |o--o{ Submission : "in force for"
TranslationSource |o--o{ TranslationCatalogue : "merged into"
TranslationPublication |o--o{ TranslationPublication : "restores"
User {
uuid id
string email "unique, sign-in"
string phone "E.164, optional"
bool phone_verified "unique number when true"
string name
string city
string country
}
ConsentRecord {
uuid id
string scope "e.g. platform, chat"
string tier "none, eval_only, training_eligible"
string source "where it was given"
datetime granted_at
datetime revoked_at "set when replaced"
json metadata "locale, consent_text_version"
}
Submission {
uuid id
string language_code
string status "pending, verified, needs_review, rejected, issue"
string consent_tier "at submission"
string raw_key "private storage"
string clean_key "private storage"
string content_hash "unique"
text excerpt "of the cleaned text"
string cleaner
bool toxicity_detected
int quality_score
}
User ||--o{ Conversation : has
Conversation ||--o{ Message : contains
Conversation ||--o{ Run : "replies by"
Message ||--o{ Run : triggers
Conversation ||--o{ ConversationEvent : "audit trail"
Run ||--o{ ToolInvocation : calls
Conversation ||--o| ConversationTag : "tagged as"
ConversationTag }o--o{ Topic : "about"
Message ||--o{ ChatFlag : "reported in"
Message ||--o| AudioClip : "voice recording"
Conversation {
uuid id
string title
string consent_tier "chat scope, at start"
datetime last_activity_at
}
Message {
uuid id
string role "user, assistant"
text content
string idempotency_key
}
Run {
uuid id
string status "queued, running, completed, failed"
int attempt
string model_name
string prompt_version
json error
}
ChatFlag {
uuid id
string reason "harmful, incorrect, other"
string status "open, confirmed, dismissed"
datetime decided_at
}
AudioClip {
uuid id
string storage_key "private storage"
string content_type
int size_bytes
float duration_seconds
string status "pending, transcribing, transcribed, failed"
int attempt
string provider
json error
}
ConversationTag {
string language
string intent
string tagger
datetime tagged_activity_at
}
Topic {
string slug
string label
bool active
}
ToolInvocation {
uuid id
string tool_name "internet_search"
string provider
string status "completed, failed"
json arguments
int result_count
}
User ||--o{ Notification : receives
Notification {
uuid id
string kind "translated when shown"
json params
string link "internal path"
datetime read_at
}
DailyMetric {
uuid id
date date "day, or cohort week"
string name "e.g. active_users"
string dimension "blank, or e.g. a language"
bigint value
}
AccountDeletionRequest {
uuid id
string status
string source
datetime requested_at
datetime completed_at
}
TranslationSource {
uuid id
string pot_key "private storage"
string content_hash
int entry_count
datetime created_at
}
TranslationCatalogue {
int id
string language_code "unique"
string draft_key "private storage"
datetime updated_at
}
TranslationPublication {
uuid id
string language_code
int version "per language"
string po_key "private storage"
string mo_key "private storage"
int translated_count
int fuzzy_count
datetime published_at
}
Primary keys are UUIDv7, so they sort by creation time and do not reveal counts.
DailyMetric stands alone: it holds aggregate integers per day, unique on
date, name and dimension, with no link to users, messages or submissions.
Phone numbers¶
User.phone holds a number in E.164 format (+12015550123), or null.
Only a verified number signs anyone in, and a verified number belongs to
one user (a conditional unique constraint). Unverified numbers may repeat,
so typing someone else's number blocks nothing; verifying a number clears
it from accounts where it is still unverified.
Consent¶
A consent decision is one ConsentRecord. Records are append-only: a new
decision adds a row and stamps the previous one with revoked_at. The model
refuses edits and single deletes, and the admin is read-only. Code reads
consent through baibu.users.consent:
consent_state(user=..., scope=...)returns the current tier, withallows(tier)for checks. No record, or a revoked latest record, meansnone.record_consent(user=..., tier=..., source=...)records a change.@consent_required(tier)protects a view; users without enough consent are sent to the consent page.
Tiers are ordered: training_eligible includes eval_only, which includes
none. Each feature that uses contributions records under its own scope.
Translations¶
TranslationSource rows are the extracted source strings (newest is
current), TranslationCatalogue holds one draft per language, and
TranslationPublication is the append-only audit trail: one row per publish
or restore, with who, when and counts. The newest publication of a language
is live. Each row's published_by, created_by or updated_by is cleared if
that user is deleted. See Translations.
Account deletion¶
Sending a deletion request deactivates the account at once and signs the
user out. Staff then move the request through submitted → approved →
in_progress → completed (or rejected, or failed and retry). The admin
enforces these transitions. Completing a request clears the requester's
name, email and message. Deleting the user in the admin removes their
consent history; the request stays, without personal details, as a receipt.
New requests are emailed to the active members of the operations-email
group, or to DJANGO_ADMINS if the group is empty. The email contains only
the receipt number, source and time, never personal details.
Requests and background work¶
- Every page goes through
TranslationSyncMiddleware(loads newly published translations; see Translations), thenLocaleMiddleware(language from the URL prefix or the language switcher) andProfileCompletionMiddleware(signed-in users missing a field inPROFILE_REQUIRED_FIELDSare sent to complete their profile; sign-in pages, terms, privacy and the admin for staff are exempt). - Creating a submission queues
baibu.submissions.tasks.clean_submissiononce the database transaction commits. - Sending a chat message queues
baibu.chat.tasks.execute_run; beat runsbaibu.chat.tasks.sweep_runsevery minute to recover stuck replies. - Before each Celery task, the worker loads newly published translations in the same way, so emails sent from tasks use them.
- Sending a voice message queues
baibu.chat.tasks.transcribe_audio, which queuesexecute_runonce the transcript is in; beat runsbaibu.chat.tasks.sweep_transcriptionsevery minute to recover stuck transcriptions. - Every night (
METRICS_COMPUTE_HOUR:METRICS_COMPUTE_MINUTE, platform time), beat runsbaibu.metrics.tasks.compute_daily_metrics, which stores the previous day's usage counts (and any days missed while the worker was down).manage.py compute_metricsrecomputes a range of days. - When
CHAT_TAGGING_ENABLEDis on, beat runsbaibu.chat.tasks.tag_conversationsevery 10 minutes. - Celery beat sends
baibu.core.tasks.heartbeatevery minute. The worker stores the time in the cache, and/health/reports the worker asok,staleorunknown./health/returns HTTP 503 only if the database or cache is down, so a stopped worker does not restart the web container.
URLs¶
The default language has no URL prefix (/accounts/login/); other enabled
languages are prefixed (/fr/accounts/login/). /health/ and /i18n/ are
never prefixed.
| Path | Page |
|---|---|
/ |
Home |
/privacy/, /terms/ |
Placeholders; every deployment replaces them |
/accounts/... |
Sign-in, sign-up, email, password, two-factor (allauth) |
/accounts/phone/change/, /accounts/phone/verify/ |
Add, change and verify a phone number (only with PHONE_SIGN_IN_ENABLED) |
/users/account/ |
Profile |
/users/account/consent/ |
Consent choice and history |
/users/account/delete/ |
Account deletion request |
/contribute/ |
The user's submissions |
/contribute/new/ |
Submit text |
/chat/ |
Conversations and a new chat |
/notifications/ |
The user's notifications |
/chat/replies/<id>/report/ |
Report an assistant reply |
/staff/ |
Staff overview, review queues (review/submissions/, review/chats/) and errors/ |
/chat/consent/ |
Chat consent choice |
/chat/<id>/ |
One conversation |
/translations/ |
Translations overview (translators only) |
/translations/<code>/ |
Translate one language: edit, upload, download, publish |
/translations/<code>/history/ |
Published versions and restore |
/chat/voice/, /chat/<id>/voice/ |
Voice message upload (only with CHAT_STT_PROVIDER set) |
/chat/audio/<id>/ |
Play back one's own recording |
/staff/usage/ |
Usage counts for staff with the view daily metric permission |
/staff/usage/export.csv |
The same rollups as CSV |
/admin/ |
Django admin (path set by DJANGO_ADMIN_URL) |
/health/ |
Health check (JSON) |