Architecture¶
Django apps¶
Comaney is split into three Django apps and a project package.
comaney/ (project package)¶
Settings, root URL configuration, and middleware. Also contains a dynamic markdown renderer that serves optional public pages (imprint, privacy policy, etc.) from environment-variable-supplied markdown content.
feusers/¶
Everything related to users and authentication. This app defines the FeUser model, which is Comaney's custom user type. It does not use django.contrib.auth.User.
Responsibilities: registration, login, logout, email confirmation, password reset, multi-method two-factor auth (TOTP and FIDO2/WebAuthn security keys), API key management, account settings, and account deletion.
Session authentication works by storing feuser_id in the Django session after a successful login. The @feuser_required decorator (in budget/decorators.py) checks this on each request and sets request.feuser.
budget/¶
The core budgeting functionality. Contains all models (Expense, ScheduledExpense, Category, Tag, DashboardCard), all budgeting views, and the business logic for:
- Expense CRUD and bulk actions
- Scheduled expense template management and cron-driven generation
- Category and tag management
- The modular dashboard (YAML card parsing and data computation)
- AI express creation, and the shared AI-calling infrastructure other apps' AI features build on (see AI architecture below)
- Email notifications
- End-of-month allowance handling logic
- The query language parser (translates the search bar mini-language into Django Q objects)
api/¶
A thin REST API layer over the budget models. Authenticated via Bearer token rather than session cookies. Contains serializers for converting model instances to JSON, and an @_require_auth decorator that validates the token and injects the FeUser into the view.
No business logic lives here that isn't already in the budget layer.
URL routing¶
/ → feusers app (login, register, public pages)
/budget/ → budget app (expenses, dashboard, scheduled, categories)
/api/v1/ → api app (Bearer token REST API)
/admin/ → Django admin
/docs/ → Docs site served from docs/build/site/
Middleware¶
Requests pass through the middleware stack in this order:
- SystemMisconfiguredMiddleware: injects a warning banner if the app is misconfigured (e.g., SMTP not set up).
- SecurityMiddleware: Django's standard security headers.
- WhiteNoiseMiddleware: serves compressed static files directly from Gunicorn.
- SessionMiddleware: cookie-based session support.
- LastSeenMiddleware: updates
feuser.last_seenat most once every 5 minutes for authenticated requests. - Standard Django middleware (CSRF, messages, X-Frame-Options, etc.).
Financial periods¶
All budget data is scoped to a financial period. The period is determined by the user's month_start_day and month_start_prev settings. budget/date_utils.py provides financial_month_range() and financial_year_range() to compute the date boundaries for any given month or year.
The dashboard, expense list, and API all accept ?year= and ?month= parameters and use these helpers to scope queries.
Dashboard card system¶
Cards are stored as DashboardCard model instances containing only a raw YAML string and a creation timestamp. All layout and configuration information lives inside the YAML.
When the dashboard loads, the frontend fetches all cards via the session-authenticated card API. For each card, the server parses the YAML, applies the card's query filter against the current period, and computes either a scalar value (cell cards) or grouped chart data (bar/pie charts). The result is returned as JSON.
AI architecture¶
Three features call the AI: express expense creation (budget/express_service.py, view in budget/views/express.py), dashboard card AI assist (budget/dashboard_card_ai.py), and Catalog Partnership tag/category mapping (buddies/services/partnership_ai.py).
All three talk to the AI exclusively through budget/ai_service.py's AIService class -- nowhere else in the codebase constructs an anthropic.Anthropic client, calls .messages.create, or hand-parses an AI JSON response. AIService owns how to talk to the AI safely: own-key/shared-trial-key resolution, the actual API call, JSON-response recovery (one repair retry if the model's reply doesn't parse), trial-budget billing, and classifying a failure into a typed exception (AIAuthenticationError, AIBillingError, AITransientError, AIRefusalError, AIInvalidResponseError, AIBudgetExceededError) -- including disabling the shared trial key and emailing the admin (see AI Trial Key) on an authentication or billing failure, for any of the three features, not just express creation.
AIService deliberately knows nothing about categories, tags, projects, or dashboard cards. Each feature module owns what to ask: assembling its own system prompt text and validating the payload it gets back before it's used. A new AI feature is one more prompt_* method on AIService plus a feature module that builds its prompt and calls it -- it never needs to reimplement JSON recovery, billing, or error handling.
Static files¶
SCSS and JS source files in build/ are compiled to static/dist/ by the build script. WhiteNoise serves them with a content-hash in the URL (via CompressedManifestStaticFilesStorage), enabling aggressive browser caching.
Authentication flows¶
Session auth (web UI): On login, the server looks up the FeUser by email, verifies the submitted password with check_password(), and only then stores the user's database PK in the session as feuser_id. If the account has any second factor (FeUser.has_2fa_enabled), a twofa_pending_id is stored instead and the user is redirected to twofa_verify; the full session (feuser_id) is only established after a factor is validated.
Second-factor architecture (feusers/second_factor_registry.py, feusers/second_factor_service.py): TOTP and FIDO2/WebAuthn are both specializations of an abstract SecondFactorAuth model (feusers/models/second_factor.py); concrete subclasses are TOTPFactor and WebAuthnFactor. Each method registers itself once via register_factor_type(FactorType(...)), which records its model, display name, setup URL, and login-challenge template. feusers/views/twofa.py (login verification, method switching, removal, primary selection, recovery-code regeneration) and feusers/views/account.py (the profile list) drive entirely off this registry via get_all_factors()/method_key_of() and never branch on a method name directly, so adding a further method needs only a new model, a register_factor_type() call, a setup view, and one entry in twofa.py's _LOGIN_VERIFIERS dispatch table.
Exactly one factor (of any type) is is_primary=True per user at a time, enforced by second_factor_service.set_primary(). A single global FeUser.twofa_recovery_hash (not per-method) protects the whole account: it is generated once on a user's first-ever factor, and consuming it via second_factor_service.consume_recovery_code() deletes every factor of every method, since a user who needed the recovery code has just proven they can reach none of them.
Bearer token auth (REST API): The Authorization: Bearer <key> header is read on every API request. The key is looked up against FeUser.api_key in the database.
Registration protection: The registration form uses a client-side proof-of-work challenge to deter bot signups. The browser must compute a hash nonce before the form can be submitted.