# GovOPlaN Module Architecture

GovOPlaN is structured as a platform kernel plus installable modules. The kernel starts and composes the platform. Modules own product behavior and contribute backend routes, database metadata, permissions, WebUI routes, navigation metadata, capabilities, and events.

The current package name is still `govoplan-core`, but the architecture target is a smaller kernel. Access, tenancy, policy, audit, and admin semantics are platform-module responsibilities.

Access extraction is complete enough that current ownership is described here,
in [`ACCESS_RBAC_MODEL.md`](ACCESS_RBAC_MODEL.md), and in the
`govoplan-access` repository docs.
The event and audit trace contract is tracked in
[`EVENTS_AND_AUDIT.md`](EVENTS_AND_AUDIT.md).
Policy decision, source provenance, and explain-response contracts are tracked
in [`POLICY_CONTRACTS.md`](POLICY_CONTRACTS.md).
The experimental remote WebUI bundle loading design is tracked in
[`REMOTE_WEBUI_BUNDLES.md`](REMOTE_WEBUI_BUNDLES.md).
The cross-product semantic layers, source-authority modes, and candidate
Mandates, Services, Parties, and Decisions boundaries are canonical in the
meta repository's
[`INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md`](../../govoplan/docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).

## Layer Model

| Layer | Purpose | Examples |
| --- | --- | --- |
| Kernel | Bootstraps and composes the platform | app factory, module registry, route aggregation, migration orchestration, capability/event contracts, health metadata |
| Platform modules | Cross-cutting governance capabilities | access, tenancy, policy, audit, admin, ops |
| Service modules | Reusable operational capabilities | files, mail, templates, recipients, notifications |
| Business modules | Public-sector workflows | campaigns, cases, forms, approvals, appointments |
| Connector modules | External system integration | FIT-Connect, XÖV/XTA, DMS/eAkte, ERP, IDM |

This table is the technical composition model. The product portfolio uses a
more detailed institutional layer model, but it does not change dependency
direction: Core provides contracts and composition; modules own semantics;
packages compose modules.

## Institutional Semantic Boundaries

Cross-module references must keep these answers distinct:

- Organizations owns where structures, units, and functions exist.
- Identity owns who a subject is; Access owns accounts, roles, permissions,
  and authorization decisions; IDM owns effective function assignments.
- A Mandates capability will answer why a unit or function is competent for a
  task, jurisdiction, subject, or period. It must not become another RBAC
  system.
- A Services capability will own versioned institutional service definitions;
  Portal presents and starts them.
- A Parties capability will own procedure-local participant roles,
  representation, and delivery authority; it must reference rather than copy
  Identity, Organizations, and Addresses subjects.
- A Decisions capability will own formal institutional outcomes and their
  authority, facts, rules, reasoning, effects, correction, and review.
  Approvals owns review gates, Committee owns deliberation/votes, and Workflow
  Engine owns coordination.

Start each missing concept as a versioned DTO/provider contract used by a
bounded journey. A repository is justified only when the concept gains
independent persistence, lifecycle, security/operations behavior, release
reason, and reuse. Core must not store these domain objects.

## Kernel Responsibilities

The kernel target owns:

- the server entry point and platform configuration
- module discovery, manifest validation, registry validation, route aggregation, and platform metadata APIs
- database engine/session lifecycle and module migration orchestration
- module install-plan validation, signed catalog verification, license entitlement checks, and installer request orchestration
- capability registry, command/event contracts, and lifecycle hooks
- shared WebUI shell contracts, generic WebUI components, and module route/nav rendering
- centralized mapping from serializable icon names to renderable frontend icons
- health, OpenAPI aggregation, and runtime diagnostics

The kernel must not own product semantics such as users, tenants, RBAC decisions, governance policies, audit storage, mail behavior, file behavior, or campaign behavior. Those belong to platform, service, or business modules.

## Current Compatibility Responsibilities

During the staged split, `govoplan-core` still contains compatibility surfaces
for tenancy settings, governance/policy contracts, audit helpers, CSRF/API
helpers, and secret helpers. The extracted access implementation lives in
`govoplan-access`; live ORM table definitions have been split across their
platform owners using module-prefixed table names. The old core route,
admin-service, and access-security re-export modules have been removed.
Callers must use module-owned imports, the public `govoplan_core.auth` request
dependency facade, or kernel capabilities.
The remaining platform compatibility surfaces are temporary until the matching
platform modules are fully self-contained:

- `govoplan-access`
- `govoplan-tenancy`
- `govoplan-policy`
- `govoplan-audit`
- `govoplan-admin`

New code should avoid deepening these compatibility dependencies. Prefer explicit kernel contracts and module capabilities over direct imports.

Core must not import module feature pages or module business logic directly. It should interact with modules through manifests, entry points, metadata, capabilities, events, and route contributions.

The compatibility/deprecation plan for the current split line is:

- keep documented public compatibility imports until the owning module exposes a
  stable replacement and all in-tree callers have migrated
- remove deep implementation re-export modules once callers can use module-owned
  public APIs or kernel capabilities
- preserve migration/table compatibility for already-created development and
  release databases
- document remaining compatibility surfaces here and in the owning module README
- reject new cross-module imports that bypass manifests, capabilities, events,
  or public module APIs

The retention windows and removal checklist for database bridges,
configuration/export schemas, and runtime/API aliases are defined in
`COMPATIBILITY_POLICY.md`.

## Stable Kernel Contracts

The following contracts are the baseline API that modules can rely on:

- `ModuleManifest`
- `ModuleCompatibility`
- named interface contract provider/requirement metadata
- module uninstall guard provider contract
- `MigrationSpec`
- route factory contract
- capability factory contract
- access DTO/protocol contracts in `govoplan_core.core.access`
- resource ACL provider contract
- bounded reference-option search provider contract
- single-tenant and optional batched tenant summary provider contracts
- tenant delete-veto provider contract
- WebUI module contribution contract
- navigation metadata contract
- command/event envelope contract
- policy decision and source provenance contract in `govoplan_core.core.policy`
- external object reference and integration-maturity contract in
  `govoplan_core.core.external_references`
- action/effect preview and execution contract in
  `govoplan_core.core.automation`
- workflow definition contribution and runtime-worker contracts

Changes to these contracts must be versioned or accompanied by compatibility shims.

Tenant list pages prefer `tenant_summary_batch_providers`. A batch provider
receives the unique tenant IDs on the current page and returns count mappings
keyed by tenant ID. Missing tenant keys mean that the provider has no counts for
that tenant; provider errors remain visible. Modules that expose only the
single-tenant contract remain compatible through a per-tenant fallback.
Destructive tenant lifecycle planning deliberately continues to use the
single-tenant path so it invokes every registered provider for the target
tenant, independent of ordinary list-page projections.

This list is the Milestone A kernel-contract freeze baseline. New module work
may extend the kernel by adding explicit contracts, but existing contracts must
remain source-compatible through the 0.1.x split line unless a migration shim
and deprecation note are provided.

### Architecture Metadata

`ModuleManifest.architecture` is the backward-compatible, versioned product-
portfolio declaration for:

- module kind and institutional architecture layer;
- evidence-backed maturity claim (`concept`, `scaffold`, `vertical_slice`,
  `reference_ready`, `supported`, or `lts`);
- owned and explicitly non-owned concepts;
- supported source-authority modes;
- reference packages, tested providers, and known limits;
- migration, upgrade, recovery, security, operations, and documentation
  evidence references.

Core validates the claim and all provider references during registry startup.
`reference_ready`, `supported`, and `lts` claims require a named reference
package and the cumulative evidence set; target-tested providers additionally
require provider evidence. A supported module with migrations must include
migration evidence. Signed release catalogs retain and revalidate the
declaration. The meta manifest check validates repository evidence paths and
provides an opt-in `--require-architecture` rollout gate. Platform metadata,
Docs, and Ops project the same declaration. A module cannot make itself
supported solely by changing its maturity string.

Known access-related capability names are defined in
`govoplan_core.core.access`, including:

