# Core architecture

The service follows ownership-driven architecture. Django's composition root connects domain capabilities and infrastructure adapters without moving business behavior into generic service layers.

## Owners

| Package | Responsibility |
| --- | --- |
| `domains/business/tours` | Catalog, geography, departures, tour content and tour CMS plugins |
| `domains/business/support` | Customer requests, manager assignment, answers and ticket transitions |
| `domains/business/staff` | Employees, positions and departments |
| `domains/business/careers` | Vacancies and their CMS representation |
| `domains/business/offer` | Marketing offers |
| `domains/system/c_auth` | Application profiles, identity-provider integration and application authorization |
| `domains/system/content` | Managed text, questions and answers |
| `domains/system/file_storage` | File identity, access and lifecycle |
| `domains/system/user_settings` | Available preferences and their persisted values |
| `infrastructure/c_cms` | CMS adapters, reusable plugins, content-viewer transport and registration |
| `infrastructure/api` | Shared DRF fields, serialization and HTTP mechanisms |
| `infrastructure/media_processing` | Persistent image jobs, serializer-derived profiles, retries, readiness and API media contracts |
| `infrastructure/forms`, `infrastructure/admin` | Shared framework adapters |
| `core` | Settings, URL composition, service initialization, scheduler and prerender coordination |

Models, admin, serializers, views, CMS plugins and tests stay with the owner of their behavior. UI on the frontend follows the same ownership principle, without requiring identical directory layers on the backend.

## Dependencies and public contracts

- Domain and infrastructure runtime code consume configuration through `django.conf.settings`
- Runtime code must not import the composition root
- Generic infrastructure must not import business owners
- Cross-domain dependencies must represent an actual responsibility and use the owning module's contract
- `core/composition/cms.py` connects tour serialization to the CMS registry
- Django app labels remain explicit, including `vacancies` for the careers package
- Keep model labels, URL namespaces, CMS plugin identifiers and serializer contracts intentional
- `vl_core`, `vl_helpers` and `vl_snippet` are local source packages with their existing namespaces
- `media` and `tasks` contain runtime data, backed by external host volumes

## Identity

Keycloak owns credentials, registration, password recovery, verification and SSO. Fun Tours uses django-allauth's OpenID Connect provider with authorization code flow and PKCE. Each application has its own client. Profiles and business permissions remain local; OIDC identities are linked by provider and subject through `SocialAccount`.

The provider must supply a verified email. Email alone never automatically links an existing application account. Provider tokens are not exposed to Vue or stored in browser localStorage. The application API uses the HttpOnly Django session created by the OIDC login, with CSRF required for authenticated writes. The HTTP adapter preserves 401 for a missing session and 403 for an authenticated CSRF or permission failure. Vue sends CSRF only to its configured API origin.

Legacy public password, personal-token and JWT refresh endpoints have been removed. CMS preview uses the authenticated session and preserves the prior superuser permission boundary; it accepts no query credential. The local Django staff administration remains a separate operational entry point. Keycloak configuration and secrets come from environment variables; the identity service is deployed independently from GitLab and from the tour application.

The authentication domain owns `IdentitySession`: a binding from a Django session to the verified issuer, client, subject and provider session identifier. The public allauth login signal creates this binding after verified login and limits the application session to `IDENTITY_SESSION_MAX_AGE` (1800 seconds by default). It stores no raw provider token.

`POST /api/auth/logout/` ends only the application session. The CSRF-protected `POST /api/auth/logout/identity/` also returns the configured provider logout URL for browser navigation; Keycloak confirms logout. `POST /api/auth/identity/backchannel/` accepts only signed RS256 logout events validated against the configured issuer, audience and trusted JWKS endpoint. Receipt creation and matching-session deletion share one database transaction, allowing failed delivery to retry without acknowledging a partial revocation. Duplicate receipts are idempotent.

Session bindings use database cascading foreign keys so deletion by a preceding image remains valid during rollback. `initialize_application` retires legacy unbound allauth sessions while preserving bound sessions and local staff sessions. This transition runs after migrations and before rollout through the existing lifecycle contract.

The local shared identity installation and the Levels gateway are documented in `/home/vivazzi/projects/LEVELS/sso/README.md` and `/home/vivazzi/projects/LEVELS/projects/levels/devops/identity/README.md`

The retired personal token is absent from the model, serializers, admin and frontend. Its database column remains nullable for the previous image's rollback window: older code can still select the column and newer code can omit it on insert. The explicit state/database migration separates that temporary schema compatibility from application ownership. Remove the inert column only after incompatible images leave the supported rollback window.

## Clean migration baseline

The modernization deliberately replaces the former project migration history with generated initial migrations for the current model state. This was explicitly authorized for the pre-launch application. Historical import bridges were removed because the new migration graph imports the actual owners.

This baseline is for a fresh database. Do not apply it over an old production database by faking migrations. Preserve the old database, migrate a new one, transfer the required application content, and verify it before switching configuration.

Third-party migration histories remain owned by their packages. Library updates, content transfer and application initialization are separate operations. Reference data previously created by historical data migrations must be considered explicitly when preparing a new environment.

The local modernization used a separate PostgreSQL database, restored the existing design content, and left the original database available. Backups were written before changing the baseline.

## Lifecycle and CI/CD

Services own their own commands, schemas, background work and health checks. The application orchestrator owns release preparation, operation order, dependency readiness and failure propagation. GitLab chooses when, where and for which revision to invoke the orchestrator. Docker Compose provides local development and disposable CI container lifecycle. Kubernetes and Helm provide the release runtime through the orchestrator infrastructure adapter.

See [GitLab setup and release commands](../../devops/ci/README.md)

## CMS site configuration

`DJANGO_SITE_ID` selects the Django Site that owns CMS pages and navigation, defaulting to 1 for a fresh database. Set it explicitly when importing content with another Site primary key. An existing Site record without the matching CMS pages produces missing navigation even when tour APIs remain available. Keep the Site domain consistent with the environment origin.

## Verification

Run service checks and tests inside the configured core container. Test database creation applies the initial migration graph; test setup creates the reference data required by domain behavior. The test runner respects the caller's database-retention option rather than forcing reuse. Test cache aliases use an isolated in-process backend so tests cannot populate or flush the running development site's Redis. Actual Redis readiness is verified separately against the deployed environment.

Architecture tests verify app labels, current migration imports, CMS composition and forbidden imports. Behavioral tests cover domain APIs and CMS plugins. Identity tests cover disabled legacy login, verified email requirements, redirects, cookie flags and the CSRF boundary on OIDC initiation.

See [dependency decisions](DEPENDENCIES.md)
