Self-contained units of functionality with explicit contracts.
A Forsetti module pairs a Swift type with a bundled JSON manifest. Together they declare its identity, version, capabilities, runtime requirements, and any UI contributions — and the runtime manages the full lifecycle from discovery through activation to shutdown.
Anatomy of a Module
Every module conforms to one of three protocols and ships a bundled JSON manifest. The Swift type and the manifest must describe the same module before it can activate.
protocol ForsettiModule {
/// Runtime identity snapshot
var descriptor: ModuleDescriptor { get }
/// JSON-backed activation contract
var manifest: ModuleManifest { get }
/// Lifecycle, with a capability-scoped context
func start(context: ForsettiContext) async throws
func stop(context: ForsettiContext) async
}
/// UI and app modules add structured contributions
protocol ForsettiUIModule: ForsettiModule {
var uiContributions: UIContributions { get }
}
Module Contracts
Identity & Type
Every manifest declares a stable moduleID, a moduleVersion with major, minor, and patch numbers, and a moduleType of service, ui, or app. The moduleID is unique — duplicate IDs fail discovery — and is typically written in reverse-domain form.
Compatibility
Each manifest declares its supported platforms — iOS, macOS, or both — and a Forsetti version range via minForsettiVersion and an optional maxForsettiVersion. The CompatibilityChecker evaluates these, with the schema and template version, before activation.
Capabilities
Modules request permission-like capabilities — networking, storage, secure storage, telemetry, toolbar_items, view_injection, routing_overlay, and more. The host capability policy must grant each one, and the module declares matching runtime requirements for I/O, UI IDs, and data isolation.
Entitlements
Paid or gated modules carry an iapProductID. Before a module starts, the entitlement provider decides whether it is unlocked — backed by StoreKit on iOS, static unlocks on macOS, or a custom provider. A module with no product ID is treated as unlocked.
Module Lifecycle
A module transitions through defined states as the runtime processes it. Each state has clear entry and exit conditions.
The manifest is loaded and indexed by moduleID. The runtime knows the module exists but has not evaluated it yet.
The manifest identity and a content hash are stored in the registration store, so later drift between the accepted contract and the current manifest is caught.
After compatibility, registration, and entitlement checks pass, the registry factory builds the module instance from its entryPoint.
start(context:) has run with a capability-scoped context. A service module runs in the background; a UI or app module also publishes its UI contributions.
stop(context:) has run — during shutdown or when an entitlement is revoked. UI contributions are cleared, and persisted desired-activation state is left intact unless explicitly changed.
UI Contribution Model
UI and app modules publish a structured UIContributions value. The runtime validates it against the manifest and the host renders the approved data — modules never touch host UI directly.
Contribution Types
A UI module publishes a UIContributions value: toolbar items, slot-based view injections, an overlay schema, and an optional theme mask. Each contribution type requires its own capability.
Declared IDs
Template 1.1 modules declare every UI ID they will contribute — viewIDs, slotIDs, toolbarItemIDs, routeIDs, and pointerIDs — under runtimeRequirements.ui. Undeclared contributions fail activation before the surface updates.
Host Rendering
The runtime publishes only the active UI or app module contributions through the UISurfaceManager. The host resolves each viewID through its ForsettiViewInjectionRegistry and renders slots by priority.
// A UI module publishes structured contributions
struct AnalyticsModule: ForsettiUIModule {
var uiContributions: UIContributions {
UIContributions(
viewInjections: [
ViewInjectionDescriptor(
slot: "main",
viewID: "analytics.summary",
priority: 100
)
]
)
}
func start(context: ForsettiContext) async throws {}
func stop(context: ForsettiContext) async {}
}
Structured Contribution Types
| Contribution | Required Capability | Descriptor Type |
|---|---|---|
| Toolbar item | toolbar_items |
ToolbarItemDescriptor |
| View injection | view_injection |
ViewInjectionDescriptor |
| Overlay schema | routing_overlay |
OverlaySchema |
| Theme mask | ui_theme_mask |
ThemeMask |
Start building modules.
Read the documentation for step-by-step guides on creating your first module, defining contracts, and contributing UI.