- `access.principalResolver`
- `access.directory`
- `access.permissionEvaluator`
- `access.resourceAccess`
- `access.tenantProvisioner`
- `access.administration`
- `access.governanceMaterializer`
- `tenancy.tenantResolver`
- `security.secretProvider`
- `audit.sink`

`govoplan-access` currently registers `auth.principalResolver`,
`auth.permissionEvaluator`, `access.principalResolver`,
`access.permissionEvaluator`, `access.directory`, `access.tenantProvisioner`,
`access.administration`, and `access.governanceMaterializer`.
`govoplan-tenancy` registers `tenancy.tenantResolver`. The minimal
authenticated platform set is now `access`; tenancy is optional and adds tenant
administration plus tenant resolver behavior when installed.
Feature modules should prefer these capabilities over direct reads of
access/tenant ORM models when they need labels, group membership, default
access provisioning, counts, audit actor labels, or tenant metadata.

Other stable runtime capabilities currently include:

- `identity.directory` and `identity.search`
- `organizations.directory`
- `idm.directory`
- `calendar.outbox` and `calendar.scheduling`
- `poll.scheduling`
- `notifications.dispatch`
- `workflow.definitionContributions` and `workflow.runtimeWorker`

Modules contribute reusable process baselines through
`ModuleManifest.workflow_definitions`. Each contribution pins its origin module
and version, stable key, schema and content hash, native graph/BPMN content,
governance ceilings, execution mode, and required capabilities/interfaces.
`govoplan-workflow-engine` reconciles these declarations idempotently. A module
upgrade appends a baseline revision without replacing the active revision or
mutating a local override; the optional `govoplan-workflow` package supplies
the comparison, derivation, and reset UI.

### Named Interface Contracts

Capabilities are runtime objects. Named interface contracts are compatibility
metadata. A module uses them when it depends on a versioned cross-module API
shape but should not hard-code a package or repository release line.

Manifest fields:

- `provides_interfaces`: contracts this module provides, each with `name` and
  `version`
- `requires_interfaces`: contracts this module needs, each with `name`,
  optional `version_min`, optional `version_max_exclusive`, and optional
  `optional: true`

Interface names use dot-separated lower-case identifiers such as
`files.spaces` or `mail.delivery`. A requirement range is interpreted as
`>= version_min` and `< version_max_exclusive`; the exclusive upper bound is
intended for SemVer major-version lines. Missing optional interfaces are
allowed, but an installed provider with an incompatible version blocks
activation because the integration would otherwise bind to an unsafe API.

### Source Authority And Provider Operations

Integration maturity and configured authority are independent. The existing
external-reference maturity ladder describes whether an adapter can discover,
link, search, read, publish, synchronize, migrate, or replace. A binding must
also state whether GovOPlaN is native authoritative, the external system is
authoritative, GovOPlaN keeps a mirror, both sides use governed sync, GovOPlaN
adds only a governance overlay, or the object is link-only.

`ModuleManifest.external_providers` composes existing contracts rather than
replacing them. Each declaration describes owned object/field groups, authority modes,
operations, revisions, freshness, health, limits, idempotency, conflicts,
outcome-unknown handling, evidence, correction/compensation, reconciliation,
outage behavior, classification, purpose, retention, and secret requirements.
Core owns the typed declaration and validation. Connectors and domain modules
own the actual protocol and domain behavior; configuration packages select the
effective mode and block missing/incompatible/unhealthy providers; Docs and Ops
explain the result. Effect-capable declarations fail validation unless their
retry, concurrency, outcome-unknown, correction, reconciliation, evidence,
audit, timeout, outage, classification, purpose, retention, and secret behavior
is explicit.

Declarations are release-time capability claims. Configured state is projected
separately through `ModuleManifest.external_provider_state_providers`. A state
provider receives a bounded tenant context and returns one sanitized observation
per configured binding: stable binding reference, effective authority mode,
active/configured state, health, freshness, conflict, recovery readiness,
observation/last-success time, and scalar metrics. Core validates and aggregates
those observations, isolates provider failures, and never accepts URLs,
credentials, or arbitrary nested metadata as runtime-state fields. Docs removes
binding-level detail from ordinary-user projections; Ops may show the full
sanitized operator projection.

Configuration-package preflight selects the exact requested binding from this
runtime state before evaluating authority, health, freshness, and recovery. A
healthy sibling binding therefore cannot mask an unhealthy required binding.
Providers with multiple configurations must use non-secret, stable references
such as `calendar:sync-source:<id>`.

Current named interfaces, generated from the source manifests by the workspace
contract checks, are:

- `addresses.contact_writer`, `addresses.lookup`, `addresses.recipient_source`
- `calendar.outbox`, `calendar.scheduling`
- `campaigns.access`, `campaigns.delivery_tasks`,
  `campaigns.mail_policy_context`, `campaigns.policy_context`,
  `campaigns.retention`
- `dist_lists.expand`, `dist_lists.source`, `dist_lists.writer`
- `evaluation.feedback`, `evaluation.result_aggregation`, `evaluation.scoring`
- `files.access`, `files.campaign_attachments`
- `mail.campaign_delivery`
- `notifications.dispatch`
- `poll.availability_matrix`, `poll.option_selection`,
  `poll.response_collection`, `poll.signed_participation`,
  `poll.workflow_context`
- `rest.function_publication`
- `scheduling.candidate_slots`, `scheduling.decision_handoff`
- `soap.operation_publication`

Core validates named interface contracts in three places:

- registry activation rejects missing required interfaces and incompatible
  providers
- installer preflight reports the same failures before a module set is
  activated
- signed catalog validation normalizes the metadata and warns when catalog
  entries cannot satisfy each other's ranges

Module-id dependencies still decide startup ordering and mandatory package
presence. Named interfaces decide whether the versions in the active module
set are compatible.

FastAPI route dependencies for authenticated endpoints are imported from the
core `govoplan_core.auth` facade. Routers may import that public API for
`ApiPrincipal`, `get_api_principal`, `has_scope`, `require_scope`, and
`require_any_scope`; they must not import access ORM models or
`govoplan_access.backend.*` implementation internals.

Current live table ownership:

- core scope table: `core_scopes` (used by access/core baseline; managed by
  `govoplan-tenancy` behavior when the tenancy module is installed)
- `govoplan-access`: `access_accounts`, `access_users`, `access_groups`,
  `access_roles`, `access_system_role_assignments`,
  `access_user_group_memberships`, `access_user_role_assignments`,
  `access_group_role_assignments`, `access_api_keys`,
  `access_auth_sessions`
- `govoplan-admin`: `admin_governance_templates`,
  `admin_governance_template_assignments`
- `govoplan-audit`: `audit_log`
- `govoplan-core`: `core_system_settings`

Current admin route ownership follows the same boundary: access contributes
users, groups, roles, system accounts/roles, auth, sessions, and API-key
administration; tenancy contributes tenant registry/settings routes; admin
contributes system settings, overview, and governance-template routes; audit
contributes audit-log routes. Governance template metadata and assignment
routes live in `govoplan-admin`; materializing those templates into
access-owned groups and roles is performed by the
`access.governanceMaterializer` capability.

Current admin WebUI ownership mirrors that route split. `govoplan-access`
contributes the `/admin` route shell and admin nav item. Other platform modules
contribute individual admin sections through the `admin.sections` UI capability.
`govoplan-admin` contributes the overview, system settings, and governance
template sections through that capability. Access-owned tenant/user/group/role
sections remain in the access package until their owning platform modules take
them over.

Cross-module feature contracts live under focused kernel contract modules. For
example, `govoplan_core.core.campaigns` defines
`campaigns.access`, `campaigns.mailPolicyContext`,
`campaigns.policyContext`, `campaigns.deliveryTasks`, and
`campaigns.retention`. The campaign module registers these capabilities so mail
can resolve campaign owner/policy context and delivery tasks, files can validate
campaign file-share access, and core retention can call campaign-owned cleanup
logic without importing campaign ORM models. Keep these contracts small
DTO/protocol surfaces and register concrete behavior from the owning module.

