Skip to content

Architecture Overview

James Daley edited this page Jun 18, 2026 · 3 revisions

Architecture Overview

Forsetti is organized around strict package boundaries. The runtime core has no dependency on UI frameworks, StoreKit, or host app code. Platform and host layers adapt the core to Apple services and presentation.

Layer Map

flowchart TB
    subgraph Consumer["Consumer Application"]
        AppTarget["App target"]
        AppModules["App-owned modules"]
        AppServices["Host-owned services"]
    end

    subgraph Forsetti["Forsetti Package"]
        HostTemplate["ForsettiHostTemplate"]
        Platform["ForsettiPlatform"]
        Examples["ForsettiModulesExample"]
        Core["ForsettiCore"]
    end

    AppTarget --> HostTemplate
    AppTarget --> Platform
    AppTarget --> Core
    AppModules --> Core
    AppServices --> Core
    HostTemplate --> Platform
    HostTemplate --> Core
    Platform --> Core
    Examples --> Core
Loading

Target Responsibilities

Target Responsibility Must Not Do
ForsettiCore Define contracts, module models, lifecycle orchestration, events, logging, compatibility, activation, and UI contribution state. Import SwiftUI, StoreKit, or host app code.
ForsettiPlatform Provide Apple platform adapters such as StoreKit entitlements and default service implementations. Own app-specific product rules or host-specific UI.
ForsettiHostTemplate Provide reusable SwiftUI host control surfaces, controller state, overlay routing, and view injection registry. Become the application domain layer.
ForsettiModulesExample Demonstrate modules and manifests for tests and reference. Become a production dependency by accident.
Consumer app Own concrete app modules, product configuration, services, and end-user presentation choices. Patch Forsetti internals to solve app-specific behavior.

Dependency Rules

flowchart LR
    Core["ForsettiCore<br/>lowest layer"]
    Platform["ForsettiPlatform"]
    Host["ForsettiHostTemplate"]
    Examples["ForsettiModulesExample"]
    App["Consumer App"]

    Platform --> Core
    Host --> Core
    Host --> Platform
    Examples --> Core
    App --> Core
    App --> Platform
    App --> Host
Loading

The rule is directional: higher layers may depend on lower layers, but lower layers cannot know about higher layers. Required behavior crosses boundaries through protocols, manifests, service injection, or events.

Runtime Components

Component Layer Role
ForsettiRuntime Core Composes services, policy, entitlement provider, registry, loader, module manager, event bus, logger, router, registration store, and UI surface manager.
ManifestLoader Core Loads bundled JSON manifests from ForsettiManifests and validates required fields, schema/template pairing, runtime requirement shape, and duplicate IDs.
CompatibilityChecker Core Validates schema version, manifest template version, platform, Forsetti version range, and requested capabilities.
ModuleRegistry Core Maps manifest entryPoint strings to module factories.
ModuleRegistrationStore Core Persists manifest identity records and hashes for drift detection.
ModuleManager Core Registers discovered manifests, activates, deactivates, restores, persists, validates module identity and runtime requirements, and manages the active UI module.
UISurfaceManager Core Publishes active UI module contributions into host-readable state.
ForsettiContext Core Gives modules scoped access to services, eventing, logging, and routing.
ForsettiHostController Host template Converts runtime state into UI-ready module lists and user operations.
ForsettiEntitlementProviderFactory Platform Creates StoreKit or debug/static entitlement providers.

Control Plane and Data Plane

flowchart TD
    Control["Control plane<br/>manifests, registration, compatibility, capability policy, entitlements"] --> Activate["Activation decision"]
    Data["Data plane<br/>services, events, logging, routing"] --> Running["Running module"]
    Activate --> Running
    Running --> UI["UI surface contributions"]
    UI --> Host["Host presentation"]
Loading

The control plane decides whether a module may run. The data plane is what an already-running module can use safely.

Design Constraints

Constraint Reason
Modules declare capabilities before activation. Permission-like behavior must be reviewable.
Modules declare runtime requirements before activation. Required services, UI IDs, data isolation, and provider roles must be inspectable.
Discovered manifests are registered before activation. Manifest drift is detected before lifecycle code runs.
Service and UI contribution use is enforced at runtime. Capabilities are runtime permissions, not advisory metadata.
Paid modules use entitlement checks before start(context:). Lock state is runtime policy, not just UI decoration.
UI modules expose structured contributions. Host composition remains deterministic and inspectable.
Exactly one UI/app module is active by default. Application UI ownership stays clear while service modules can run concurrently.
Service dependencies are resolved through protocols. Modules stay portable and testable.
Architecture tests enforce import direction. Boundary drift is caught in CI instead of by convention.

Clone this wiki locally