Architecture & processes
Proton combines a native MoonBit application with the Chromium browser engine. The application process manages windows, application state and operating-system features. Web pages provide the interface and invoke host operations through messages.
Components and responsibilities
| Component | Responsibility |
|---|---|
| Native MoonBit application | Runs the application entry point, business logic and asynchronous tasks |
| Proton | Provides APIs for windows, lifecycle, browser contents and frontend/host communication |
| CEF (Chromium Embedded Framework) | Embeds Chromium in the native application, providing browser creation, callbacks and process communication interfaces |
| Chromium | Implements the web platform, including rendering, JavaScript, networking and browser storage |
| Web frontend | Implements the application interface with HTML, CSS and JavaScript |
Applications use the public API in moonbit-community/proton. Proton's native bindings are compiled from source into the application; CEF/Chromium is loaded as a separate runtime and bundled for distribution. Pages use that bundled browser engine rather than an operating-system WebView.
The frontend can use ordinary JavaScript or JavaScript produced by the MoonBit compiler. Rabbita is an optional MoonBit UI library; proton_rabbita integrates commands and subscriptions with it. Neither is a required part of the Proton runtime.
Process model
The application executable runs MoonBit host code and hosts CEF's browser process. This process manages native windows and browser instances and coordinates Chromium subprocesses. Page scripts execute in renderer processes; GPU and utility processes provide graphics and other browser services.
Application process
MoonBit application logic
|
Proton -- Native windows
|
CEF browser process
| Chromium inter-process communication
+-- Renderer processes: pages, JavaScript, workers
+-- GPU process: graphics
+-- Utility and other processes: browser services
cef_process is the helper executable CEF launches for subprocesses. CEF selects the role at launch, so the same helper can serve different subprocess roles. It does not execute the application's MoonBit entry point. The helper matches the application's Proton release and CEF runtime.
Chromium manages process allocation according to pages and runtime state. One renderer can host multiple tasks, such as a page and workers. Window counts, browser-instance counts, task counts and OS process counts therefore do not map one to one. Task resource metrics cannot simply be summed into application-wide usage.
Windows and browser contents
One Proton application runtime can manage multiple native windows. Each window owns its main browser contents and can host additional child browser views:
Application runtime
+-- Window A
| +-- Main browser contents
| +-- Child browser view
+-- Window B
+-- Main browser contents
A window owns its title, position, dimensions and native decorations. Browser contents own documents, navigation, scripts and page events. A child view is an independent browser within the window's content area, with its own document and bounds. Navigating it does not navigate the main page; removing it does not close the entire window.
These objects have distinct lifetimes: a page can reload or its renderer can terminate while the native window remains alive. Closing a window also closes its hosted browser contents. See the application API for creation, closing and recovery contracts.
Execution and communication boundaries
Native UI objects are managed by their creating thread. Proton connects native event processing to the moonbitlang/async external event loop: native callbacks enqueue records and wake the scheduler, then MoonBit handles events and application callbacks. Asynchronous application tasks can yield execution, but expensive synchronous work still occupies the host event loop.
Renderers and the host have separate execution environments. Proton injects a bridge into pages, delivers frontend requests to registered host handlers and returns their results or errors. The host can also send event notifications. Payloads are serialized; shared type definitions do not create shared memory.
proton_contract describes typed commands and events, and proton_client supports MoonBit frontend calls. JavaScript frontends use the page bridge directly. Host command bindings and capabilities select which operations are available to which pages. See commands, events and capabilities for protocol, cancellation and authorization behavior.
Development and distribution
Development and distribution use the same native host and browser architecture. The sources of page content and runtime files change.
| Scenario | Page source | Native runtime source |
|---|---|---|
| Inline HTML | A string compiled into the application | Shared runtime and helper installed by setup |
| Frontend development server | Development URL configured through the CLI | Shared runtime and helper installed by setup |
| Packaged application | Inline content or packaged static resources; URL entries may also use remote pages | Runtime and helper shipped in the artifact |
A frontend development server serves page resources; it does not run host business logic. Opening the same URL in an ordinary browser provides no Proton native bridge. Packaging assembles the application executable, required page resources, CEF runtime and helper into a distributable artifact. Configuration documents file responsibilities and output paths; Command Line Interface documents packaging formats and requirements.
Environment requirements
The installation commands below target 0.4.0, which is not yet available on Mooncakes. See Release Notes for API migration details.
Proton 0.4.0 supports the following host build environments. Native builds and packages target the host operating system; cross-compilation is not provided by the Proton CLI.
| Platform | Architecture | Build requirements |
|---|---|---|
| macOS | Apple Silicon | Xcode Command Line Tools, clang |
| Windows | x64 | Visual Studio C++ Build Tools, MSVC and Windows SDK; developer terminal |
| Linux | x64 | C/C++ build tools, pkg-config, X11/RandR and GTK development libraries; graphical environment for execution |
On Ubuntu 24.04, the dependency set includes build-essential, pkg-config, libx11-dev, libxrandr-dev, libgtk-3-dev, libwebkit2gtk-4.1-dev, libnotify-dev, libnss3, libgbm1 and libasound2t64. Package names and runtime compatibility vary by distribution.
NSIS is required only for NSIS output. Signing identities and notarization credentials are distribution requirements, not prerequisites for local development.
Tool commands
| Tool | Role | Installation or inspection |
|---|---|---|
| MoonBit | Compiler, package manager and runtime tools | Official installation; moon version |
| Proton CLI | Project development and packaging | moon install moonbit-community/proton_cli@0.4.0; proton_cli --version |
| Warren | Isomorphic frontend dev server and build | moon install moonbit-community/warren@0.4.5; warren --help |
| Node.js / npm | Warren's JavaScript build tooling | Node.js installation; node --version, npm --version |
MoonBit's binary directory must be on PATH. Minimal applications with inline HTML do not require Warren or Node.js. moonx's deprecated native mode is not required by the documented frontend commands.
Runtime installation
proton_cli cef setup resolves the release's Chromium runtime and matching source-built helper. Its initial run needs network access and may take several minutes. Arbitrary CEF downloads do not satisfy the release contract.
| Location | Content |
|---|---|
~/.proton/store/ | Immutable shared CEF SDK/runtime installations |
~/.proton/helpers/ | Helpers selected by platform and Proton version |
PROTON_RUNTIME_STORE | Optional absolute override for runtime storage |
Runtime/helper files are not copied into project source. They are assembled into distributable applications during packaging. CLI and Proton module versions must agree. proton_cli doctor checks project configuration, tools, runtime and helper without changing them.
To create an application with these tools, follow the minimal application tutorial. The repository checkout below is needed only to run the examples in this book.
Source checkout and runtime
Examples use the repository workspace. Clone the release commit, install the required tools, and set up its runtime once:
git clone https://github.com/moonbit-community/proton.git
cd proton
git checkout bdb169302952db553deda6de015887c7a6a19831
moon update
proton_cli cef setup
After setup, run the command listed on the chosen example page from this repository root. These commands use the repository workspace, not a scaffolded application.
Application API reference
The root moonbit-community/proton package is the public application facade. Use the builder before run; use context and handle operations while the application is running. CLI project metadata does not replace runtime configuration.
| Area | Owner / entry | Reference |
|---|---|---|
| Startup, readiness, quit, task cleanup | App, ApplicationContext, lifecycle hooks | Lifecycle |
| Window declarations, instances, child browsers | App, WindowManager, WindowHandle, ViewHandle | Windows |
| Request/response communication | contract, registrar, client | Commands |
| Frontend notifications and subscription lifetime | emitters, client subscriptions | Events |
| Host operations and renderer authorization | App.capability, extension-specific scope | Capabilities |
| Cookies, browser storage, proxy | SessionHandle, startup builder | Browser sessions |
| Signed application updates | App.update_channel, PendingUpdate | Updates |
| Process ownership and activation | single-instance builder, context methods | Process control |
| Paths, files and build metadata | project configuration and application paths | Configuration |
For the complete set of methods, overloads, typed errors and defaults, use the versioned API index. This reference describes when to use each entry point, which object owns its state, and how it behaves during startup, cancellation and shutdown.
Application lifecycle
App configures an application before startup. It is created through the root Proton facade and executed through the MoonBit async runtime. Proton installs its native event-loop integration during initialization; application code does not install or poll a separate UI loop.
Entry sources
| Constructor | Entry |
|---|---|
html(title, html, ...) | Inline HTML document |
url(title, url, ...) | URL |
file(title, path, ...) | Local HTML file |
asset(title, path, ...) | Managed application asset entry |
Each returns an App builder. Shared options include width, height, debug mode and resizability. entry_html, entry_url, entry_file and entry_asset configure the entry on an existing builder.
Configuring an application
Configuration methods, including config, debug, add_window, and on_* hook
registration, modify the existing App and return Unit. Use .. to retain the
application value across configuration calls:
let app = @proton.html("Hello", "<h1>Hello</h1>")
..config(@proton.load_config())
..debug()
app.single_instance()
app.run()
A direct call such as app.single_instance() returns Unit. Constructors still
return App; queries such as app.name() retain their result types. run() and
run_or_abort() remain ordinary async calls.
To migrate an existing builder chain, change its configuration calls from . to
... Keep . for queries and the final run() / run_or_abort(). Configuration
validation, registration order, and lifecycle behavior are unchanged.
Identity
@proton.load_config() returns the complete typed AppConfig and reports loading errors immediately. Read identifier, backend, frontend, and package_config directly from this value. Install it with app.config(config), which is mutually exclusive with app.identifier(...). The identifier is stable application identity, distinct from the window title, product name, and version.
app_path(), resource_dir() and is_packaged() describe the execution environment. Development paths and packaged resources have different locations; frontend URLs are not backend filesystem paths.
Hooks and owned state
| Hook | Meaning |
|---|---|
app_lifecycle(on_start, on_shutdown) | Application-level state; the start result is passed to shutdown |
window_lifecycle(on_ready, on_close) | Per-window state; the ready result is passed to close |
on_window_close_request | Async allow/deny decision before window closure |
Application and window contexts expose task groups and window management. Window contexts additionally expose their handle and event emitter. A window's state must not outlive the resources it refers to.
Running the application
Calling run() starts the configured application and awaits its lifecycle. It is async and raises AppRunError on failure. run_or_abort() instead reports a failure and aborts. Startup, task ownership and exit behavior are described below.
Startup order
Startup validates configuration, resolves identity and paths, initializes logging, and acquires the single-instance lock when configured. A secondary instance that forwards its activation returns without creating an application runtime.
For the primary instance, session creation precedes application start hooks. Successful start hooks establish the application state. Proton then opens declared startup windows that were not already opened by a start hook, waits for the required bridge initialization, and activates the initial windows through their ready hooks. Application readiness follows successful initial activation; configuring a window or receiving a native creation event does not establish readiness.
A start hook may await context.windows().open(id). A committed exit stops startup. A denied quit request leaves the application running; requesting quit is not itself a committed exit.
Task and cleanup ownership
Use application task groups for work that belongs to the application, and window task groups for work that belongs to a window. Their scopes are canceled and drained on shutdown; detached work outside these scopes is not made lifecycle-safe by holding a window handle.
Each lifecycle hook that successfully returns state installs its paired cleanup hook. A hook that fails before returning has not produced state to pass to its paired cleanup; previously established scopes still clean up. Cleanup hooks run under cancellation protection. Reported cleanup failures can accompany the original failure in AppRunError, rather than replacing it.
Ready/close hooks own per-window state; creation and browser event callbacks report observations. A renderer can navigate or crash while its native window remains alive. Page tasks and subscriptions must therefore also respect page lifetime, as described in commands and events.
Quit decisions and forced exit
| Operation | Contract |
|---|---|
context.quit(exit_code=0) | Requests orderly shutdown; cancelable |
context.exit(exit_code=0) | Forces shutdown and process termination, including status zero |
handle.close() | Requests closing that window; subject to its close interceptor |
The default LastWindowClosedPolicy::Quit requests shutdown after the last window closes; KeepRunning keeps the application running until an explicit quit or exit. Browser and child-view teardown must finish before normal runtime shutdown completes.
An orderly quit runs on_before_quit, closes windows through their close decisions, then runs on_will_quit after windows close. ApplicationQuitDecision::Prevent cancels the quit; a denied window close also cancels the request. Cancellation does not recreate windows that have already closed. on_quit observes the final exit code before runtime teardown and is not a veto point.
Forced exit bypasses cancelable decisions, including an already pending decision, but still performs the final quit notification and runtime cleanup. It is not equivalent to externally killing the process. Scheduled relaunches are started after cleanup and before process termination.
On successful ordinary zero-status quit, App.run() returns. A successful nonzero quit terminates with that status; forced exit terminates even for zero. Code following run() must not be the only place for required shutdown cleanup.
Commands
A command is a typed request/response operation across the renderer–host boundary. Payloads are serialized; a shared MoonBit type does not share memory or execute backend code in the renderer.
Contract and binding
| API | Contract |
|---|---|
proton_contract.command[Request, Response](name) | Declares a typed application route; does not register a handler |
App.commands(register, targets?) | Installs application command bindings for selected renderer targets |
CommandRegistrar.bind(command, handler) | Binds an async handler taking (CommandContext, Request) and returning Response |
proton_client.invoke(command, request) | Async frontend invocation; returns Response or raises ClientFailure |
proton_rabbita.invoke(...) | Adapts completion/failure to Rabbita commands |
On the backend, Request must implement FromJson and Response must implement ToJson. The frontend requires the inverse conversions. Contract and registration errors are distinct from failures of a running request. Application route names must be unique and valid; descriptors are validated during binding and invocation.
The handler context identifies the caller and supports emit_to_caller. A handler may await backend work. Business outcomes such as validation rejection can be modeled in the response type rather than thrown as transport failures.
JavaScript interface
The injected bridge exposes application methods as window.__MoonBit__.app.<name>(request). Calls return promises; failures reject them. Application routes use app:; extension operations use ext:. An ordinary browser page has no injected native bridge.
Request scope and page invalidation
Each accepted request has its own command scope. Awaited work, child tasks, and deferred cleanup in that scope belong to the request. A handler or its scoped cleanup failing is a request failure (handler_failed), not an instruction to terminate the application. Runtime infrastructure failures remain application errors.
Navigation, renderer termination, or bridge failure invalidates the old page's pending requests. Once a bridge attempt has failed, that failed page cannot start new application commands until a new valid attempt is established. Initialization-time requests are permitted; waiting for every request until ready would prevent legitimate initialization work.
Caller cancellation and page invalidation stop waiting and request cancellation of outstanding work. Neither rolls back business side effects already performed. Use application-level operation identifiers or transactions when a retry must not duplicate an operation. Do not infer successful execution from successful message submission.
Cancellation
proton_client.invoke_with_callbacks returns a cancellation function. It cancels response observation and requests transport cancellation; late responses are ignored. Async invoke also cancels the pending request when its task is cancelled. The request scope described above governs the backend work; cancellation does not undo completed side effects.
Client failures
| Variant | Meaning |
|---|---|
BridgeUnavailable | Native bridge is absent |
InvalidContract | Invalid command/event descriptor |
RemoteFailure | Backend rejection with code, message and optional detail |
TransportFailure | Communication failure |
ResponseDecode | Response does not decode as the declared type |
RequestCancelled | The pending request was cancelled |
Command error codes
RemoteFailure.code identifies the failure independently of its message:
| Code | Meaning |
|---|---|
invalid_payload | The request does not match the command's input type |
unknown_op | The requested operation is not registered |
handler_failed | The command handler raised an error |
host_closed | The command host has closed |
permission_denied | The page is not permitted to invoke the operation |
Handle codes rather than parsing message text, and handle unrecognized codes as
other remote failures. Backend diagnostics remain in application logs. Development mode
also includes them in detail; message remains
a caller-facing description. Expected business outcomes belong in the command's
response type.
Resource limits
Proton does not impose a fixed payload-size limit on commands. Payloads are serialized as JSON and remain subject to memory and underlying transport constraints. Large messages increase serialization, copying and parsing costs.
Request concurrency has no global fixed admission limit in 0.4.0. Applications still need to bound expensive work according to their own resource and ordering requirements; the bridge does not serialize unrelated business operations into a transaction.
Events
Events are typed host-to-renderer notifications with no response value. proton_contract.event[Payload](name) declares a descriptor; it does not install a listener or retain previous notifications.
Destinations
| API | Destination and lifetime |
|---|---|
CommandContext.emit_to_caller(event, payload) | The page that issued the current command |
WindowContext.events() | Produces a window event emitter |
WindowEventEmitter.emit(event, payload) | The associated window while it remains alive |
Host payloads require ToJson. Window emitters belong to window lifetime; a saved emitter must be removed from application state when that window closes. Emission is not an implicit broadcast to all windows.
Subscriptions
proton_client.subscribe(event, listener, failure) decodes payloads through FromJson and returns a Subscription. Subscription.close() releases the listener. Subscription setup can raise ClientFailure; event decoding failures are reported as EventDecode through the failure callback.
proton_rabbita.subscribe integrates subscription ownership into Rabbita. Its options include a subscription key, retry count, ready command and client override; full signatures are in the Rabbita API.
The JavaScript interface is window.__MoonBit__.app.on(name, callback). The callback receives an event envelope containing payload; registration returns an unsubscribe function.
Delivery semantics
- Notifications missed before subscription are not replayed.
- Command responses and events are separate deliveries; their relative order is not an application synchronization contract.
- Listener disposal ends observation; events are not a durable queue or acknowledgment protocol.
- A notification that state changed can invalidate a frontend query. The authoritative snapshot is obtained through a command.
Subscription lifetime and state synchronization
A subscription belongs to the current renderer document or UI component. Close it when that owner is disposed. A reload creates a new document and requires a new subscription; keeping a native window alive does not preserve JavaScript listeners across reloads.
For state synchronization, subscribe before loading the initial snapshot, use events to invalidate that snapshot, and ignore responses from superseded queries. Subscribing first avoids a gap before the initial read, but does not turn two independent messages into an atomic transaction. Include a revision in application data when the consumer must detect stale snapshots or missed changes.
Application-level lifecycle notifications such as on_window_created and on_render_process_gone are host callbacks, not proton_contract frontend events. Register them on the App builder; use a command or explicit event if the frontend also needs that information.
The event tutorial demonstrates subscription and cleanup. The Todo tutorial demonstrates invalidation followed by a snapshot query.
Windows and browser views
Window declarations belong to App; operations on a running window belong to WindowHandle. Browser navigation and developer tools belong to WebContentsHandle, obtained through WindowHandle.web_contents().
Identity and declaration
The primary window ID is main. App.add_window(id, config, open_on_start?) declares secondary windows; their IDs must be nonempty, unique and different from main. Titles are display text and do not identify windows.
Secondary windows open at startup by default. open_on_start=false defers opening until WindowManager.open(id). ApplicationContext.windows() and WindowContext.windows() expose the manager. All windows belong to one application runtime.
WindowConfig(title, entry, ...) holds dimensions, size hint, theme, titlebar style, traffic-light position, and initial views. Use App(config) for the primary window, App.main_window(config) to replace its startup configuration, and App.add_window(id, config, ...) for secondary windows. Registrations snapshot the views array. The html, url, file, and asset shortcuts remain available for a simple primary window.
Opening and instance identity
WindowManager.open(id) is asynchronous and returns an activated WindowHandle. The id selects a declaration; it is not a permanent identity for every future native instance. After closing and reopening a declared window, obtain the new handle through open or find; an old handle does not become valid again.
Cancellation before activation commits discards the queued open or closes the instance created by that operation. After activation commits, the application owns the window: canceling the caller later does not close it. Unknown declarations and invalid window state produce window-session failures; task cancellation follows the async task's cancellation semantics.
hide() preserves the instance, browser, and associated work. close() initiates teardown and can be denied. Do not equate a close request with completed cleanup. Per-window state should be released by its lifecycle cleanup, not immediately after requesting close.
Lookup and browser readiness
WindowManager.find(id) returns the current active instance, including a window that is still starting. It does not wait for native browser initialization, page loading, or command bridge readiness. Obtaining a WebContentsHandle does not wait for these conditions either.
These are distinct conditions:
- Native browser initialization makes browser queries such as
WebContentsHandle.state()available. During startup, a query can raiseWindowSessionError::OperationFailedbecause the browser is not initialized. Do not assume that all browser operations are queued until initialization completes. - Page loading concerns the current navigation. For work that needs a loaded page, observe
on_web_contents_eventfor that window or view, then verify the expected URL and page state.DidFinishLoadreports a loading-to-idle transition; by itself it does not prove that the intended navigation succeeded. HandleLoadFailedseparately. - Command bridge readiness concerns communication with the current page. A browser handle or a load-completion event alone does not establish that the bridge is ready.
Coordinate operations with the condition they actually require. A fixed delay after find() is not a readiness guarantee, and navigation or closure can invalidate earlier observations.
Runtime operations
| Area | WindowHandle operations |
|---|---|
| Visibility and focus | show, hide, focus, is_visible, is_focused |
| Geometry | bounds, set_bounds, position, set_position, content_size, set_content_size |
| Window state | minimize, maximize, restore, set_fullscreen |
| Appearance | set_theme, set_background_color, set_title, set_menu |
| Lifetime | close |
Native operations can raise WindowSessionError. A handle is not valid indefinitely: window-owned state and retained event destinations must be released when the window closes. A hidden window is still alive.
Close semantics
App.on_window_close_request registers an async callback that receives the window handle and returns WindowCloseDecision::Allow or Deny. window_lifecycle.on_close is cleanup after closure; it is not a veto hook. The default last-window policy is Quit. KeepRunning leaves the application running with no open windows and requires an explicit exit path. See application lifecycle.
Child browser views
WindowConfig.views declares initial views on either primary or secondary windows; WindowHandle.add_view creates one dynamically and returns ViewHandle. A view is a child browser hosted inside a window, not a second top-level window. Its bounds use a top-left origin; visibility and z-order are independent of the main page. remove_view removes a child. Closing the parent must also complete child-browser teardown.
Browser and view boundaries
Both WindowHandle.web_contents() and ViewHandle.web_contents() return WebContentsHandle. Navigation, script evaluation, zoom, audio, developer tools, printing, downloads and session access use that common handle. Child geometry, visibility, z-order and removal remain on ViewHandle; native window controls remain on WindowHandle. Child navigation does not navigate the main page.
Subscribe to App.on_web_contents_event for main-page and child-page events. Callbacks carry the common handle: window_id() identifies the owning window and view_id() is None for its main page. Handles remain bound to one native instance; reusing a declaration id does not revive an old handle. A child view does not receive the application command bridge.
on_render_process_gone covers both main pages and child views without requiring an extra on_web_contents_event subscription. Renderer termination invalidates page work; it is not a normal window close. Decide whether to reload or present recovery UI based on the reported details, and recreate page subscriptions after navigation.
A view declaration sets initial geometry; it does not provide automatic layout. Update its bounds when the parent content layout changes.
PDF output
WebContentsHandle.print_to_pdf(path, options?) is asynchronous and returns Unit after successful completion. Call it in an async context and handle failure at the call site; no completion-event subscription or request-id correlation is needed. Concurrent calls on main and child pages keep separate completion identities. Closing the page, losing its renderer, or ending the application wakes outstanding callers. Cancellation stops waiting, not the underlying native print job; the destination file may still be produced.
Platform behavior
WindowThemePreference controls a window's theme; system_appearance() reports system appearance. These are separate concerns. Titlebar styles and native controls differ by platform. Traffic-light positioning applies to macOS; the frontend layout must account for native controls when using an overlay titlebar.
Extensions and capabilities
Extensions register reusable host operations. A renderer capability installs an extension's backend and grants a permission scope to selected renderer targets. Adding proton_ext as a module dependency only makes code available; it does not install handlers or grant access.
Registration and targets
App.capability(capability, targets?) configures installation and access. With no explicit targets, the grant applies to the main entry. RendererTarget.entry(window=...) and RendererTarget.bundled(window=...) select entry or bundled-page targets for a named window. Permissions do not implicitly become global merely because windows belong to one application.
Application commands use app: routes; extension operations use ext:<extension>/<operation>. JavaScript can invoke an installed operation through window.__MoonBit__.core.invokeOp(route, request), which returns a promise.
Filesystem scope
proton_ext/fs.capability accepts PermissionRoot values pairing a host directory with permitted operations. The scope is configured by the backend; renderer requests cannot widen it.
| Property | Behavior |
|---|---|
| Relative root or request path | Resolved against resource_dir() |
| Path outside allowed roots | Rejected, including symlink escapes |
| Operation absent from root grant | Not authorized by that grant |
| Text payloads | UTF-8 |
Supported filesystem operation names include read_file, write_file, mkdir, readdir, remove, rmdir, rename, realpath, exists, kind and size. Canonical path checks and operations are serialized within the extension.
Availability and errors
A missing capability leaves its route unavailable. An installed extension can still reject a request because of scope, invalid arguments, a platform limitation or an operating-system failure. These failures are reported through the command bridge; installation does not imply that every native operation will succeed.
Capability-specific scope types are documented in the extension API; platform coverage is summarized below.
Platform differences and selection
These are key boundaries of implemented 0.4.0 capabilities, not a promise of availability without desktop services:
| Capability | macOS | Windows | Linux |
|---|---|---|---|
| Files, paths, host HTTP, child processes | Supported | Supported | Supported |
| Native dialogs, text clipboard | Supported | Supported | Depends on desktop session / GTK and related backends |
| System notification extension | Supported | Not implemented | Not implemented |
| Tray | Menus and platform events | Click, right-click, double-click and menus | Requires AppIndicator / Ayatana and desktop support |
| Desktop sources and thumbnails | Displays; thumbnails require screen-recording permission | Visible titled windows and GDI thumbnails | X11/RandR displays; thumbnails are null |
Capability grants are separate from operating-system authorization. Declaring a screen or media capability does not grant OS privacy permission. Handle OS denial, unavailable backends and missing application grants separately. Check support where provided, such as the tray capability; menu events are more portable than platform mouse gestures.
Host networking and child processes
The net extension performs host HTTP requests and returns status, headers and UTF-8 text. It is not browser fetch, does not share the browser cookie jar and does not follow redirects automatically. Its text response is not a lossless binary transfer interface. Processes spawned by the process extension belong to its application lifetime: wait collects handles, kill still needs wait, and application shutdown cancels and reaps remaining children.
See the extension API for the complete inventory and scope types. Check target grants, OS permission and platform backend support independently.
Browser sessions
A browser session contains cookies, cache, authentication state and web storage. Obtain its handle from ApplicationContext.session() even before a window exists, or from any WebContentsHandle.session(). Both access the same application session. Closing a page does not invalidate the session, and clearing shared data affects the other pages using that profile.
Startup settings
App.session_partition(name) selects a persistent profile below the application's sessionData directory. It is application startup configuration, not a per-window incognito switch. Windows in the application runtime use that configured profile. Use stable application identifiers and partition names to preserve data across launches.
App.proxy(server, bypass?) configures Chromium's startup-wide proxy. It cannot be changed during the run. This controls browser traffic; it does not configure every MoonBit networking library in the host.
The on_session_created callback exposes the partition and resolved data path before application start hooks create their windows. Packaged resources and the browser profile are different paths; do not put writable profile data inside the application bundle.
Operations
| API | Effect |
|---|---|
get_cookies | Asynchronously reads matching cookies; optional URL and HttpOnly selection |
set_cookie | Sets a cookie with URL, name, value and optional domain/path/security attributes |
delete_cookies, flush_cookies | Deletes selected cookies or flushes persistent cookie storage |
clear_cache, clear_storage_data | Clears cache or selected web-storage kinds |
clear_auth_cache | Clears cached HTTP authentication |
clear_certificate_exceptions | Clears certificate exception state |
close_all_connections | Closes session connections |
Operations raise SessionError and require a live application runtime, not a live page. Handles remain usable with no windows under KeepRunning; application teardown wakes pending cookie readers and invalidates retained session handles. Concurrent cookie reads are independent. A clearing operation does not erase application-owned files or implement an application logout protocol; invalidate backend credentials and UI state separately.
Full attributes and StorageDataKind variants are in the API reference.
Application updates
Application updates replace the distributed application, not the development CLI or Mooncakes dependencies. Configure App.update_channel(endpoint, public_keys, check_on_launch?, freshness_days?) before running the app.
Channel and checks
The endpoint must use HTTPS and there must be at least one trusted RSA public key. Automatic checking defaults to enabled; manifest freshness defaults to 30 days and must be positive. Keys use the format validated by updater public-key.
The automatic check runs after successful startup and reports failures through logging without making startup fail. For explicit checks, use ApplicationContext.check_for_update() and handle its result and errors. NotConfigured is distinct from UpToDate; a context used after its application ends no longer has an active channel.
An offered PendingUpdate has passed manifest trust, freshness and revision checks. It exposes version, revision, size and optional notes URL, but the artifact has not yet been downloaded.
Download and installation
PendingUpdate.download() writes the artifact into a private stage and verifies its size, digest and signature. It does not replace application files or exit on any platform. Repeated downloads of the same prepared revision are a no-op.
After downloading, call ApplicationContext.quit_and_install() when the user chooses to apply the update:
match context.check_for_update() {
Available(update) => {
update.download()
context.quit_and_install()
}
UpToDate | NotConfigured => ()
}
The request follows the quit lifecycle. Preventing quit cancels installation and retains the download for another attempt. Once quit is accepted, Proton closes the runtime and completes cleanup before installation. If single-instance mode is enabled, its reservation stays held throughout replacement. Windows transfers the reservation to its NSIS installer before the old process exits; the installer releases it after installation. macOS and Linux retain the lock while replacing the application artifact, then release it before launching the replacement. Successful handoff terminates the old process, including when the exit code is zero.
The method returns when the request is accepted, not when the new version is ready. Missing downloads and conflicting exit or relaunch requests fail immediately; later installation or launch failures are reported through App::run(). A forced exit() supersedes a pending update request. A pending update exit refuses explicit release of a held single-instance lock; an application that already released its configured lock cannot request installation. Ordinary quit discards the download without installing. Cleanup failure prevents installation. Old managed update artifacts are removed after the replacement successfully starts.
Publisher responsibilities
Package a stable application identifier with a monotonically increasing revision. The updater metadata options in CLI package describe the artifact location, publication instant and revision. updater public-key only formats a public key; it does not generate keys, sign an update manifest or host an endpoint. OS signing/notarization and updater signature verification serve different checks; configuring one does not configure the other.
Process control and metrics
Application process control determines which instance owns the runtime and how later launches reach it. It is separate from window visibility and from the task metrics exposed by Chromium.
Single-instance ownership
App.single_instance() uses the application identity to select one owning process. Later instances forward URL, document or reopen activation and return after the primary loop accepts it. Acceptance does not wait for asynchronous launch handlers to complete. Forwarding has a five-second deadline; failure does not kill the primary or start another owner.
ApplicationContext.has_single_instance_lock() queries ownership and release_single_instance_lock() releases it during the run. Releasing does not terminate the process. Choose deliberately whether another process may now become the owner. Register launch-input handling for both initial and forwarded activations.
App state versus window state
Application focus, hide/show, active/hidden queries and readiness describe the application. A window handle controls one window. A hidden window remains alive, and an application with KeepRunning can remain alive with no windows. Always provide an explicit exit path for background applications.
Task metrics
ApplicationContext.task_metrics() returns AppTaskMetric rows for Chromium tasks. A renderer and multiple workers can share a process. Task ids identify tasks; the hosting process usage in multiple rows may be identical because it belongs to the shared process. Do not sum rows into a process or application total.
CPU is zero until a sampling interval has passed; 100% represents one fully used core. Memory is -1 before measurement, not zero bytes. The fields are process_cpu_percent and process_memory_bytes. These observations do not confer ownership of a process and should not be used to kill helpers independently of Proton's lifecycle.
API references
Create a project
This guide creates a desktop window with the minimal template. You will run it, change its contents, and build the executable. Finish prerequisites before starting.
1. Create the application
Run this command in the directory where you keep projects, not inside another MoonBit workspace:
proton_cli new hello-proton --template minimal --yes
cd hello-proton
The explicit --template minimal matters: the non-interactive default is isomorphic. --yes accepts generated defaults. For a distributable application, choose your identity at creation with --identifier com.example.hello-proton.
Template files
hello-proton/
moon.mod
proton.project.json
app/
moon.pkg
main.mbt
moon.modgives the module its name and declares versioned dependencies. Add a library here before importing its packages.app/moon.pkgimports the packages used by the entry and marks it as a native executable.app/main.mbtdefines the async entry, HTML, and application builder.proton.project.jsontells the CLI which package to run and how to identify/package the app.
Module dependencies and package imports are separate. A dependency makes a module available; a package import selects the APIs and aliases used by that package.
2. Prepare and run
From hello-proton/:
moon update
proton_cli cef setup
proton_cli dev
A native window opens with the generated greeting. The terminal remains occupied while the app runs. Close the window before continuing.
The CLI builds the native executable and starts it with the configured project metadata. This template has inline HTML, so there is no frontend server to start.
3. Change the page
Replace the entire contents of app/main.mbt with:
///|
async fn main {
let html =
#|<!doctype html>
#|<html lang="en">
#|<meta charset="utf-8">
#|<title>Hello Proton</title>
#|<style>
#| body { font: 18px system-ui; padding: 32px; }
#|</style>
#|<h1>Hello from MoonBit</h1>
#|<p>This page lives inside a desktop window.</p>
#|</html>
@proton.html("Hello Proton", html, width=900, height=700)
..config(@proton.load_config())
.run_or_abort()
}
Run proton_cli dev again. You should see “Hello from MoonBit”, a short paragraph, and a 900 × 700 initial window.
@proton.html creates the app builder. Its first argument is the window title; the second is the HTML document. The #| lines form a MoonBit multiline string.
..config(@proton.load_config()) loads the complete application configuration, including its required identity, and installs it on the application. The display title alone is not an application identity.
.run_or_abort() runs until the app exits and reports startup/runtime failures. The entry is async, so retain the template's moonbitlang/async import.
4. Check and build
After closing the window:
moon check --target native
proton_cli build
A successful build creates the native executable; it does not yet assemble a distributable application. Build and distribute explains that next step.
If the app does not start, run proton_cli doctor and inspect the full terminal error. If it builds but shows the old page, stop the previous process and rerun dev; this template has no frontend hot-reload server.
Where to go next
To add interaction to this page, follow calling the backend. To write the UI in MoonBit too, use the complete isomorphic example.
Calling the backend
A command lets frontend code ask the native backend to do work. This guide adds a working greeting form to the minimal project. It uses plain JavaScript first so the native boundary is visible; the Todo tutorial uses shared MoonBit types and Rabbita.
Add the contract dependency
In moon.mod, add this entry inside the existing import { ... } block, preserving the existing dependencies:
"moonbit-community/proton_contract@0.4.0",
Replace app/moon.pkg with:
import {
"moonbitlang/core/json",
"moonbitlang/async",
"moonbit-community/proton",
"moonbit-community/proton_contract",
}
supported_targets = "native"
pkgtype(kind: "executable")
Run moon update from the project root. A module dependency makes the library available; the package import makes @proton_contract usable in the entry.
Define, register, and call a command
Replace app/main.mbt with this complete example:
///|
struct GreetRequest {
name : String
} derive(FromJson, ToJson)
///|
let greet : @proton_contract.Command[GreetRequest, String] =
@proton_contract.command("greet")
///|
async fn main {
let html =
#|<!doctype html>
#|<html lang="en">
#|<meta charset="utf-8">
#|<title>Greeting</title>
#|<style>body { font: 18px system-ui; padding: 32px; }</style>
#|<label>Name <input id="name" value="MoonBit"></label>
#|<button id="greet">Greet</button>
#|<p id="result" role="status"></p>
#|<script>
#| document.querySelector("#greet").onclick = async () => {
#| const result = document.querySelector("#result");
#| try {
#| result.textContent = await window.__MoonBit__.app.greet({
#| name: document.querySelector("#name").value
#| });
#| } catch (error) {
#| result.textContent = String(error);
#| }
#| };
#|</script>
#|</html>
@proton.html("Greeting", html)
..config(@proton.load_config())
..commands(fn(registrar) raise {
registrar.bind(greet, (_context, request) => {
"Hello, " + request.name + "!"
})
})
.run_or_abort()
}
Run proton_cli dev. Enter “Ada” and click Greet. The page should display “Hello, Ada!”.
There are three connected parts:
GreetRequestdescribes the JSON request, andgreetdeclares its route and response type...commands(...)registers a handler withregistrar.bind. The handler receives the caller context and decoded request.window.__MoonBit__.app.greet(...)returns a JavaScript promise for the response. The page awaits it and handles rejection.
Declaring a descriptor alone does not register a handler. Keep command names unique within the application.
If the page opens in a normal browser, window.__MoonBit__ is absent. Start the desktop app with proton_cli dev, not just the frontend URL.
Arguments and return values
The object property name matches the request field. Add serializable fields to the request when an operation needs more input. Results can be strings, numbers, arrays, or structs with ToJson.
The native handler executes in the backend. It is the right place for business validation or native operations; the renderer does not directly call arbitrary MoonBit functions.
Errors
A malformed request, unknown command, or handler failure rejects the JavaScript promise. Keep the try/catch around the call and show a useful message.
Expected business outcomes should be response data. For example, the Todo template returns Changed, InvalidTitle, or MissingTodo. Its frontend handles these separately from ClientFailure, which describes bridge, transport, and decoding failures.
Do not assume a request succeeded just because the frontend button handler ran. Wait for its result before updating UI that depends on successful completion.
Async work and caller context
Command handlers can perform async work. The context identifies the caller and can send a typed event to it; sending events to the frontend extends this exact example.
Sending events to the frontend
Use events when the backend needs to notify a frontend about a change. This guide extends the greeting command from calling the backend: clicking the button still invokes a command, but the displayed message now comes from an event.
Declare the event
In app/main.mbt, add this declaration alongside the existing greet descriptor, outside main:
///|
let greeted : @proton_contract.Event[String] =
@proton_contract.event("greeted")
The event name is greeted and its payload is a string. It has no response type: emitting an event does not ask the frontend to return a value.
Send to the caller
Inside ..commands(...), replace the existing registrar.bind(greet, ...) call with:
registrar.bind(greet, (context, request) => {
let message = "Hello, " + request.name + "!"
context.emit_to_caller(greeted, message)
message
})
context.emit_to_caller sends to the page that invoked this command. The final message is still the command response. The response and event are separate deliveries; do not rely on a particular ordering between them.
Listen before triggering work
Replace the contents between <script> and </script> in the HTML string with the following JavaScript. In the MoonBit multiline string, prefix each line with #| as in the previous example:
let stopListening;
document.querySelector("#greet").onclick = async () => {
const result = document.querySelector("#result");
const app = window.__MoonBit__.app;
try {
if (!stopListening) {
stopListening = app.on("greeted", ({ payload }) => {
result.textContent = "Event: " + payload;
});
}
await app.greet({ name: document.querySelector("#name").value });
} catch (error) {
result.textContent = String(error);
}
};
window.addEventListener("pagehide", () => {
stopListening?.();
});
Start the app, enter a name, and click Greet. The expected message is now “Event: Hello, Ada!”. Clicking again updates it without adding another listener.
The first click installs the listener before invoking the command, so this example does not emit its first notification before subscribing. app.on returns an unsubscribe function. Here it is kept until the page leaves; a component-based UI should release it when the owning component is disposed.
Notify a window outside a command
Background work may need to notify a window after the originating command has completed. The Todo template saves context.events() from window_lifecycle(on_ready=...) and removes that destination in on_close. The saved WindowEventEmitter can emit typed events while the window remains alive.
Choose destinations explicitly. Do not retain stale window emitters or treat every event as a global broadcast. See the complete Todo example for the attach/detach lifecycle.
Events and state
A listener can miss events while it is absent. For application state, use an event to invalidate data, then query a fresh snapshot. The Todo app subscribes before its initial query and refreshes on todos_changed.
Use command results for acknowledgement of requested work. Use events for notifications to observers. If a workflow requires durable delivery or replay, implement that in the application's state model rather than assuming the event bridge provides it.
Managing windows
This tutorial adds a secondary window to the minimal application, then controls its visibility and closing behavior. Start from that project with its existing package imports; replace its entry point with the code below.
Create a second window
Replace app/main.mbt with:
///|
async fn main {
@proton.html("Main", "<h1>Main window</h1>", width=900, height=700)
..config(@proton.load_config())
..add_window(
"help",
@proton.WindowConfig(
"Help",
@proton.AppEntry::Html("<h1>Help</h1><p>Close this window to return.</p>"),
width=480,
height=320,
),
)
..window_lifecycle(
on_ready=context => {
let id = context.handle().id()
println("Ready: " + id)
id
},
on_close=id => println("Closed: " + id),
)
.run_or_abort()
}
Run proton_cli dev. Both Main and Help should open. The terminal prints each window's ID when it is ready and when it closes. Closing Help leaves Main running; closing the last window exits the app.
The primary window's ID is main. Secondary IDs must be nonempty, unique, and different from main. The ID identifies a window in code; its title is display text.
Open a window on demand
add_window opens the declared window at startup by default. Set open_on_start=false to declare it without opening it. Later, use context.windows().open("help") from an application or window context.
For example, add open_on_start=false to the Help declaration above, and add this builder call before .run_or_abort() to open it from the application startup hook:
..app_lifecycle(
on_start=context => { ignore(context.windows().open("help")) },
on_shutdown=_ => (),
)
A later user action can call the same window manager operation from a retained application context. Do not create a second Proton runtime for a second window.
Use and release handles
context.handle() inside on_ready gives the current WindowHandle. It supports operations such as focus(), hide(), show(), and close(). Browser operations are available through handle.web_contents().
A lifecycle callback's return value becomes the corresponding on_close argument. The example returns an ID; the Todo template uses that ID to remove a saved event destination. Keep window-owned state in this lifecycle and stop using the handle after close.
Decide when the application exits
The default last-window policy quits the application. Tray and background applications can add:
..last_window_closed_policy(@proton.LastWindowClosedPolicy::KeepRunning)
With that policy, closing all windows does not stop the process. Provide an explicit quit action before using it in a real application.
Use on_window_close_request for an asynchronous close decision: return WindowCloseDecision::Allow to proceed or Deny to keep the window open. A confirmation or unsaved-document check belongs here; on_close is cleanup after closing.
Platform-specific window behavior
Native window decorations and available controls differ across platforms. Check the window API before relying on platform-specific methods. Keep frontend titlebar layout and native configuration together when using an overlay titlebar.
A secondary page is also a separate capability target. Grant only the host operations it needs; see native capabilities.
Using native capabilities
Extensions expose reusable host operations to the renderer. A capability installs an extension's backend handlers and grants access to a specific scope. This tutorial adds file reading to the minimal application: the backend grants one directory, and the page requests a text file inside it.
Add the filesystem extension
In the minimal project's moon.mod, add this dependency inside import { ... }:
"moonbit-community/proton_ext@0.4.0",
Use these imports in app/moon.pkg, then run moon update:
import {
"moonbitlang/async",
"moonbit-community/proton",
"moonbit-community/proton_ext/fs",
}
supported_targets = "native"
pkgtype(kind: "executable")
Prepare a file
Create a directory named workspace at the project root. Inside it, create message.txt containing:
Hello from the filesystem.
Replace app/main.mbt with:
///|
async fn main {
let html =
#|<!doctype html>
#|<html lang="en">
#|<meta charset="utf-8">
#|<title>Read a file</title>
#|<button id="read">Read message.txt</button>
#|<pre id="result" role="status"></pre>
#|<script>
#| document.querySelector("#read").onclick = async () => {
#| const result = document.querySelector("#result");
#| try {
#| const reply = await window.__MoonBit__.core.invokeOp(
#| "ext:fs/read_file", { path: "./workspace/message.txt" }
#| );
#| result.textContent = reply.content;
#| } catch (error) {
#| result.textContent = String(error);
#| }
#| };
#|</script>
#|</html>
@proton.html("Read a file", html)
..config(@proton.load_config())
..capability(
@fs.capability([
@fs.PermissionRoot("./workspace", ["read_file"]),
]),
)
.run_or_abort()
}
Run proton_cli dev and click Read message.txt. The page should display the file contents. This is an actual host filesystem read, not a browser file picker.
Understand the grant
The capability has three relevant choices:
- Operation: only
read_fileis allowed; this page cannot use this grant to write or delete. - Root: only paths inside
./workspaceare allowed. - Target: with no explicit
targets, the grant applies to the main window entry.
The low-level route ext:fs/read_file belongs to the filesystem extension. Application commands use the separate app: route space. You do not register the filesystem handler yourself.
Relative roots and requests resolve against @proton.resource_dir(), which the CLI configures for the application. Do not assume they resolve against an arbitrary terminal working directory.
Check failures deliberately
Change the request path to a missing file within workspace: the call should fail and the catch block should show the error. Change it to a file outside the allowed root: the grant should reject access.
Removing ..capability(...) does not stop the app from starting, but the route becomes unavailable. Adding an extension dependency alone is not a permission grant.
Multiple windows and persistent files
For a second window, select explicit RendererTarget::entry(window="...") or RendererTarget::bundled(window="...") targets when adding the capability. Grant each page only what its feature requires.
The local workspace directory is useful for this development exercise. Installed resources may be read-only; use an appropriate writable application-data or user-selected directory for persistent files. Package files you intentionally ship via resources configuration.
Other capabilities
Dialogs, clipboard, shell, tray, and other extensions follow the same explicit installation/grant model, but each defines its own scope and platform support. The notification extension in this release targets macOS; do not infer platform support from the framework's overall platform list.
Consult the extension API for the capability builder and request/response types you need.
Build a complete Todo application
This tutorial uses the isomorphic template to connect a shared contract, native state, and a Rabbita UI. You will first run the generated Todo app, then add Complete all and Reopen all operations across all three modules.
Complete prerequisites, including Node.js for Warren's build tooling. You do not need to modify the earlier minimal app.
Create and explore the app
From outside another MoonBit workspace:
proton_cli new todo-app --template isomorphic --yes
cd todo-app
moon update
moon install moonbit-community/warren@0.4.5
proton_cli cef setup
The generated configuration already invokes the installed Warren executable. Start the application:
proton_cli dev
Add “Alpha” and “Beta”, mark one complete, and search for it. The generated app already supports create, complete, delete, and filtered queries. It keeps the list in memory, so restarting clears the data.
Close the application before editing. Keep the generated module names and package imports; all changes below extend the existing template.
Template organization
The isomorphic template has three modules connected by a workspace:
todo-app/
moon.work
proton.project.json
shared/
moon.mod
moon.pkg
todo_contract.mbt
backend/
moon.mod
app/
moon.pkg
main.mbt
todo/
moon.pkg
backend.mbt
commands.mbt
frontend/
moon.mod
main/
moon.pkg
main.mbt
internal/query/
public/
The root moon.work connects three modules:
- shared defines serializable payloads and command/event descriptors. It is used by both build targets.
- backend builds to native code.
todo/backend.mbtholds Todo state and operations;todo/commands.mbtbinds those operations;app/main.mbtstarts the app. - frontend builds to JavaScript.
main/main.mbtdefines the Rabbita UI;public/contains its HTML and stylesheet.
The template's frontend/internal/query manages query state and subscriptions. It belongs to the generated application and can be changed with it; it is not a public Proton API.
Application commands use ordinary bind calls; this template requires no command code-generation rule.
Understand the existing flow
For an existing create action, Create invokes create_todo; the backend validates and mutates; MutationReply reports success or a business rejection. todos_changed invalidates the frontend snapshot and triggers a query.
The frontend's draft is local UI state. The backend list is authoritative application state. The new action must mutate the full backend list, not just the rows currently visible under a search filter.
1. Extend the shared contract
Append to shared/todo_contract.mbt:
///|
pub(all) struct SetAllCompletedRequest {
completed : Bool
} derive(ToJson, FromJson)
///|
pub extend SetAllCompletedRequest with ToJson::{to_json}
///|
pub extend SetAllCompletedRequest with FromJson::{from_json}
///|
pub let set_all_completed : @proton_contract.Command[
SetAllCompletedRequest,
MutationReply,
] = @proton_contract.command("set_all_completed")
The boolean chooses completion or reopening. We reuse the template's MutationReply, so the existing response handling remains useful. Both targets import this same descriptor.
2. Implement the state change
Append to backend/todo/backend.mbt:
///|
fn Backend::set_all_completed(
self : Backend,
completed : Bool,
) -> @shared.MutationReply {
let mut changed = false
for index = 0; index < self.todos.length(); index = index + 1 {
let todo = self.todos[index]
if todo.completed != completed {
self.todos[index] = { ..todo, completed, }
changed = true
}
}
if changed {
self.version += 1
}
@shared.Changed(version=self.version)
}
This operation traverses the full list and advances the revision once if anything changed. Repeating the same operation leaves the revision unchanged. It returns the current version even for an empty list.
3. Register the handler and send invalidation
Inside the existing Backend::register_commands method in backend/todo/commands.mbt, add another binding beside the existing bindings:
registrar.bind(@shared.set_all_completed, (_context, request) => {
let reply = self.set_all_completed(request.completed)
self.notify(reply)
reply
})
Keep the old bindings. self.notify is the generated backend's existing helper: it sends todos_changed to the event destinations attached by the app's window lifecycle.
No new event type is needed. The state changed in the same way as another Todo mutation, so existing observers should refetch the same query.
4. Connect the frontend action
All changes in this step are in frontend/main/main.mbt.
Add one variant inside the existing enum Msg:
SetAllCompleted(Bool)
Add this arm inside update's existing match msg:
SetAllCompleted(completed) =>
(
{ ..model, error: None, },
@proton_rabbita.invoke(
@shared.set_all_completed,
{ completed, },
reply => emit(MutationReceived(reply)),
error => emit(CommandFailed(error)),
),
)
model, emit, MutationReceived, and CommandFailed already exist in this function and module. The success path reuses the template's mutation response handling; the error path keeps bridge failures visible.
Inside view, find div(class="todo-toolbar", [...]). Append these two buttons to its child array, after the existing Refresh button:
button(
type_="button",
on_click=emit(SetAllCompleted(true)),
"Complete all",
),
button(
type_="button",
on_click=emit(SetAllCompleted(false)),
"Reopen all",
),
The template already imports button, so no new import is needed. The boolean in the message travels through the request to the backend.
5. Check the behavior
Append this regression check to backend/todo/backend_wbtest.mbt:
///|
test "bulk completion changes the whole list once" {
let backend = Backend()
ignore(backend.create("Alpha"))
ignore(backend.create("Beta"))
assert_true(backend.set_all_completed(true) is @shared.Changed(version=3))
assert_true(backend.snapshot("").todos.all(todo => todo.completed))
assert_true(backend.set_all_completed(true) is @shared.Changed(version=3))
assert_true(backend.set_all_completed(false) is @shared.Changed(version=4))
assert_true(backend.snapshot("").todos.all(todo => !todo.completed))
}
From the project root:
moon check --target js,native
moon -C backend test todo --target native
moon -C frontend test --target js
proton_cli dev
Add two todos. Complete all should complete both, and Reopen all should reopen both. Search for only one todo and repeat the operation, then clear the filter: both items should have changed. Repeating Complete all should not advance the revision again.
If the backend changes but the display does not, check that you retained self.notify and the window attach/detach hooks. If the new command is unavailable, check its registration and restart the backend.
How the UI stays current
The template's todo_app creates a query for list_todos invalidated by todos_changed. Its application-local query helper subscribes before fetching and cancels superseded requests when input changes.
The mutation response refreshes the initiating UI; the event invalidates observers. These can overlap, so the helper tracks request generations and avoids letting an older result overwrite a newer search. This is application code in frontend/internal/query, not another server or a public Proton state-management framework.
Build the finished application
Stop development and run proton_cli build. Choose the following branch for your host OS. Run packaging commands from the todo-app project root.
macOS and Windows
For local acceptance, create an unsigned app artifact. On Windows, app means a directory containing the EXE and its dependencies, not a macOS bundle.
proton_cli package --release --format app --dry-run
proton_cli package --release --format app
Open the artifact path printed by the CLI under dist: the .app on macOS, or the application EXE inside the Windows output directory. Do not copy the EXE alone. This step needs no NSIS and does not perform distribution signing or notarization.
Linux
The default template supplies no icon. Save a valid PNG icon as icons/app.png inside the project and add this field to the existing package object in proton.project.json:
"icons": ["icons/app.png"]
Check that appimagetool --version runs. If necessary, install it in your user directory from an Ubuntu x64 shell. Extracting the packaging tool avoids requiring FUSE to run it:
mkdir -p "$HOME/.local/lib/proton-appimagetool"
cd "$HOME/.local/lib/proton-appimagetool"
curl -fL -o tool.AppImage https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage
echo 'ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 tool.AppImage' | sha256sum -c -
chmod +x tool.AppImage
./tool.AppImage --appimage-extract
ln -sf squashfs-root/AppRun appimagetool
export PATH="$HOME/.local/lib/proton-appimagetool:$PATH"
appimagetool --version
Return to the todo-app project root, then run:
proton_cli package --release --format appimage --dry-run
proton_cli package --release --format appimage
Launch the output AppImage in a graphical session. If the runtime environment cannot mount AppImages, run the artifact with --appimage-extract and launch AppRun inside the extracted directory.
Acceptance
With the development server stopped, launch the artifact, add two items, and verify Complete all, Reopen all, and search. Packaging failure, launch failure, and incorrect button behavior are separate stages; dry-run alone is not acceptance.
The list is intentionally in memory and resets on restart. Implement persistence in the backend. See packaging reference for icons and distribution requirements.
Command Line Interface
This page lists every proton_cli command and its options. Application IPC commands are documented separately in Commands. Configuration file fields are defined in project configuration.
Invocation and global options
proton_cli [--cwd <directory>] <command> [options]
| Option | Meaning |
|---|---|
-C, --cwd <directory> | Working directory; default .; available to subcommands |
-h, --help | Help for the selected command |
-V, --version | CLI version; equivalent in purpose to version |
--config, --moon-target-dir and the doctor report path are resolved from the selected working directory. dev --frontend-path and package icon/output paths are resolved from the project configuration directory. Paths in a project configuration are resolved from that file; frontend.dist uses the frontend directory. Explicit command options override the corresponding configuration values.
Command index
| Command | Purpose |
|---|---|
help | Show command help |
version | Print CLI version |
new | Create an application project |
dev | Build and run an application in development mode |
build | Build frontend assets and the native backend |
package | Build and assemble distribution artifacts |
doctor | Report environment and project diagnostics |
cef setup | Install the release-matched runtime and helper |
cef requirements | Print the embedded CEF requirement |
updater public-key | Validate and format a trusted updater public key |
help
proton_cli help [command] [subcommand]
Displays help without executing the selected command. For example, proton_cli help cef setup describes runtime installation; it does not install anything. proton_cli <command> --help is the equivalent option form.
version
proton_cli version
Prints proton_cli <version>. It accepts no command-specific options and does not perform the update check.
new
proton_cli new [path] [options]
Creates project source files. Without --yes, interactive prompts are used outside CI. The defaults below apply when no prompt or explicit option overrides them.
| Argument / option | Default and meaning |
|---|---|
[path] | todo; destination directory |
--template <name> | isomorphic; accepts minimal or isomorphic |
--title <text> | Derived from the project directory name; application/window title |
--author <name> | username; MoonBit module author |
--identifier <id> | dev.proton.<normalized-project-name>; reverse-DNS application identifier |
--width <pixels> | 960; positive integer |
--height <pixels> | 640; positive integer |
--check, --no-check | Check enabled by default; runs moon check --target js,native after creation |
--git, --no-git | Explicitly enable/disable Git initialization; non-interactive default does not initialize Git |
-y, --yes | Skip prompts and accept defaults |
--dry-run | Print planned files without writing them |
minimal creates a native module with inline HTML. isomorphic creates shared, frontend and backend modules. Invalid identifiers, nonpositive dimensions and unsupported template names are rejected.
dev
proton_cli dev [options] [-- <application-arguments>...]
Builds and runs the native backend. When a frontend command is configured, it starts that command, waits for readiness and supplies the development URL to the application. Backend changes require a rebuild/restart; frontend refresh is owned by the frontend tool.
| Option | Default and meaning |
|---|---|
--config <path> | Project configuration; defaults to proton.project.json in the working directory |
--package <selector> | Overrides the configured backend package; fallback app |
--moon-target-dir <path> | Override the internal Moon invocation's output directory |
--url <url> | Override frontend.dev_url |
--command <command> | Override frontend.before_dev |
--frontend-path <path> | Override the frontend command's working directory |
--timeout-ms <milliseconds> | 30000; frontend readiness timeout |
--ready-path <path> | Use an HTTP readiness probe at this path; otherwise checks TCP connectivity |
--no-frontend | Do not start a frontend command; use an externally managed server |
--setup, --no-setup | Automatic installation of missing runtime/helper; disabled by default |
-- <arguments>... | Arguments for the application, not for Moon |
A CLI-managed frontend requires a command and rejects an already occupied development endpoint. HTTP probing requires curl. A readiness timeout prevents application launch. The CLI owns the frontend process it starts; an external server has a separate lifetime.
build
proton_cli build [options] [-- <moon-build-arguments>...]
Runs the configured frontend build, validates frontend output, then builds the native backend. A failed frontend command prevents the backend build. The result is build output, not a complete distribution.
| Option | Default and meaning |
|---|---|
--config <path> | Project configuration; defaults to proton.project.json in the working directory |
--package <selector> | Overrides the configured backend package; fallback app |
--moon-target-dir <path> | Override the internal Moon build's output directory |
--no-frontend | Skip the frontend build command; does not generate missing frontend assets |
-- <arguments>... | Forward to moon build, e.g. -- --release |
The backend target is always native. A forwarded --target override is rejected.
package
proton_cli package [options]
Builds and assembles the application, frontend assets, CEF runtime, matching helper and declared resources for the host platform. A project configuration is required. This command does not accept trailing Moon arguments.
| Option | Default and meaning |
|---|---|
--config <path> | Project configuration; defaults to proton.project.json |
--package <selector> | Override backend package |
--release | Use release build output; otherwise debug |
--dry-run | Resolve and print the plan without executing the full build/package flow |
--product-name <name> | Override package.product_name |
--version <version> | Override application version; does not select a Proton version |
--format <format> | Repeatable; app, zip, dmg, nsis, appimage, subject to host support |
--output <directory> | Override output directory |
--icon <path> | Repeatable; override configured icons |
--url-scheme <scheme> | Repeatable; override configured URL schemes |
--nsis-install-mode <mode> | currentUser, perMachine or both; overrides Windows configuration |
--macos-minimum-system-version <version> | Override package.platforms.macos.minimum_system_version |
--sign | Sign the application; disabled by default |
--notarize | macOS signing, notarization and stapling; implies --sign |
--updater-base-url <https-url> | Emit update metadata using this artifact base URL; requires publication instant and revision |
--updater-published-at <instant> | Publication instant in YYYY-MM-DDTHH:MM:SSZ; requires base URL |
--updater-revision <integer> | Monotonic release sequence embedded in the app and update metadata |
CLI lists such as --format override configured lists. Application identity comes from the top-level configuration identifier; package has no --identifier option. --release does not imply signing. A successful dry run does not verify artifact creation or runtime behavior. See packaging for platform formats, configuration defaults and resource rules.
Signing environment
Credentials are environment configuration, not command options.
| Variable | Meaning |
|---|---|
PROTON_MACOS_SIGNING_IDENTITY | Required identity for macOS signing |
PROTON_MACOS_ENTITLEMENTS | Optional entitlements file |
PROTON_MACOS_NOTARY_PROFILE | macOS notary credential profile; PROTON_NOTARY_PROFILE is the fallback |
PROTON_MACOS_BUILD_NUMBER | Optional bundle version override; one to three numeric components |
PROTON_MACOS_ALLOW_ADHOC | 1 permits ad-hoc signing for local diagnostics |
PROTON_WINDOWS_CERTIFICATE | Required certificate path for Windows signing |
PROTON_WINDOWS_CERTIFICATE_PASSWORD | Certificate password; defaults to empty |
PROTON_WINDOWS_TIMESTAMP_URL | Timestamp service; defaults to http://timestamp.digicert.com; none disables it |
doctor
proton_cli doctor [options]
Inspects the environment and current project without installing dependencies or repairing project files. It returns failure when diagnostics fail.
| Option | Meaning |
|---|---|
--verbose | Include detailed project diagnostics |
--json | Render one JSON document; conflicts with --quiet |
--quiet | Render only failing diagnostics; conflicts with --json |
--output <path> | Write the report to a file |
cef setup
proton_cli cef setup
Installs the CEF runtime and helper selected by this Proton release in the shared user-level stores and prints the runtime root. It accepts no command-specific options, writes no project runtime-selection file and does not select an arbitrary CEF version. Installation requires network access when artifacts are missing.
cef requirements
proton_cli cef requirements
Prints the release's embedded CEF requirement as JSON. It does not install the runtime or inspect an application's running browser. No command-specific options.
updater public-key
proton_cli updater public-key --modulus <hex> [--exponent <hex>]
Validates a trusted RSA public key and prints a comment with its bit length followed by rsa-sha256:<modulus>:<exponent>, suitable for App::update_channel's public_keys.
| Option | Meaning |
|---|---|
--modulus <hex> | Required RSA modulus; accepts the Modulus= prefix produced by OpenSSL |
--exponent <hex> | Public exponent; default 010001 (65537) |
Surrounding whitespace is trimmed and hexadecimal is normalized to lowercase. The runtime's key parser validates the key, including modulus size and exponent. This command does not generate a key pair, read PEM files, sign artifacts or publish updates.
Exit status and update checks
| Status | Meaning |
|---|---|
0 | Command succeeded |
1 | Execution failed, or diagnostics reported failure |
2 | Argument parsing failed |
Commands other than version perform a best-effort CLI update check. PROTON_NO_UPDATE_CHECK=1 disables it. An unavailable update service does not prevent the command from running.
Packaging reference
The Proton CLI packages a project into a distributable containing its executable, frontend assets, CEF runtime, matching helper and declared resources. A backend executable alone is not a complete distribution. Packaging targets the host platform.
Field types and defaults are defined in Configuration.
Platform overrides
Each platform object accepts formats, resources and sign. Windows additionally accepts nsis_install_mode; macOS accepts minimum_system_version. A platform format list replaces the shared list. Platform resources are combined with shared resources. Path values use the project configuration directory as their base.
| Platform | Formats | Default |
|---|---|---|
| macOS Apple Silicon | app, zip, dmg | app, zip |
| Windows x64 | app, zip, nsis | app, zip |
| Linux x64 | appimage | appimage |
Icon and tool prerequisites
| Artifact | Icon and tooling |
|---|---|
| macOS app / zip | ICNS for the application icon; native Apple tooling for builds and signing |
| macOS dmg | Uses system hdiutil to create an image from the app |
| Windows app / zip | Windows SDK rc.exe embeds ICO into the EXE; PNG is not automatically converted |
| Windows nsis | Also requires NSIS makensis |
| Linux appimage | Requires a PNG icon and executable appimagetool on PATH |
The default scaffold does not provide icons. On Linux, missing PNG validation fails before invoking AppImage tooling. See the Todo packaging steps for preparation. Supply the platform's icon format rather than expecting Windows to convert icons intended for other platforms.
NSIS installation mode
| Value | Behavior |
|---|---|
currentUser | Default; current user, no administrator installation required |
perMachine | All users, elevation required |
both | Installer offers a scope choice; may prompt for elevation even for current-user installation |
Changing the mode is not an installation-scope migration. Updates to an existing machine-wide installation should retain its intended scope.
Minimum macOS version
minimum_system_version only writes macOS LSMinimumSystemVersion metadata. It does not make a binary built against a newer SDK/CEF compatible with older systems. Validate the complete artifact on the minimum supported OS; compilation alone is insufficient.
Signing and notarization
--release controls build mode and does not imply signing. On macOS, --sign requests signing; --notarize requests notarization, stapling and validation using configured credentials. Identity and credential environment variables are listed in the CLI reference.
An unsigned or locally signed artifact does not establish a trusted publisher identity. Credentials are deployment configuration, not application source.
Runtime resource behavior
Packaged frontend files load without the development server. Backend resource paths resolve through resource_dir(). Installed resources may be read-only; persistent application data belongs outside them.
--dry-run validates and displays the packaging plan. It does not prove that packaging, signing or the packaged runtime works. The Todo tutorial includes the application build and package exercise.
Diagnostics reference
Diagnostics come from distinct layers: project/toolchain validation, native startup and lifecycle, bridge requests, and frontend rendering. A successful frontend preview does not verify native operations; a successful build does not verify packaged execution.
Diagnostic interfaces
| Interface | Scope |
|---|---|
proton_cli doctor | Read-only project, toolchain, runtime and helper checks |
proton_cli --version / moon version | Tool versions |
WebContentsHandle.open_devtools() | Chromium inspector for a browser |
App.run() | Typed application execution errors |
App.run_or_abort() | Error reporting followed by abort on failure |
ClientFailure | Frontend contract, bridge, transport and decoding failures |
WindowHandle.web_contents() provides the browser handle. DevTools belongs to that browser's lifetime. Opening it from a window-ready hook must preserve any existing hook state and cleanup behavior.
CDP endpoint discovery
Set PROTON_REMOTE_DEBUGGING_PORT=0 when launching the native application to let Chromium select an available port. Read the actual browser WebSocket endpoint from its process output:
DevTools listening on ws://127.0.0.1:<port>/devtools/browser/<id>
Use the announced endpoint for that process lifetime; do not reserve and release a port before launch or assume a fixed port. An explicit port remains supported. This is Chromium's diagnostic endpoint, not Proton's application command transport.
The application must have debugging enabled (App.debug()); the port variable
does not enable it. For native MoonBit automation, see the optional
proton_cdp library.
Logs
Proton uses tonyfettes/xlog. Development output uses stderr; packaged applications use the platform log directory. Application categories use app.*; framework categories use proton.*. Application level and category settings are preserved.
| Environment variable | Effect |
|---|---|
MOON_XLOG | xlog filtering |
PROTON_LOG_OUTPUT=stderr | Select stderr output, including for terminal-launched packaged apps |
PROTON_CEF_LOG | Temporary switch for separate CEF internal diagnostics; disabled by default |
File output depends on packaged metadata. CEF diagnostics are not the application logging interface.
Locate the actual log
Packaged applications write proton-<pid>.log by default. Directories use the application identifier, not its display name:
| Platform | Default directory |
|---|---|
| macOS | ~/Library/Logs/<identifier>/ |
| Windows | %LOCALAPPDATA%<identifier>\Logs\ |
| Linux | $XDG_STATE_HOME/<identifier>/logs/, or ~/.local/state/<identifier>/logs/ when unset |
App.path(AppPathKind::Logs) returns the resolved path. Use that result when set_path or set_app_logs_path overrides the default. PROTON_LOG_OUTPUT=stderr redirects a terminal-launched packaged app to the terminal; unpackaged apps cannot use file mode.
Diagnose by stage
| Symptom | Check and next action |
|---|---|
| CLI cannot find runtime | Compare proton_cli --version with the application's proton dependency; inspect cef requirements and run cef setup; retain the complete setup error |
| dev never reaches frontend readiness | Run before_dev inside frontend.path, check dev_url host/port; use --no-frontend for an existing service instead of starting a second one |
| No bridge in a normal browser | Launch the native application through proton_cli dev; browser previews have no host bridge |
| permission_denied / unknown_op | Check bound descriptors, capability targets and current window/page; adding a module dependency is insufficient |
| Frontend command not found | Check that the configured tool is installed and on PATH; Warren is installed separately from Proton CLI |
| Request or response decode failure | Compare the command descriptor types with the actual JSON payload; inspect ClientFailure and backend logs |
| Packaged resource 404 | Inspect the artifact resource tree and relative HTML URLs, frontend.dist and package.resources; retry without a development server |
| Process remains after window close | Check KeepRunning and denied quit decisions first, then final exit/cleanup errors; record process state and do not count manual killing as successful shutdown |
| appimage packaging fails | Check the PNG and appimagetool --version; distinguish icon validation from create AppImage tool failures |
Reporting a problem
Include the command and full error, Proton and MoonBit versions, OS and architecture, and whether the failure occurs in development or in a packaged application. Attach proton_cli doctor --json output, the relevant application log, and a minimal reproduction. For shutdown failures, record whether the process exited on its own or was terminated manually. Review logs for application data or credentials before sharing them.
Project configuration
proton.project.json contains application identity and CLI build/package metadata. It is a JSON object with four recognized top-level fields. Unknown fields are rejected. Window state, command registration and capabilities belong to the MoonBit application builder, not this file.
The default filename is proton.project.json. To use another filename such as moon.proton.json, pass --config moon.proton.json to dev, build or package; it is not an automatically discovered alias.
Top-level fields
| Field | Type | Requirement / meaning |
|---|---|---|
identifier | string | Required, validated application identifier |
backend | object | Optional at decoding; provides the CLI backend location and entry |
frontend | object | Optional frontend command and asset configuration |
package | object | Optional distribution metadata; required for metadata-driven packaging |
identifier is trimmed and must have at least two nonempty dot-separated components. Each component starts with an ASCII letter or digit and contains only ASCII letters, digits or hyphens.
A minimal configuration is:
{
"identifier": "com.example.app",
"backend": { "path": ".", "package": "app" }
}
Backend
When backend is present, both fields are required.
| Field | Type | Interpretation |
|---|---|---|
path | string | Directory, resolved relative to the configuration directory |
package | string | Moon package selector used from that directory |
Frontend
All frontend fields are optional at decoding. Individual CLI commands impose additional requirements.
| Field | Type | Default / interpretation |
|---|---|---|
path | string | Configuration directory if omitted; command working directory |
dev_url | string | No default; development page URL |
before_dev | string | No default; frontend server command |
before_build | string | No default; frontend build command |
dist | string | No default; output directory, relative to frontend.path when specified |
dev requires a frontend command when it manages a configured development URL. --no-frontend connects to an externally managed server. build runs the configured build command before validating the frontend output and building the backend. An existing occupied development endpoint is rejected when the CLI is responsible for starting its server.
Paths and resources
Backend/frontend paths, package icons, package resources and package output are resolved from the configuration directory. frontend.dist is resolved from the frontend directory. Absolute paths remain absolute. Path separators follow the host operating system. Loading configuration preserves path strings and does not require referenced files or directories to exist; build and packaging operations check the inputs they use.
@proton.resource_dir() is the application resource base: the CLI supplies the project root in development; packaged applications use their resources directory; direct runs without managed metadata use the startup working directory. Installed resources are not a persistent writable data store.
package
These fields belong to the package object in proton.project.json.
| Field | Type | Default / meaning |
|---|---|---|
product_name | string | Required display name |
version | string | Required application version; independent of Proton's version |
formats | string array | Host defaults when omitted |
icons | string array | Empty; icon paths relative to configuration directory |
prepare | string | Absent; preparation command |
resources | string array | Empty; additional packaged resources |
sign.binaries | string array | Empty; additional binaries selected for signing |
url_schemes | string array | Empty; registered application URL schemes |
document_types | object array | Empty; document associations |
output | string | dist, relative to configuration directory |
platforms | object | Optional macos, windows, linux overrides |
A document type contains required name and extensions; role defaults to Viewer. The canonical identifier is the top-level identifier, not a package field. Changing the product name does not change application identity.
package.sign
| Field | Type | Default / meaning |
|---|---|---|
binaries | string array | []; additional binaries to sign, relative to the configuration directory |
Omitting sign adds no extra binaries. This object does not enable signing: use package --sign or --notarize.
package.document_types[]
| Field | Type | Default / meaning |
|---|---|---|
name | string | Required document type display name |
extensions | string array | Required nonempty list of file extensions |
role | string | Viewer; document role passed to packaging |
package.platforms
| Field | Type | Default / meaning |
|---|---|---|
macos | object | Absent; macOS overrides |
windows | object | Absent; Windows overrides |
linux | object | Absent; Linux overrides |
Each platform object accepts the following fields. Unknown fields are rejected, including nsis_install_mode outside Windows.
| Field | Type | Default / meaning |
|---|---|---|
formats | string array | Shared package.formats; an explicit list replaces it |
resources | string array | []; appended to shared resources, duplicates removed |
sign | object | Absent; accepts only binaries (string array, default []), appended to shared signing inputs with duplicates removed |
nsis_install_mode | string | Windows only; currentUser (default), perMachine, or both |
minimum_system_version | string | macOS only; major.minor[.patch]. Sets LSMinimumSystemVersion; omitted when unset. Does not change compilation targets or validate binary compatibility. |
If the resolved format list is empty, the host default formats apply. CLI options override the resolved configuration. See packaging behavior for supported formats, signing and installation modes.
Configuration ownership and directory layout
Proton does not require the directory layout of either scaffold template. The CLI locates the backend, frontend and resources through this file; see the path resolution rules under Paths and resources below.
| File or entry | Responsibility |
|---|---|
| moon.mod | Module name and versioned dependencies |
| moon.pkg | Package imports, targets and executable entry declarations |
| moon.work (when using multiple modules) | Groups modules into a workspace |
| proton.project.json | Application identity, CLI build paths and packaging metadata |
| MoonBit App builder | Windows, commands, capabilities and lifecycle behavior |
A module dependency makes a module available; a package import selects the APIs used by that package. backend.package selects the build entry and does not replace Moon's package declarations.
Build output
Moon writes build artifacts under _build/ by default. Frontend output depends on the frontend build tool and should correspond to frontend.dist. frontend/dist is an isomorphic template choice, not a fixed framework path. The packager writes distributables to package.output, which defaults to dist.
These directories hold generated artifacts determined by source, build commands and project configuration. The runtime and helper installed by setup live in shared user-level storage, outside the application source tree.
Complete configuration example
The paths below are illustrative: referenced icons, resources and commands must exist in the application project.
{
"identifier": "com.example.notes",
"backend": { "path": "backend", "package": "app" },
"frontend": {
"path": "frontend",
"dev_url": "http://127.0.0.1:4300",
"before_dev": "warren dev --browser-entry main --direct --port 4300",
"before_build": "warren build --browser-entry main",
"dist": "dist"
},
"package": {
"product_name": "Notes",
"version": "1.0.0",
"formats": ["app", "zip"],
"icons": ["icons/app.icns", "icons/app.ico", "icons/app.png"],
"prepare": "node scripts/prepare.mjs",
"resources": ["resources"],
"sign": { "binaries": [] },
"url_schemes": ["notes"],
"document_types": [
{ "name": "Note", "extensions": ["note"], "role": "Editor" }
],
"output": "dist",
"platforms": {
"macos": { "formats": ["app", "dmg"] },
"windows": { "formats": ["nsis"], "nsis_install_mode": "currentUser" },
"linux": { "formats": ["appimage"] }
}
}
}
Runtime configuration
Load configuration independently, then install it on the application:
let config = @proton.load_config()
let app = @proton.html("Hello", "<h1>Hello</h1>")..config(config)
if config.frontend is Some(frontend) {
if frontend.dev_url is Some(url) {
println(url)
}
}
load_config() reads and validates the complete typed AppConfig, raising
AppConfigError immediately on failure. Development uses proton.project.json;
packaged applications use proton-package.json. For an explicit file path, use
@proton.AppConfig::load(path). Applications can also construct AppConfig directly.
Read fields from the configuration value. Nested sections are optional structs:
config.frontend contains dev_url, path, dist, before_dev, and
before_build. JSON package maps to config.package_config; JSON
backend.package maps to the backend's app_package field.
proton_cli dev supplies the effective configuration after --url, --command,
--frontend-path, and --package overrides. Packaging likewise embeds the
effective configuration, including CLI overrides for formats and platform settings.
Relative paths retain their spelling; reading a field does not resolve paths
or execute commands.
app..config(config) installs an independent copy and is mutually exclusive with
app..identifier(...). Invalid configuration or a mismatch with the packaged
identity prevents startup. Retain config to inspect it; there is no application
configuration getter. app.name() and app.version() read the installed snapshot
without further file I/O.
Minimal application
This example starts a native window from an inline HTML string. It shows the smallest application entry point: select page content, assign an application identifier, and run the application.
01_run · Source files: main.mbt
Behavior
A single 800 × 600 window displays “01 run”. The builder declares identity and starts the application.
async fn main {
let html =
#|<!doctype html>
#|<html>
#| <head><meta charset="utf-8"><title>01 run</title></head>
#| <body><h1>01 run</h1></body>
#|</html>
@proton.html("01 run", html, width=800, height=600, debug=true)
..identifier("dev.proton.01-run")
.run_or_abort()
}
Implementation
The HTML is a MoonBit value passed directly to the application builder. No frontend server, asset lookup or command bridge is needed for this page. The explicit identifier supplies application identity for direct repository execution; a scaffold instead obtains it with load_config().
Reuse this form for a small self-contained page. Move to an asset entry when the page needs independently built CSS/JavaScript files. debug=true is an example choice, not a distribution requirement.
Run
After completing source checkout and runtime setup, run from the repository root:
moon -C examples run 01_run --target native
Limits and interpretation
Use this to isolate runtime setup from frontend tooling. There is no command bridge or external resource loading in the example.
Embedded HTML
This example keeps HTML in a separate source file and embeds it in the executable as a MoonBit string. It demonstrates how to maintain page content separately without requiring a runtime HTML file.
12_embed · Source files: main.mbt, hello.html, hello.mbt, moon.pkg
Behavior
The window loads the embedded resource through html(). The package prebuild rule produces hello.mbt from hello.html.
async fn main {
@proton.html("12 embed", resource, width=800, height=600, debug=true)
..identifier("dev.proton.12-embed")
.run_or_abort()
}
Implementation
resource is generated from hello.html by the package's embed prebuild rule. The runtime receives the same kind of string as the minimal example; it does not read hello.html from disk at launch.
Edit hello.html and rebuild. Do not edit generated hello.mbt. Embedding a document does not automatically embed every relative image or script it references; use an asset entry for a directory of resources.
Run
After completing source checkout and runtime setup, run from the repository root:
moon -C examples run 12_embed --target native
Limits and interpretation
Edit hello.html, not generated hello.mbt. Embedded HTML does not automatically include sibling files; use the sidecar example for separate JS/CSS.
Filesystem capability
This example lets a renderer request host file operations through the filesystem extension. The backend grants a specific directory and operation scope; the page exercises that grant through the command bridge.
18_extension_fs · Source files: main.mbt, fs.html
Behavior
The page exposes read, write and directory operations. The native entry registers the fs capability with a PermissionRoot and an operation allowlist.
async fn main {
@proton.html("18 extension fs", resource, width=960, height=720, debug=true)
..capability(
@fs.capability([
@fs.PermissionRoot(".", [
"read_file", "write_file", "exists", "kind", "size", "readdir", "mkdir",
"remove", "rmdir", "rename", "realpath",
]),
]),
)
..identifier("dev.proton.18-extension-fs")
.run_or_abort()
}
Implementation
The backend installs the filesystem extension and chooses both its root and allowed operations. The renderer supplies a path and operation, but cannot grant itself a wider root. A missing file and an unauthorized operation are separate failure cases.
This example grants write/delete access for demonstration. In an application, narrow the list to the required operations and use a deliberate data directory. The page's HTML and JavaScript demonstrate the raw extension bridge; MoonBit frontends can use typed descriptors.
Run
After completing source checkout and runtime setup, run from the repository root:
moon -C examples run 18_extension_fs --target native
Limits and interpretation
The example grants write and deletion operations under its working directory. Use a disposable directory for experiments; production apps should grant only their required paths and operations.
Typed events
This example sends progress notifications while an asynchronous command runs. It demonstrates the separate roles of a command response and an event subscription when displaying ongoing backend work.
40_event_broadcast · Source files: main.mbt, app.html
Behavior
Starting the ticker produces tick events and a done event. The command also returns its collected result. The run_id distinguishes runs.
Implementation
Excerpt from the entry point; use the source link above for the complete context.
async fn run_ticker(
context : @proton.CommandContext,
payload : StartPayload,
) -> TickerResult {
let count = clamp(payload.count, 1, 20)
let interval_ms = clamp(payload.interval_ms, 100, 2000)
let ticks : Array[TickEvent] = []
for index in 1..<=count {
@async.sleep(interval_ms)
let tick = TickEvent::{
run_id: payload.run_id,
index,
total: count,
remaining: count - index,
}
ticks.push(tick)
context.emit(tick_event, tick)
}
let done = DoneEvent::{ run_id: payload.run_id, total: count, }
context.emit(done_event, done)
TickerResult::{ ticks, done, }
}
A typed extension binds start to run_ticker. The handler awaits between ticks and emits tick/done notifications before returning a final TickerResult. Progress events and the command result have separate roles: a displayed tick is not completion of the command.
The count and interval are clamped by the backend. Reuse the request id in your UI to distinguish overlapping runs, close subscriptions with their owner, and handle request cancellation. Despite the historical directory name, this is not a durable broadcast log.
Run
After completing source checkout and runtime setup, run from the repository root:
moon -C examples run 40_event_broadcast --target native
Limits and interpretation
Events and command responses serve different purposes. Register listeners before starting a run; an event is not a durable history or an acknowledgement.
Multi-window commands
This example exposes command operations to two windows in one application. It shows how window declarations, renderer grants and caller identity work together without giving every page implicit access.
45_bridge_multi_window · Source files: main.mbt
Behavior
The identify command returns both the supplied label and CommandContext.window_id(). Calls from different windows retain their originating window identity.
Implementation
Excerpt from the entry point; use the source link above for the complete context.
async fn main {
@proton.html(
"Bridge Multi A",
page_html("A"),
width=520,
height=420,
debug=true,
)
..add_window(
"secondary",
@proton.WindowConfig(
"Bridge Multi B",
@proton.AppEntry::Html(page_html("B")),
width=520,
height=420,
),
open_on_start=false,
)
..capability(@proton_extension.capability(multi_window_extension()), targets=[
@proton.RendererTarget::entry(),
@proton.RendererTarget::entry(window="secondary"),
])
..app_lifecycle(
on_start=async fn(context) { ignore(context.windows().open("secondary")) },
on_shutdown=fn(_state) { },
)
..identifier("dev.proton.45-bridge-multi-window")
.run_or_abort()
}
One extension implementation serves two independently authorized renderer targets. The secondary declaration has open_on_start=false and is opened explicitly in the application start hook. The handler returns context.window_id(), proving that caller identity comes from the host context rather than the page's label.
Reuse shared business handlers, but list each intended target explicitly. Keep per-window UI state on the frontend and shared state in the host. Delayed calls can overlap; do not infer response ownership from whichever window currently has focus.
Run
After completing source checkout and runtime setup, run from the repository root:
moon -C examples run 45_bridge_multi_window --target native
Limits and interpretation
The optional delay_ms makes overlapping requests visible. A logical window name identifies an application window, not a globally reusable native handle.
HTML with sidecar assets
This example loads an HTML entry together with separate JavaScript, CSS and worker files. It demonstrates the resource layout needed when a page is served from application assets rather than a development server.
46_asset_sidecar_resources · Source files: main.mbt, app/app.html, app/app.js, app/app.css, app/worker.js
Behavior
asset() loads app.html with its sibling resources. The example also contains a second page for navigation.
async fn main {
let app = @proton.asset(
"Asset Sidecar Resources",
"46_asset_sidecar_resources/app/app.html",
width=760,
height=500,
debug=true,
)..capability(@proton_extension.capability(extension()))
app..identifier("dev.proton.46-asset-sidecar-resources").run_or_abort()
}
Implementation
The asset entry names an HTML document within the resource tree. Its neighboring scripts, styles and worker source remain separate files, so their relative URLs must remain valid after packaging. The extension registration is independent of asset selection.
When adapting this example, keep the resource tree layout and configure packaging to include it. Verify a packaged application with the development server stopped. A working development URL does not demonstrate that sidecar files were packaged.
Run
After completing source checkout and runtime setup, run from the repository root:
moon -C examples run 46_asset_sidecar_resources --target native
Limits and interpretation
Run from the examples directory so the relative asset path resolves correctly. Deployments must package the complete resource tree, not only the HTML file.
Native application menu
This example builds a native application menu and updates its command state while the window is running. It connects menu actions to a live window handle and releases that reference when the window closes.
49_app_menu · Source files: main.mbt
Behavior
Menu actions update enabled, visible and checked state. Native menu roles and application commands are declared in MoonBit.
Implementation
Excerpt from the entry point; use the source link above for the complete context.
async fn main {
@proton.html(
"App Menu State Review",
html(),
width=900,
height=640,
debug=true,
)
..menu(menu_bar())
..identifier("dev.proton.49-app-menu")
..commands(register_commands)
..window_lifecycle(
on_ready=context => {
let window = context.handle()
window_slot.val = Some(window)
window
},
on_close=fn(_window) { window_slot.val = None },
)
.run_or_abort()
}
The menu is native UI configured on the builder. Command handlers change or inspect its state through a live window handle. The ready hook stores that handle; the close hook clears it so later commands cannot deliberately reuse a closed instance.
Keep stable menu command ids separate from translated labels. Native role items delegate standard platform behavior, while application command items need application handling. For multiple windows, replace the example's single window slot with explicit ownership per window.
Run
After completing source checkout and runtime setup, run from the repository root:
moon -C examples run 49_app_menu --target native
Limits and interpretation
Menu placement and supported roles depend on the desktop platform. A menu command may have no focused window; handlers must account for that.
Embedded browser view
This example places a child browser beside a sidebar in the main window. It demonstrates the distinction between the main page and a separately navigable browser view, including explicit bounds for the child.
53_view_minimal · Source files: main.mbt
Behavior
WindowConfig.views adds a browser at x=288 with an 832 × 720 viewport. It loads example.com separately from the host HTML.
Implementation
Excerpt from the entry point; use the source link above for the complete context.
async fn main {
@proton.App(
@proton.WindowConfig(
"Minimal View",
@proton.AppEntry::Html(sidebar),
width=1120,
height=720,
views=[
(
"browser",
@proton.view("https://example.com/", width=832, height=720, x=288),
),
],
),
)
..debug(enabled=true)
..identifier("dev.proton.53-view-minimal")
.run_or_abort()
}
The main browser renders the sidebar; WindowConfig.views creates a second browser inside the same native window. x=288 reserves the sidebar width, and the child loads example.com independently. This is useful for a host shell with an embedded page, rather than a separate top-level window.
The example intentionally has fixed geometry. A production layout must recompute child bounds after resizing and decide how remote navigation is handled. Granting commands to the local host page does not mean remote child content should receive the same capabilities.
Run
After completing source checkout and runtime setup, run from the repository root:
moon -C examples run 53_view_minimal --target native
Limits and interpretation
The remote page requires network access. Bounds are fixed in this minimal example; responsive view layout belongs to the application. Parent shutdown ends the child lifetime.
Application locale
This example sets the application locale at startup and observes it through commands, Chromium and native menus. It illustrates the framework locale setting; translating application-owned text remains the responsibility of the application.
56_i18n · Source files: main.mbt, app.html
Behavior
The app selects zh-CN and displays backend preferences alongside navigator.language and navigator.languages.
Implementation
Excerpt from the entry point; use the source link above for the complete context.
async fn main {
let locale = @proton.Locale::parse("zh-CN") catch {
error => abort(error.message())
}
@proton.html("Proton 国际化", resource, width=860, height=600, debug=true)
..locale(locale)
..capability(@proton_extension.capability(i18n_extension()))
..identifier("dev.proton.56-i18n")
.run_or_abort()
}
The application explicitly parses zh-CN and passes it to locale(). The extension reads the resulting locale preferences from its command context; Chromium and native framework labels use the same startup choice. Windows uses a separate entry variant without the menu declared for other hosts.
This selects framework/browser locale; it does not translate arbitrary application strings. Keep application text in your own catalog. Locale is a startup choice in this example, not a live language-switch implementation.
Run
After completing source checkout and runtime setup, run from the repository root:
moon -C examples run 56_i18n --target native
Limits and interpretation
Proton propagates locale; it does not translate application text. This example omits native menus on Windows; do not infer menu parity from its other platforms.
Background residency
This example combines background residency with single-instance activation. Closing the last window keeps the application alive, so a later launch can reach the existing instance instead of starting another runtime.
57_background_residency · Source files: main.mbt
Behavior
KeepRunning retains the process. A second launch delivers Reopen; the handler recreates main only when it is absent.
Implementation
Excerpt from the entry point; use the source link above for the complete context.
async fn main {
@proton.html("Background Residency", page, width=760, height=480)
..identifier("com.example.proton.background-residency")
..single_instance()
..last_window_closed_policy(@proton.LastWindowClosedPolicy::KeepRunning)
..on_launch_input(async fn(context, input) noraise {
if input is Reopen && context.windows().find("main") is None {
ignore(context.windows().open("main")) catch {
error => println("failed to reopen main window: " + error.to_string())
}
}
})
.run_or_abort()
}
KeepRunning changes last-window behavior, while single_instance routes later launches into the existing process. On Reopen, the callback first checks whether main exists and opens a new instance only when absent. Closing a window and quitting the application are deliberately different operations.
A real background application needs an accessible explicit Quit action, such as a tray command. Stopping the development process demonstrates forced termination, not successful lifecycle cleanup. Reopen obtains a fresh window instance; do not keep the previous handle.
Run
After completing source checkout and runtime setup, run from the repository root:
moon -C examples run 57_background_residency --target native
Limits and interpretation
Closing the window intentionally does not quit. End the process using the platform Quit action or stop the development command. This differs from the default last-window policy.
Release Notes
0.4.0 (Unreleased)
0.4.0 unifies window and browser APIs, shares typed configuration between the application and CLI, and coordinates update installation with application shutdown. This release includes breaking API changes.
Breaking API changes and migration
| Area | 0.3.4 | 0.4.0 |
|---|---|---|
| Window configuration | Separate primary-window setters and add_window(id, title, entry, ...) | WindowConfig shared by App(config), main_window(config) and add_window(id, config, ...) |
| Initial child views | App.with_view(...) | WindowConfig.views, for primary or secondary windows |
| Browser operations | window.browser() and page methods directly on ViewHandle | window.web_contents() and view.web_contents(), both returning WebContentsHandle |
| Browser events | BrowserEvent, ViewEvent, separate subscriptions | WebContentsEvent and on_web_contents_event; use window_id() and optional view_id() for identity |
| Sessions | Browser-owned handle, SessionHandle.window_id(), WindowSessionError | Application-owned handle, context.session(), SessionError; no window_id() |
Request number plus PdfPrinted / PdfPrintResult | Async web_contents.print_to_pdf(path, options?) -> Unit | |
| Configuration | app.load_config(), ProjectConfig, AppMetadataError | app.config(@proton.load_config()), typed AppConfig, AppConfigError |
| Updates | update.install(); update.restart() | update.download(); context.quit_and_install() |
- Window configuration: move
title,size,theme,titlebar_style,traffic_light_position, andentry_*builder settings intoWindowConfig.html,url,file, andassetremain shortcuts; live native-window controls remain onWindowHandle. Window registration copies the initial views array, and secondary-window reopening recreates declared children. #417 - Web contents:
WebContentsHandlebecomes an opaque common handle rather than an enum to unpack. Navigation, download, and permission callbacks receive it too. Move window zoom and child-page operations to this handle; layout, visibility, z-order and removal stay on the view. Child pages gain printing, downloads and session access, but do not gain the application command bridge. Request completions remain associated with the originating page, and macOS main-page title updates are delivered correctly. #418 - Session lifetime: cookies, cache, authentication, certificate exceptions and connection operations use the shared application request context. Handles work before windows open and after pages close, including
KeepRunningwith no windows. Concurrent cookie reads are independent; runtime teardown wakes pending readers and invalidates retained handles. Update exception handling toSessionError. This remains one shared profile, not per-window sessions. #419 - PDF completion: call
print_to_pdfin an async context and handle completion directly; remove request/event correlation. Page closure, renderer termination and application shutdown end outstanding waits. Cancellation stops waiting, not the native print job, so a file may still be produced. #433 - Typed configuration:
@proton.load_config()reads the complete configuration once; fields such asconfig.frontend.dev_urlremain directly accessible.App.config(config)keeps an independent snapshot, andname()/version()read it without reopening files. CLI development overrides are included in the launch snapshot. Packagedproton-package.jsonnow has the complete configuration shape with nestedpackage; rebuild application and packaged metadata together. Rabbita and scaffold defaults advance to 0.16.4, removing the deprecated moonback dependency. #435 - Update lifecycle: downloading verifies and retains the artifact without replacing files. Installation follows cancelable quit and successful runtime cleanup. Denying quit retains the download; ordinary quit discards it; forced exit supersedes installation. A successful handoff terminates the old process even with exit code zero.
quit_and_install()returns on request acceptance, not replacement startup. The extension response is nowUpdateDownloaded, without the oldchangedfield. #436
Fixes
- Cancelling the task running
App.run()now cleans up native windows, browsers and helpers during both startup and normal execution. Cancellation still propagates to the caller. #414 - Linux and macOS packaging correctly copies files and directories whose paths begin with
-, preserving executable permissions. #415 - Update installation retains single-instance ownership while replacing application files. Automatic Windows updates wait for the original process to exit instead of forcibly terminating it. macOS relaunch works when the new version changes its executable name. macOS/Linux applications with an update channel reject delayed starts of the replaced executable. #436
Upgrading
Keep the CLI and application proton_* dependencies on the same version, then run proton_cli cef setup for the matching helper. Installing the new CLI does not migrate existing application code. Rebuild packaged applications to generate the new configuration format.
Rabbita is updated to 0.16.4. Warren remains 0.3.3 and async remains 0.22.4; the CEF version is unchanged. See application APIs, configuration and the complete changelog.
0.3.4 — 2026-09-28
- Lifecycle: application quit interception and forced exit, application-level window/browser/session events, and focus/visibility controls. Fixes cover forced exit during
will_quit,exit(0), child-renderer termination, page recovery, and closed-window collection. - Communication: preserve command error codes, separate command failures from runtime failures, and remove the global pending-request limit. Extension operation inventories now come from bindings.
- Browser APIs: navigation history and inserted CSS controls; task metrics distinguish CEF tasks from processes.
- Platforms and packaging: Windows desktop media consent and child-view geometry fixes; macOS query autorelease pools and alert layout; recoverable packaging locks, explicit minimum macOS version metadata, and executable Linux helpers in AppImage staging.
- CLI: explicit
newoptions no longer prompt again. Async is updated to 0.22.4.
Upgrade notes
Keep the CLI and application proton_* modules on 0.3.4 and run proton_cli cef setup for the matching helper. Reinstalling the CLI does not rewrite existing projects.
The briefly introduced app_metrics API is now task_metrics with AppTaskMetric: task identity is not process identity, and process_cpu_percent / process_memory_bytes can repeat across tasks. Do not sum task rows as application resource usage. See PR #412. Unused legacy command configuration APIs were removed in PR #408; application command bindings remain the supported route.
0.3.3 — 2026-09-21
- View lifecycle: removed and explicitly closed child views retain the identity needed to deliver their terminal close event. Coverage includes views closed before browser creation and stale events after replacement. PR #378
- Dependencies: async 0.22.1, x 0.5.5, lexer/parser/moon_config 0.4.0, Rabbita 0.16.2 and Warren 0.3.3. Scaffold defaults and documentation now use these versions. Cancellation handling and protected cleanup follow async's new semantics. PR #379
Updating an application
Use 0.3.3 consistently for the application's proton_* dependencies and CLI. Existing projects retain their configuration and source; installing a new CLI does not rewrite them. Install Warren 0.3.3 when using the isomorphic frontend, and run proton_cli cef setup for the matching helper before building.
Applications that directly use async should review its 0.22 cancellation behavior: cancellation is no longer caught as an ordinary error. A cancelled task's wait() can report TaskCancelled; child process shutdown can complete with an exit result after reaping.
Earlier releases
0.3.2 changes and repository history contain earlier changes.