## API Efficiency Contracts

GovOPlaN uses conditional GET and delta collections to reduce reload cost
without giving every module a custom synchronization format.

### Conditional GET

Core applies conditional GET handling centrally for successful JSON `GET`
responses:

- Responses receive a weak `ETag` based on the serialized JSON body.
- Responses are marked `Cache-Control: private, no-cache`.
- Responses vary by `Authorization`, `Cookie`, `X-API-Key`, and
  `Accept-Language`.
- Matching `If-None-Match` requests return `304 Not Modified` without a body.
- Responses with `Set-Cookie`, `Content-Disposition`, `Content-Encoding`, a
  non-JSON content type, a non-200 status, or `Cache-Control: no-store` are not
  converted.

The WebUI `apiFetch` client keeps an in-memory conditional cache for reusable
safe requests. It sends `If-None-Match` after an endpoint has returned an ETag,
returns the cached payload on `304`, and clears the cache generation after
unsafe methods.

This avoids retransmitting unchanged snapshots. It does not identify which row
changed inside a collection.

### Mutation Preconditions

Weak response ETags are cache validators only. Mutable aggregates expose a
separate positive, monotonic revision and an opaque strong ETag generated by
`govoplan_core.core.concurrency.strong_resource_etag`. HTTP mutations send that
strong ETag in `If-Match`; capability and worker calls carry the equivalent
typed `expected_revision`.

Core's compare-and-set primitive advances the revision in the same transaction
as the domain mutation. A missing HTTP precondition is `428 Precondition
Required`, a stale HTTP precondition is `412 Precondition Failed`, and a
domain/reconciliation conflict is `409 Conflict`. Conflict responses contain
bounded resource and revision metadata rather than the complete current
object. Modules may opt into the conservative three-way merge helper, but must
declare protected workflow, delivery, ownership, lock, evidence, signature,
and cryptographic paths that can never be merged automatically.

### Delta Collections

Collection endpoints that can expose row-level changes should use the shared
delta contract instead of inventing module-specific formats.

Core provides `core_change_sequence` as the shared monotonic change sequence.
Modules record append-only entries in the same database transaction as the
resource write. Watermarks are encoded as `seq:<number>` and should be treated
as opaque by clients.

Backend shape:

```json
{
  "items": [],
  "deleted": [],
  "watermark": "opaque-next-watermark",
  "has_more": false,
  "full": false
}
```

Fields:

- `items`: changed or current items since the requested watermark.
- `deleted`: deleted item markers with at least `id`, and optionally
  `resource_type`, `revision`, and `deleted_at`.
- `watermark`: opaque value the client sends as `since` on the next request.
- `has_more`: true when the client should request the next page with the
  returned watermark.
- `full`: true when the response is a full snapshot rather than an incremental
  delta.

Section-level settings endpoints use the same contract but replace `items`
with:

- `item`: the full settings object when `full: true`.
- `sections`: a map of changed settings sections when `full: false`.
- `changed_sections`: ordered section identifiers the client can merge into its
  local settings object.

Recommended query parameters:

- `since`: opaque previous watermark. If omitted or expired, return a full
  snapshot with `full: true`.
- `limit`: maximum number of changed items plus deleted markers.
- `include_deleted`: whether deleted markers should be returned.
- `cursor`: opaque keyset cursor for table pages where offset shifts would make
  row-level merging unsafe.

Modules should record changes with:

- `module_id`: the owning module, for example `files`.
- `collection`: the delta collection, for example `files.assets`.
- `resource_type`: stable row kind, for example `file` or `folder`.
- `resource_id`: stable resource identifier.
- `operation`: `created`, `updated`, or `deleted`.
- `tenant_id`: tenant scope when the change is tenant-owned.
- `payload`: small, non-secret routing metadata that helps determine whether a
  tombstone belongs to the requested view.

Sequence retention is explicit. Cleanup jobs must call
`prune_sequence_entries(...)` rather than deleting `core_change_sequence` rows
directly. Pruning records a retention floor per module, collection, and tenant
scope. Endpoints compare incoming watermarks with that floor; a watermark older
than the floor is not safe for incremental replay, so the endpoint must return a
full snapshot with `full: true`. A first-use `seq:0` watermark remains valid
until such a floor exists, even if unrelated collections have advanced the
global sequence.

### Bounded Reference Selectors

Cross-module selectors use the module-neutral contract in
`govoplan_core.core.references`; consumers must not load an optional module's
complete directory and filter it in memory.

- Providers receive a normalized `ReferenceSearchRequest` with `kind`,
  `tenant_id`, `query`, `selected_values`, `limit`, optional opaque `cursor`,
  and policy context.
- Providers apply visibility and text filtering before materializing rows and
  return `ReferenceSearchPage(options, next_cursor, has_more)`.
- A page contains at most the requested bounded search results. Already-selected
  references are retained in addition to that bound so historical values remain
  readable and removable even when they are inactive, deleted, or outside the
  current search page.
- API consumers expose `next_cursor` and `has_more`. The current searchable
  selector requests the first bounded page for each query; later load-more UI
  can use the same cursor without changing the provider contract.
- `access.reference_options` supplies SQL-backed account, membership, and group
  searches. When it is absent, Core degrades to the legacy Access directory or
  to principal-only/unavailable references without importing Access.
- The shared WebUI `apiReferenceOptionProvider` resolves selected values in
  chunks of at most 200, preventing a large existing selection from turning
  into an unbounded request.

### Cursor/Keyset Pages

Offset pagination remains supported for compatibility and for first page loads,
but it is not safe as the merge anchor for row-level deltas on page 2 and later.
When a delta-capable table can be paged beyond the first page, the endpoint
should expose keyset cursors:

- Core provides `encode_keyset_cursor`, `decode_keyset_cursor`, and
  `keyset_query_fingerprint` in `govoplan_core.core.pagination`.
- Cursors are opaque to clients and contain the endpoint scope, query
  fingerprint, and last-row keyset values.
- The fingerprint must include every query input that changes membership or
  order: scope, tenant, page size, sort column, sort direction, and filters.
- Reusing a cursor with different sort or filter parameters must fail with a
  client error rather than returning a mismatched slice.
- Responses may still include `page`, `page_size`, `pages`, and `total` for
  existing UI components, but `cursor` identifies the current slice and
  `next_cursor` is the safe anchor for the next slice.
- The first visit to an arbitrary page can use offset compatibility. The
  response should include the start cursor for that page so later reloads and
  delta requests use keyset semantics.

Concrete consumers:

- `GET /api/v1/files/delta`: without `since`, returns the current files/folders
  snapshot for the requested owner/campaign scope. With `since=seq:<number>`,
  returns changed files, changed folders, and tombstones for resources that
  left the current view.
- `GET /api/v1/campaigns/delta`: returns accessible campaign rows and campaign
  tombstones when ownership, sharing, or soft deletion removes a campaign from
  the current list.
- `GET /api/v1/campaigns/{campaign_id}/workspace/delta`: returns a workspace
  snapshot first, then changed campaign/version metadata and optional summary
  refreshes when version, job, issue, or delivery-attempt changes invalidate the
  workspace view.
- `GET /api/v1/campaigns/{campaign_id}/jobs/delta`: returns a paginated job
  table snapshot first, then stable row deltas for cursor-backed job pages.
  The job list also supports offset compatibility for first visits to a page and
  returns `cursor`/`next_cursor` for stable reloads. Filtered, created, deleted,
  or stale-watermark requests fall back to a full page snapshot when pagination
  membership can shift.
- `GET /api/v1/admin/users/delta`, `/groups/delta`, `/roles/delta`,
  `/system/roles/delta`, `/system/accounts/delta`, and `/api-keys/delta`:
  return access administration row deltas with tombstones where rows leave the
  visible view.
- `GET /api/v1/admin/system/settings/delta`: returns section deltas for system
  defaults, tenant capability flags, language packages, privacy retention
  policy, maintenance mode, and raw settings.
- `GET /api/v1/admin/tenant/settings/delta`: returns tenant-local setting
  sections and also reports language-section changes when system language
  packages or enabled language codes change.
