Files
govoplan-core/docs/MODULE_ARCHITECTURE.md
Albrecht Degering 635d25c74c
Some checks failed
Dependency Audit / dependency-audit (push) Has been cancelled
Module Matrix / module-matrix (push) Has been cancelled
chore: consolidate platform split checks
2026-07-10 12:51:19 +02:00

52 KiB

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, and in the govoplan-access repository docs. The event and audit trace contract is tracked in EVENTS_AND_AUDIT.md. Policy decision, source provenance, and explain-response contracts are tracked in POLICY_CONTRACTS.md. The experimental remote WebUI bundle loading design is tracked in REMOTE_WEBUI_BUNDLES.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

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_access.auth request dependency API, 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

Stable Kernel Contracts

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

  • ModuleManifest
  • ModuleCompatibility
  • 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
  • tenant summary provider contract
  • 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

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

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.

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 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 tenancy plus access; the registry inserts tenancy before access when only feature modules are requested. 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.

FastAPI route dependencies for authenticated endpoints are access-owned and published from govoplan_access.auth. 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:

  • govoplan-tenancy: tenancy_tenants
  • 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.

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:

{
  "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.

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:

[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

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

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.

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 the database heads to the configured script heads instead of assuming one linear revision when multiple modules are enabled.
  • 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.

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 govoplan-web, 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.

Modules should provide:

  • pinned backend and WebUI package refs for official catalog entries
  • compatibility metadata in the module manifest
  • 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.

Example:

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 }) }
  ]
};

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:

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:

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 dependencies and optional dependencies. Optional behavior should be enabled by module presence 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 repository includes scripts/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
  • FastAPI routers may import the published govoplan_access.auth dependency API
  • 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 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, when implemented:

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

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 row from a validated catalog entry. Catalog signature and approved-channel policy are enforced before the row is saved.
  • 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;
  • 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:

cd /mnt/DATA/git/govoplan-core
./.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
./.venv/bin/python scripts/check_dependency_boundaries.py

scripts/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:

cd /mnt/DATA/git/govoplan-core
bash scripts/check-module-matrix.sh

Core WebUI host verification:

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