- `GET /api/v1/admin/configuration-changes/delta`: returns changed
  configuration requests and history records.
- `GET /api/v1/admin/audit` and `/api/v1/admin/audit/delta`: return append-only
  audit events using the same scope, sort, and filter query parameters. The list
  supports offset compatibility plus `cursor`/`next_cursor` keyset paging; the
  delta endpoint can replay changes against a cursor-backed slice.
- `GET /api/v1/mail/settings/delta`: returns mail profile row deltas plus the
  current scoped mail profile policy when profile-policy dependencies changed.
  The WebUI consumes this for system, tenant, user, group, and campaign mail
  settings panels.
- `GET /api/v1/files/connectors/settings/delta`: returns file connector
  profile, credential, connector-space, and scoped connector-policy deltas.
  Credential changes also include referencing profiles because profile rows
  display credential-derived state.

Open retrofit scope:

- Additional module-specific settings pages should expose section deltas as
  their settings APIs stabilize. Remaining likely candidates are future
  booking/resource configuration pages and settings pages introduced by new
  modules.
- Remaining high-volume tables should adopt the cursor/keyset contract before
  enabling arbitrary-page row deltas. Current rollout follow-ups:
  `govoplan-files#22` for large file-space server windows,
  `govoplan-mail#9` for provider-aware mailbox message cursors,
  `govoplan-calendar#7` for event-window deltas,
  `govoplan-tenancy#1` for tenant administration row deltas, and
  `govoplan-admin#2` for governance/module-operation list deltas.

## Module Responsibilities

A module owns one bounded feature area. A module can include both backend and WebUI code in the same repository so feature behavior and frontend integration evolve together.

A module owns:

- backend routers and feature services
- SQLAlchemy models for module-owned tables
- module migrations and migration metadata
- module permissions and role templates
- module-specific schemas, policies, and domain rules
- module package metadata and retirement providers used by install/uninstall preflight
- WebUI pages, feature-specific components, API clients, route contributions, and navigation metadata

A module should not own generic platform UI. If a component is useful outside one module, move it to `@govoplan/core-webui` and parameterize it there before reusing it.

## Backend Contract

Backend modules register through the `govoplan.modules` entry point and expose a `ModuleManifest`.

Example:

```toml
[project.entry-points."govoplan.modules"]
files = "govoplan_files.backend.manifest:get_manifest"
```

The manifest should declare:

- `id`, `name`, `version`
- `compatibility` when the module needs a minimum/maximum core version or a
  newer manifest contract
- required `dependencies` and `optional_dependencies`
- permissions and role templates
- router factory
- migration metadata and script location
- frontend package metadata
- navigation metadata using serializable icon names
- uninstall guard providers for data, migration, worker, or scheduler vetoes

A tenant-level managed `RoleTemplate` may set `default_authenticated=True`
only when every authenticated tenant member must receive that narrow baseline
while the contributing module is installed. Access derives the explicit grant
from the active manifest set during authorization without mutating the request
transaction. It may materialize a non-assignable role row for administration,
but no per-user assignment is required and role edits cannot remove the
baseline.
This is not a shortcut for feature authorization: keep the template narrow and
continue to enforce each domain action's own permission and resource policy.
System-level, unmanaged, wildcard-bearing, or slug-colliding automatic
templates are rejected by registry validation.

Backend nav metadata must use icon-name strings, not frontend components:

```python
NavItem(
    path="/files",
    label="Files",
    icon="folder",
    required_any=("files:file:read",),
    order=40,
)
```

Core validates manifest shape when the platform registry is built. The current
supported manifest contract version is `1`, and frontend asset manifests use
contract version `1`. Registry validation rejects unsupported contract versions,
invalid module ids, duplicate dependency declarations, self-dependencies,
mismatched migration/frontend metadata, invalid frontend package names, and
frontend/nav routes that do not declare usable paths and labels.

Backend route contributions are also validated before they are mounted. Startup
routers and live module activation fail fast if two routers register the same
HTTP method and path. That keeps OpenAPI output and FastAPI route order from
silently masking a module collision.

Tenant deletion and cleanup use the registry-owned delete-veto contract. A
module that owns tenant-bound data may declare `delete_veto_providers` on its
manifest for resource types such as `tenant` or `group`. Providers receive
`(session, tenant_id, resource_id)` and should return `DeleteVetoIssue`, an
iterable of `DeleteVetoIssue`, or `None`; older exception-based providers are
still treated as blocking vetoes. Core attributes each issue to the provider
module and adds resource context before the tenancy module exposes the issues
through the deletion plan. `blocker` issues prevent destructive or retire
operations, `warning` issues explain retained data, and `info` issues document
non-blocking lifecycle facts.

## Database And Migrations

Core owns the database/session lifecycle. Modules access the database through core session dependencies and register their models/migrations through their manifest.

Rules:

- Do not create independent database engines in modules.
- Use core session dependencies, base metadata, and migration orchestration.
- Keep module-owned tables and migrations in the module repository.
- Keep cross-module foreign-key assumptions explicit and conservative.
- Register module metadata in `MigrationSpec` so core can discover it.
- Optional module migrations may create multiple Alembic heads. Verification
  should compare database heads to Alembic's resolved `heads` target instead
  of assuming one linear revision when multiple modules are enabled. Owner
  heads can be dependency parents and therefore may not all appear in
  `alembic_version` after a full-graph upgrade.
- Treat migrations as release artifacts. Unreleased migrations may be squashed
  or rewritten before a stable release; released revision IDs are immutable
  once an installation may have recorded them. Each stable release records its
  public migration heads in `docs/migration-release-baselines.json`.
- GovOPlaN keeps two Alembic tracks. The default `release` track loads
  `versions` directories with reviewed release baselines and release-to-release
  step-up migrations. The explicit `dev` track loads `dev_versions`
  directories with the detailed development chain. Do not load both tracks for
  one migration run, and do not switch a database between tracks unless it is a
  disposable development database.

### Shared State And Runtime Ordering

Multi-host application roles use the `shared` state profile. In that profile,
PostgreSQL, Redis, a stable installation identifier, and S3-compatible object
storage are mandatory. Module durable artifacts must use Core's object-storage
contract and module-owned opaque key namespaces; node-local paths are limited
to temporary materialization. Same-host replicas may use the `host-shared`
profile and one shared volume.

Only the migration command mutates schema. PostgreSQL migration runs acquire a
deployment-wide advisory lock before module pre-tasks, Alembic, and post-tasks.
API, worker, and scheduler roles wait for exact configured migration heads and
fail closed instead of applying migrations during startup.

Runtime roles register identity, software/module composition, queues, heartbeat,
and drain state in PostgreSQL. Singleton work must use a distributed lease and
validate its monotonically increasing fencing token at the consequential
commit. See `STATE_AND_RECOVERY_CONTRACT.md` for the complete contract.

### Recovery Evidence

Operations spanning transactions, object storage, queues, or external systems
must choose an explicit Core recovery mode: atomic, compensation,
snapshot-restore, forward-recovery, or irreversible. Plans require verification
steps and mode-specific recovery material. Use idempotency keys, append-only
evidence checkpoints, and a runtime fence where work may race across nodes.

The recovery ledger is a shared primitive, not automatic coverage. A module may
claim its guarantees only after its operation records preconditions before side
effects, transitions partial/unknown outcomes honestly, and records verified
completion or recovery. Plaintext secrets must never enter recovery metadata or
evidence.

## Install, Uninstall, And Catalogs

Core owns the install plan, signed catalog validation, license entitlement
check, maintenance-mode guard, replay state, and installer request queue.
Package mutation is performed by `govoplan-module-installer` outside the
FastAPI request process.

Official catalogs can be served as static JSON from `addideas-govoplan-website`, but core
does not trust the website by location alone. A catalog must pass the configured
signature, channel, freshness, and replay rules before a catalog entry can be
planned. Catalog entries may declare `license_features`; core checks those
against the configured offline license before adding the entry to the install
plan. Catalog entries may also declare `migration_safety` as `automatic`,
`requires_review`, `forward_only`, or `destructive`; forward-only and
destructive entries require explicit operator acknowledgement in the install
plan before installer preflight allows activation. Forward-only and destructive
catalog entries must also declare a tested recovery path. Catalog update entries
can define direct-update windows with `current_version_min` and
`current_version_max_exclusive`, mark intermediate `bridge_release` targets, and
explicitly opt into reviewed downgrade or same-version package-refresh plans.
Module migration order can be declared with `migration_after` and
`migration_before` in manifests or release catalogs; installer preflight turns
that metadata, module dependencies, and named interface relationships into an
ordered migration plan.

Modules that need live-data work outside Alembic schema revisions may declare
`migration_tasks` on `MigrationSpec`. This is deliberately narrower than a
general lifecycle hook system. Each task has a stable `task_id`, one of four
phases (`pre_migration_check`, `pre_migration_prepare`,
`post_migration_backfill`, `post_migration_verify`), a short operator-facing
summary, a task version, safety metadata, and an idempotent executor. Installer
preflight blocks non-idempotent tasks, forward-only/destructive tasks without
operator acknowledgement, and installed manifest tasks that have no executor.
Catalog task metadata is surfaced before activation as pending because the
executor can only be verified after the package is installed.

Modules should provide:

- pinned backend and WebUI package refs for official catalog entries
- module dependency metadata for catalog target-state planning
- migration-safety metadata for catalog update planning
- migration task metadata when live-data checks, preparation, backfills, or
  verification must run around Alembic
- compatibility metadata in the module manifest
- named interface contracts in the manifest and catalog entry when the module
  provides or consumes cross-module APIs
- lifecycle hooks when a runtime enable/disable action needs module-specific
  work
- uninstall guards for persistent data, active workers, schedulers, or external
  bindings
- retirement providers when destructive uninstall can safely drop or retire
  module-owned data

Uninstall remains non-destructive unless the operator explicitly requests
`destroy_data` and the module provides a retirement provider that supports it.

## WebUI Contract

A WebUI module exports a `PlatformWebModule` from its package. The object
contributes local/fallback metadata and route render functions. The package
must ship `src/module.ts` with the default contribution export: Core's Vite
host imports that descriptor directly after the backend reports the module as
enabled. This keeps package-root re-exports from pulling page implementations
into the initial shell.

Example:

```ts
const FilesPage = lazy(() => import("./features/files/FilesPage"));

export const filesModule: PlatformWebModule = {
  id: "files",
  label: "Files",
  version: "1.0.0",
  dependencies: ["access"],
  navItems: [
    { to: "/files", label: "Files", iconName: "folder", anyOf: ["files:file:read"], order: 40 }
  ],
  routes: [
    { path: "/files", anyOf: ["files:file:read"], order: 40, render: ({ settings, auth }) => createElement(FilesPage, { settings, auth }) }
  ]
};
```

Route pages and substantial panels must use stable lazy imports. Core supplies
the shared loading and retryable error state around route rendering. The
initial static import closure and largest asynchronous chunk are enforced by
the budgets documented in [WEBUI_BUNDLE_BUDGETS.md](WEBUI_BUNDLE_BUDGETS.md).

WebUI modules receive only the core route context:

- `settings`
- `auth`

A module should call its own API client and module-owned backend routes. Shared API helpers should live in core only when they are truly platform-level concerns.

Modules can also contribute named UI capabilities for explicit extension
points. Capability values must be narrow, typed contracts, not imports from a
sibling feature package. For admin pages, modules contribute:

```ts
const adminSections: AdminSectionsUiCapability = {
  sections: [
    {
      id: "system-settings",
      label: "General",
      group: "SYSTEM",
      order: 10,
      allOf: ["system:settings:read"],
      render: ({ settings, auth }) => createElement(SystemSettingsPanel, { settings, auth })
    }
  ]
};
```

The access admin route shell collects all installed `admin.sections`
capabilities with `usePlatformUiCapabilities("admin.sections")`, filters them
by `anyOf`/`allOf`, and renders them without importing the contributing module's
components directly.

The configurable dashboard follows the same pattern. Core contributes only a
minimal `/dashboard` fallback when no `dashboard` WebUI module is active. The
`govoplan-dashboard` module owns the real `/dashboard` route and collects
widgets exposed through the `dashboard.widgets` capability:

```ts
const dashboardWidgets: DashboardWidgetsUiCapability = {
  widgets: [
    {
      id: "ops.health",
      title: "Operations health",
      moduleId: "ops",
      defaultSize: "wide",
      anyOf: ["ops:operations:read"],
      render: ({ settings, refreshKey }) => createElement(OpsHealthWidget, { settings, refreshKey })
    }
  ]
};
```

Dashboard widgets are module contributions, not cross-module imports. A widget
may render components from its own module and core components only. The
dashboard module is responsible for layout, visibility, refresh context, and
future server-side layout persistence.

## Icon Rules

Icons are resolved centrally by core.

Modules must provide icon names with `iconName` in frontend nav contributions and `icon` in backend manifest metadata. Modules must not import Lucide icons for navigation metadata.

Current core icon names include:

- `activity`
- `admin`
- `campaign`
- `dashboard`
- `file`
- `files`
- `folder`
- `form`
- `mail`
- `reports`
- `users`

If a module needs a new navigation icon, add the name-to-component mapping in core first, then use the name in backend and frontend metadata.

The access module uses the `admin` icon for its `/admin` route. Core only
resolves that icon name; it does not hard-code the admin route in the rail.

## Shared Component Rules

Use this rule of thumb:

- If it is platform-level or likely reusable by more than one module, define it in `@govoplan/core-webui` with parameters.
- If it is feature-specific and only meaningful inside one bounded module, keep it in that module.
- Modules must not import components from another feature module.
- If one module needs a component currently owned by another module, promote a generic version into core and replace the old usage with the core component.

Examples:

- `ExplorerTree` is core because files, mailboxes, and future modules can all render hierarchical navigation.
- `MessageDisplayPanel` is core because mail, campaign sending, and later audit/review surfaces can display message-like content.
- `AdminPageLayout`, `AdminIconButton`, and `AdminSelectionList` are core
  because access, admin, tenancy, policy, and audit panels share the same admin
  shell language.
- `MailProfileManagement` remains in the mail module because it is specific to mail transport policies and profiles.

## Cross-Module Integration

A module can declare required module dependencies, optional module
dependencies, required capabilities, and optional capabilities. Required module
dependencies are reserved for unavoidable startup ownership, such as a module
that cannot import or mount without another module package. Most runtime
relationships should be expressed as capabilities instead.

Auth/principal access is a capability contract, not a reason to hard-depend on
the `govoplan-access` repository. Current routers import `govoplan_core.auth`;
that facade delegates to access today and is the migration point for a future
provider-neutral auth kernel. Feature manifests should require
`auth.principalResolver` and `auth.permissionEvaluator`, while access remains
the default installed provider.

Tenancy is optional. Existing scoped data still uses `tenant_id` as a scope
identifier, backed by the core-owned `core_scopes` table. Access and
organizations must not import the tenancy package or declare a hard dependency
on it. Tenancy-specific administration and tenant resolver behavior live behind
the tenancy module and its capabilities.

Optional behavior should be enabled by module presence, capabilities, and
permissions, not by importing another module's WebUI internals.

Rules:

- Use core module metadata to check whether another module is installed.
- Use backend APIs/events/service contracts for runtime cooperation.
- If a sibling module needs owner-specific data, expose a narrow DTO/protocol
  capability from the owning module instead of importing its ORM models.
- Keep UI integration declarative where possible: nav items, route contributions, context actions, and explicit extension points.
- Avoid direct imports from one feature module into another feature module unless the imported package is a published API contract designed for that purpose. UI components should be promoted to core instead.

### Dependency Boundary Enforcement

The meta repository includes `tools/checks/check_dependency_boundaries.py`. It enforces the current baseline:

- kernel/core source may not add new direct imports of files/mail/campaign internals
- access source may not import files/mail/campaign internals
- feature modules may not import access implementation internals
- feature modules may not add new direct imports of sibling feature modules
- feature WebUI packages may not depend on or import sibling feature WebUI packages
- core WebUI may list module packages as host dependencies, but core WebUI source
  may not import feature WebUI internals directly; module loading stays
  declarative through the module contribution contract
- FastAPI routers import the core `govoplan_core.auth` dependency facade
- the transitional allowlist is expected to stay empty

Any future exception is extraction debt and must be temporary, documented in the
script with a reason, and removed when a capability/API/event contract replaces
it.

## Boundary Decision Register

These durable decisions close older exploratory core issues. Implementation
work should live in the owning module repositories once a boundary is clear.

Decision principles:

- Prefer connector-first when an external specialist system is likely to remain
  the system of record.
- Create a native module only when GovOPlaN must own domain semantics,
  permissions, audit, retention, configuration-package fragments, or workflow
  state.
- Keep optional behavior behind core-mediated capabilities, events, DTOs, route
  contributions, and UI contribution points.
- Do not create repositories just because a possible product area exists.

### Templates And Reporting

Tracking: `govoplan-core#190`, `govoplan-templates#1`,
`govoplan-reporting#1`.

Decision: templates and reporting are separate modules.

`govoplan-templates` owns:

- reusable renderable templates for letters, permits, emails, forms, reports,
  certificates, and notices
- template versioning, merge-field declarations, rendering profiles, output
  format choices, and preview contracts
- template package fragments that other modules can reference

`govoplan-reporting` owns:

- report definitions, data selection, dashboards, BI views, scheduled outputs,
  and export targets
- report permissions, report execution history, generated report evidence, and
  report-specific retention inputs
- downstream export handoff to files, dataflow, connectors, or publication
  surfaces

Boundary:

- Templates do not own data selection, aggregation, scheduling, or BI semantics.
- Reporting may call template rendering through a capability when a formatted
  report output is needed.
- Campaign, mail, files, workflow, and cases use templates/reporting through
  capabilities and DTOs, never direct imports.

### Sources, RSS, Datasources, And Dataflow

Tracking: `govoplan-core#192`, `govoplan-core#197`,
`govoplan-core#198`, `govoplan-connectors#3`,
`govoplan-connectors#4`.

Decision: do not create `govoplan-datasources` or `govoplan-dataflow` until a
first executable use case proves that connector/reporting/workflow ownership is
too narrow.

First slice:

- `govoplan-connectors` owns RSS/Atom consume/emit connector profiles,
  connector health, external references, source lifecycle metadata, and
  source/publish capability boundaries.
- `govoplan-files` owns file-backed governed locations and uploaded/stored file
  evidence.
- `govoplan-reporting` owns report/data views and scheduled outputs.
- `govoplan-workflow-engine` owns process state, approvals, scheduling of process
  steps, and human review.

Future `govoplan-datasources` is justified when GovOPlaN needs a broad source
catalogue for SQL databases, CSV/Excel files, APIs, RSS feeds, uploaded files,
and governed file locations with shared ownership, credentials, schema
discovery, refresh cadence, provenance, and permission boundaries.

Future `govoplan-dataflow` is justified when GovOPlaN needs first-class
pipelines for ingestion, transformation, validation, scheduling, lineage,
publication, audit events, reruns, and source-to-source workflows.

Monthly extraction/transformation work should start as a configuration package
and module collaboration across connectors, files, workflow, reporting, and
possibly templates. Create datasources/dataflow repositories only after that
package exposes repeated contracts that do not belong to an existing module.

### Calendar, Scheduling, And Appointments

Tracking: `govoplan-core#193`, `govoplan-calendar#1`,
`govoplan-calendar#2`, `govoplan-scheduling#1`,
`govoplan-appointments#1`.

Decision: use three separate modules.

`govoplan-calendar` owns:

- calendar collections, events, recurrence, availability/free-busy, resources,
  iCalendar import/export, CalDAV/Open-Xchange-style calendar adapters, and
  calendar WebUI surfaces

`govoplan-scheduling` owns:

- Terminfindung, meeting-time polls, participant availability collection,
  candidate-slot ranking, conflict explanations, reminders, and the handoff
  from a selected slot to calendar/appointment/workflow modules

`govoplan-appointments` owns:

- Terminbuchung/fixed-slot appointment booking, appointment types, booking
  rules, capacity, cancellation/no-show state, public/internal booking flows,
  and appointment evidence

Boundary:

- Calendar provides time primitives and external calendar integration.
- Scheduling chooses a suitable time.
- Appointments owns booked appointment workflows and public/internal booking
  semantics.
- Mail and notifications deliver invitations/reminders through capabilities.

### Forms And Workflow Handoff

Tracking: `govoplan-core#194`, `govoplan-forms#1`.

Decision: forms are a reusable module boundary, with runtime behavior separated
from workflow semantics.

`govoplan-forms` owns:

- form definitions, schemas, validation rules, field visibility rules,
  localization, versioning, admin editing, and reusable form package fragments

`govoplan-forms-runtime` owns:

- public/internal submissions, drafts, submitted values, validation evidence,
  attachment references, submission receipts, and handoff events

Boundary:

- Forms do not own cases, workflow transitions, tasks, or portal identity.
- Workflow/cases consume form submission events and evidence references.
- Files owns uploaded file storage and file permissions.
- Reporting/dataflow may consume submitted data through governed DTOs or
  source lifecycle contracts.

Implemented contract:

- Core owns the provider-neutral `FormDefinition`/`FormFieldDefinition` DTOs.
- Forms persists immutable exact definitions and provides `forms.definitions`.
- Forms Runtime resolves that capability, persists revisioned instances and
  events, validates draft/final values, and provides
  `forms_runtime.service_launcher`.
- Portal delegates exact `<form-id>/<revision>` bindings and never writes either
  owner's tables.

### OpenDesk Integration Profile

Tracking: `govoplan-core#195`, `govoplan-connectors#5`,
`govoplan-idm#1`, `govoplan-mail#5`, `govoplan-calendar#2`,
`govoplan-connectors#1`.

Decision: OpenDesk is an integration profile, not a monolithic module.

Ownership:

- identity: `govoplan-idm` plus `govoplan-access`
- mail/groupware: `govoplan-mail`
- calendar: `govoplan-calendar`
- files/documents: `govoplan-files` and later `govoplan-dms`
- projects/tasks: `govoplan-connectors` OpenProject connector first
- inventory/health/profile diagnostics: `govoplan-connectors`

The OpenDesk profile should describe required connector profiles, shared
identity assumptions, health checks, and optional module combinations. It must
not create direct module-to-module imports.

### Project Management And OpenProject

Tracking: `govoplan-core#196`, `govoplan-connectors#1`.

Decision: connector-first. Do not create a native `govoplan-projects` module
yet.

OpenProject integration belongs in `govoplan-connectors` first:

- profile test
- project and work-package lookup
- external-reference storage
- selected publish/synchronize capabilities for tasks, workflow, or cases

A native project module is justified only if GovOPlaN needs to own project
semantics beyond cases, tasks, workflow, appointments, documents, and reporting,
for example portfolios, project budgets, project-level resource planning, or
governed project records that cannot remain in OpenProject.

### Public-Sector Integration Landscape

Tracking: `govoplan-core#186`, `govoplan-core#215`,
`govoplan-connectors#2`, `govoplan-connectors#3`.

Decision: core owns strategy and routing; connectors owns executable
integration catalogue entries and operator inventory.

Core documents:

- product-level integration strategy
- native-vs-connector decisions
- owning module routing
- roadmap sequencing

`govoplan-connectors` owns:

- connector entry schema
- external system catalogue
- connector profiles and diagnostics
- source consume/publish lifecycle
- external references

When a target needs executable behavior, create the implementation issue in the
owning module repository and keep only cross-module decisions in core.

## Module Lifecycle

Core exposes the installed module catalog through the admin API and WebUI. The
current lifecycle model separates four states:

- installed: the Python/WebUI package is available to the process
- active: the module is present in the running platform registry
- desired: the module should be active on the next server startup
- planned package change: an operator-reviewed package install/uninstall item
  saved in system settings but not executed by the running server

The admin module manager can change the desired enabled set and apply it to the
running server. It always keeps `tenancy`, `access`, and `admin` enabled when
saving through the admin UI, and it adds required module dependencies before
saving the desired state. On startup, core always keeps the minimum
authenticated platform set `tenancy`/`access` enabled and keeps `admin` enabled
when the operator configuration includes it. Unknown saved module ids are
ignored when the matching package is no longer installed. The core app factory,
devserver, development bootstrap, background worker registry, and migration
metadata plan all read the saved desired state from `system_settings` before
building their module registry.

Hot enable/disable is a core design principle for every module:

- Core keeps one mutable active `PlatformRegistry` object and swaps its manifest
  set through the module lifecycle manager. Modules must read module presence,
  optional integrations, permissions, role templates, capabilities, navigation,
  and frontend contributions from that registry instead of caching sibling
  module availability.
- Core validates install state and dependency closure before activation.
- Core applies configured module migrations before activation. Deactivation
  never drops tables or data.
- Core mounts module routers once and guards them by active module state. A
  deactivated module's routes remain mounted internally but return a disabled
  module response until the module is active again.
- Module route factories must be side-effect-light and idempotent. They may
  configure module runtime references, but they must not start workers,
  schedulers, or irreversible external subscriptions. Use lifecycle hooks for
  those resources.
- Modules that own persistent data, background jobs, schedulers, external
  subscriptions, or irreversible migration state must expose uninstall guard
  providers through their manifest. Guards return `blocker`, `warning`, or
  `info` results and may inspect live state through the core-owned DB session.
  Default package uninstall is non-destructive, so ordinary persistent data
  should warn that data will remain dormant. Guards should block only when
  removing the package would corrupt other active modules, workers, external
  subscriptions, or deployment state. A guard failure is treated as a blocker.
- Modules that can destroy their own data must also expose a migration
  retirement provider. Destructive retirement is opt-in per uninstall plan row
  through `destroy_data: true`; the installer then snapshots the database,
  invokes the module-owned retirement executor while the package is still
  installed, and only then removes Python/WebUI packages. Without that flag,
  the same provider is used for preflight reporting only and module tables/data
  remain dormant.
- Core refreshes the active registry before frontend metadata is returned from
  `/api/v1/platform/modules`; the WebUI shell refetches this metadata after
  module changes so navigation, routes, and UI capabilities update without a
  page reload.
- Modules can provide `on_activate` and `on_deactivate` hooks for worker,
  scheduler, cache, or external subscription lifecycle. These hooks must be
  idempotent and must not mutate another module directly.
- Package install/uninstall is performed by the trusted operator installer, not
  directly inside FastAPI request handlers. The admin UI can save install plans,
  show preflight blockers, and activate/deactivate installed packages.

The package install-plan API records operator intent only:

- `GET /api/v1/admin/system/modules/install-plan` reads the saved plan,
  renders shell commands, and returns installer preflight status.
- `GET /api/v1/admin/system/modules/install-runs` returns recent installer run
  summaries and the current installer lock status.
- `GET /api/v1/admin/system/modules/install-runs/{run_id}` returns the raw run
  record for diagnosis.
- `GET /api/v1/admin/system/modules/install-requests` returns daemon handoff
  requests queued from the admin UI or CLI plus the current daemon heartbeat.
- `POST /api/v1/admin/system/modules/install-requests` queues a supervised
  installer request. It requires maintenance mode and maintenance access. The
  FastAPI request writes only the request record; it does not run package
  commands.
- `POST /api/v1/admin/system/modules/install-requests/{request_id}/cancel`
  cancels a queued request. Running requests are not interrupted by the API;
  they remain owned by the installer daemon.
- `POST /api/v1/admin/system/modules/install-requests/{request_id}/retry`
  queues a new request using the options from a failed or cancelled request.
- `GET /api/v1/admin/system/modules/package-catalog` reads approved package
  references from `GOVOPLAN_MODULE_PACKAGE_CATALOG` so operators can add known
  module refs to the install plan without typing them manually. The endpoint
  also reports catalog validity, channel, signature, trust state, and the
  configured path.
- `POST /api/v1/admin/system/modules/install-plan/catalog/{module_id}` saves
  a planned install or update row from a validated catalog entry. Installed
  modules are planned as updates. Catalog signature and approved-channel policy
  are enforced before the row is saved. When the selected catalog row requires
  companion dependency or interface-provider updates, the endpoint adds those
  rows to the plan automatically. The saved plan row can also carry a
  data-safety acknowledgement used by preflight for forward-only or destructive
  catalog entries.
- Install-plan preflight returns a structured `target_plan` summary so the
  admin UI can show current version, target version, package refs,
  migration-safety level, update-window and bridge metadata, recovery metadata,
  and acknowledgement state without requiring JSON editing.
- Install-plan preflight also returns a structured `migration_plan` summary with
  target enabled modules and ordered module migration steps. When the installer
  runs with migration enabled, the database migration command receives that
  target module set and ordered module list.
- `POST /api/v1/admin/system/modules/{module_id}/uninstall-plan` saves a
  planned non-destructive uninstall row for an installed module after it has
  been disabled. The Python distribution name is resolved from the installed
  `govoplan.modules` entry point; the WebUI package name comes from the module
  manifest. Operators can then edit the saved plan row and set `destroy_data`
  when they explicitly want module-owned tables/data retired before package
  removal.
- `PUT /api/v1/admin/system/modules/install-plan` saves planned install or
  uninstall rows. Install rows must use tagged package or git references, not
  local `file:`/workspace paths. Python install rows must also include the
  distribution package name so rollback can uninstall newly added packages.
- `DELETE /api/v1/admin/system/modules/install-plan` clears the plan.
- `govoplan-module-install-plan --format shell` or
  `python -m govoplan_core.commands.module_install_plan --format shell` renders
  the same commands from a server shell.
- `govoplan-module-installer --format shell` runs the same preflight checks from
  the server shell.
- `govoplan-module-installer --apply --build-webui` executes the saved plan
  after preflight passes, snapshots `pip freeze` and WebUI package files, writes
  a run record under the runtime installer directory, and marks planned rows as
  applied after success. Successful installs are added to saved startup state by
  default; successful uninstalls are removed from saved startup state by default.
  Use `--no-activate-installed-modules` or
  `--keep-uninstalled-modules-in-desired` only for staged rollout workflows.
- `govoplan-module-installer --supervise --migrate --health-url http://127.0.0.1:8000/health --restart-command '<restart govoplan server>'`
  is the preferred disruptive-change path. It applies the plan, optionally runs
  migrations in a fresh Python process after a fresh-process manifest
  verification, runs the restart command if provided, polls health, and
  automatically rolls packages back from the run snapshot if commands,
  migrations, restart, or health recovery fail.
- `govoplan-module-installer --daemon` runs the request executor. It polls the
  runtime request queue, claims one request at a time, and executes the same
  supervised installer flow. `--daemon-once` processes at most one queued
  request and exits, which is useful for tests or process-manager one-shot
  units. The daemon writes `daemon.status.json` under the installer runtime
  directory so the admin UI and CLI can report heartbeat/status.
- `govoplan-module-installer --enqueue-supervised` creates the same request
  record from a shell instead of from the admin UI.
- `govoplan-module-installer --daemon-status --format json` reports the daemon
  heartbeat. `--cancel-request <request-id>` and `--retry-request <request-id>`
  provide shell equivalents for the admin UI request controls.
- `govoplan-module-installer --validate-package-catalog [path] --format json`
  validates a catalog file or the catalog configured through
  `GOVOPLAN_MODULE_PACKAGE_CATALOG`.
- `--sign-package-catalog <path> --catalog-signing-key-id <key-id> --catalog-signing-private-key <pem>`
  signs a catalog with Ed25519. `--require-signed-catalog`,
  `--approved-catalog-channel <channel>`, and
  `--catalog-trusted-key <key-id>=<base64-public-key>` enforce the approved
  release-channel path from an operator shell.
- `govoplan-module-installer --rollback <run-id>` restores the saved package
  snapshots, restores the captured SQLite or external database snapshot when
  present, restores the previous desired module state when the database was not
  restored wholesale, and reruns package installation from the previous freeze
  file.
- `--database-backup-command '<command>'` and
  `--database-restore-command '<command>'` provide non-SQLite backup/restore
  hooks for migrated installer runs. The backup hook runs before migrations;
  the restore hook runs during rollback and can be overridden on the rollback
  command line.
- `govoplan-module-installer --list-runs --format json`,
  `--show-run <run-id> --format json`, and `--lock-status --format json`
  expose the same run-history and lock information from the operator shell.
- `--list-requests --format json` and `--show-request <request-id> --format json`
  expose daemon handoff records from the operator shell.

The supervisor accepts multiple restart commands and health URLs. This is the
process boundary for web, worker, scheduler, and auxiliary service restarts:
each restart command is executed, each health URL is polled, and rollback uses
the same restart/health set after restoring package and database snapshots.

The installer preflight is intentionally conservative:

- maintenance mode must be active;
- the `shared` state profile blocks in-place package mutation; clustered
  installations must roll one verified immutable module composition across all
  replicas;
- installed module manifests must be compatible with the supported manifest
  contract and current core version;
- uninstalling `tenancy`, `access`, or `admin` is blocked;
- uninstalling an active module is blocked;
- uninstalling a module still present in desired startup state is blocked;
- uninstalling a module with active/desired dependents is blocked;
- uninstalling a module that owns migrations is non-destructive by default:
  schema/data remain dormant and preflight emits a warning;
- modules that declare explicit migration retirement support should also
  register a retirement provider. Without a provider, preflight emits a
  manual-review warning. Providers may return blockers, warnings, and an
  explanatory summary, but core does not drop schema or data on behalf of
  modules.
- module-owned uninstall guard providers can veto data/migration/worker unsafe
  removals;
- install refs must be exact versions or tagged git refs;
- Python install rows must include the package distribution name for rollback;
- WebUI package changes require a WebUI root and trigger rebuild/reload status.

The installer supervisor must run outside the FastAPI server process. A server
request handler cannot reliably restart or roll back the process that is
currently executing the request. The admin UI therefore remains an operator
planning and request-submission surface; the trusted daemon/CLI is the executor.

Automatic rollback covers Python and WebUI package state. The manual/shell
daemon can run `npm install` and `npm run build`; this remains the supported
WebUI package path. Browser-loaded remote module bundles are experimental and
reserved for controlled deployments with integrity/signature policy.

For `sqlite:///` database URLs, `--migrate` also captures a SQLite backup and
rollback restores it before the supervisor restarts the server. For non-SQLite
database URLs, `--migrate` requires deployment-specific backup and restore
commands. A `--database-restore-check-command` can validate the created backup
artifact before migrations proceed. Hook commands run in the installer run
directory with:

- `GOVOPLAN_INSTALLER_RUN_DIR`
- `GOVOPLAN_DATABASE_URL`
- `GOVOPLAN_DATABASE_URL_PGTOOLS` for PostgreSQL URLs converted to the
  `postgresql://` form expected by `pg_dump`, `pg_restore`, and `psql`
- `GOVOPLAN_DATABASE_BACKUP_PATH`
- `GOVOPLAN_DATABASE_BACKUP_METADATA`

Module uninstall does not retire data by default. Package removal leaves
module-owned schema/data dormant. Explicit retirement is a later module-owned
operation guarded by retirement providers.

The running FastAPI server still reports `package_mutation_supported=false`
because dependency-manager operations are not executed inside request handlers.
The trusted mutation boundary is the operator CLI/daemon. This keeps the
interpreter, npm dependency graph, frontend bundle, migrations, and worker
process set under process-supervisor control.

Frontend module loading primarily uses the build-time package graph generated by
the core WebUI host. Installing or uninstalling a WebUI package therefore still
uses `npm install` plus a WebUI rebuild/reload by default. Core also exposes an
experimental hot remote-bundle path for modules that are enabled by the backend
but absent from the local WebUI package graph:

- `FrontendModule.asset_manifest` points at a JSON remote asset manifest.
- `asset_manifest_integrity` is an SRI-style hash for that manifest.
- `asset_manifest_signature` and `asset_manifest_public_key_id` allow the shell
  to verify the manifest when a trusted browser key is registered on
  `window.__GOVOPLAN_REMOTE_MODULE_KEYS__`.
- The remote asset manifest contract version is `1` and contains `moduleId`,
  `entry`, `entryIntegrity`, and optional `moduleExport`.
- The browser fetches and verifies the manifest, fetches and verifies the entry
  bundle, imports it dynamically, and then applies backend metadata before
  adding the module's routes/nav/capabilities.

Unsigned/unhashed remote bundles are skipped. This keeps remote loading a
controlled deployment option rather than a replacement for release package
builds.

## Maintenance Mode

Maintenance mode is the required operating state for package install/uninstall
and other disruptive system maintenance.

Core stores maintenance mode in `system_settings.settings.maintenance_mode`.
The public platform status endpoint exposes only the flag and message so the
WebUI can show a clear login-screen notice. Login remains reachable during
maintenance so an operator can sign in.

Authenticated API access is enforced at the access-principal boundary. When
maintenance mode is enabled, authenticated requests require the system scope
`system:maintenance:access`; otherwise the API returns `503 Service
Unavailable` with a maintenance-mode detail payload. The protected
`system_owner` role grants this through `system:*`. A dedicated
`maintenance_operator` role exists for accounts that should be able to access
the system during maintenance without receiving broad write permissions.

Changing the maintenance-mode flag requires both system settings write access
and `system:maintenance:access`, so an administrator cannot accidentally enable
a mode they cannot use.

The first implementation is a platform access gate. It does not replace
database backups, process supervision, migration checks, or external load
balancer maintenance pages.

## Build And Verification

Backend verification from core:

```bash
cd /mnt/DATA/git/govoplan-core
/mnt/DATA/git/govoplan/.venv/bin/python -m compileall src/govoplan_core ../govoplan-access/src/govoplan_access ../govoplan-admin/src/govoplan_admin ../govoplan-tenancy/src/govoplan_tenancy ../govoplan-policy/src/govoplan_policy ../govoplan-audit/src/govoplan_audit ../govoplan-dashboard/src/govoplan_dashboard ../govoplan-files/src/govoplan_files ../govoplan-mail/src/govoplan_mail ../govoplan-campaign/src/govoplan_campaign
/mnt/DATA/git/govoplan/tools/checks/check_dependency_boundaries.py
```

`govoplan/tools/checks/check-focused.sh` runs npm with an isolated temporary npm user config
so developer-local npm settings do not create release-check warning noise.

Focused module contract and permutation verification:

```bash
cd /mnt/DATA/git/govoplan
GOVOPLAN_CORE_ROOT=/mnt/DATA/git/govoplan-core bash tools/checks/check-module-matrix.sh
```

Core WebUI host verification:

```bash
cd /mnt/DATA/git/govoplan-core/webui
PATH=/home/zemion/.nvm/versions/node/v22.22.3/bin:$PATH /home/zemion/.nvm/versions/node/v22.22.3/bin/npm run build
```

Clean generated `dist`, `.vite`, and source-tree `__pycache__` artifacts after verification unless they are intentionally part of a release artifact.

## Release Dependency Rules

Local development may use editable Python installs and local WebUI `file:` dependencies so sibling module changes reload quickly. Release builds must use tagged git refs or published packages instead. The meta repository provides:

- `requirements-dev.txt` for local editable backend installs
- `requirements-release.txt` for tagged backend module installs
- `webui/package.release.json` for tagged WebUI module installs

Module repositories include root-level npm manifests for git installs. When cutting a release, update the Python versions, WebUI versions, release dependency refs, and repository tags together.
