Deep links
Deep links are inbound lifecycle events. The host receives a URL from the operating system, browser, launcher, notification, or store association, checks that it matches your declared routes, and dispatches a DeepLinkReceived action into the app.
By the end of this guide, you will know how to build a route handler that opens a document when the app starts from fission://document/42. The same pattern applies to larger apps: declare the capability when native configuration is needed, store the result in typed app state, request the host operation from a reducer, and test both success and failure with a memory host.
Do not parse process arguments or platform URL callbacks inside arbitrary widgets. Keep route ownership in the shell setup and reduce the typed DeepLinkReceived action into normal app state.
1. Start with the state the screen needs
Add the smallest state fields the screen needs to render honestly. For this flow, useful state is:
#[derive(Default)]
struct DeepLinkState {
// Add these fields to your existing app state.
pending_route: Option<String>,
route_error: Option<String>,
}
impl GlobalState for DeepLinkState {}
State is the source of truth for the UI. Do not store whether the host "probably" succeeded; wait for the typed success action and store the returned value.
There is no single safe add-capability value for this feature because the product owns the route/provider details, or because the default host can expose the feature without generated permission files. You still configure the shell explicitly and review target packaging before release.
Deep links are product routes, so the app must name them explicitly. Android uses intent filters. iOS and macOS use URL schemes or associated domains. Windows uses protocol activation or package metadata. Web and static sites use ordinary URL paths. Fission can store the route intent in fission.toml, but you still review the generated native files because route names and domains are product-owned.
This is the difference between the Fission API, a provider, and packaging configuration:
The Fission API is the typed Rust contract your reducer calls, such as ctx.effects.camera().capture_photo(...).
The provider is the shell-side Rust implementation that turns that typed request into the correct OS, browser, or hardware call.
Packaging configuration is the manifest, plist, entitlement, service-worker, protocol, domain, or store metadata that lets the platform permit that call in a real installed app.
If the API exists but the provider is missing, the request returns a typed unsupported error. If the provider exists but packaging is missing, the operating system or browser may deny the request before the provider can complete it.
3. Define reducers for success and failure
Capability work finishes later, after the reducer that started it has returned. You therefore need one reducer for the successful result and one reducer for the error path.
fn on_deep_link(state: &mut DeepLinkState, action: DeepLinkReceived) {
if let Some(id) = action.link.url.strip_prefix("fission://document/") {
state.pending_route = Some(format!("document:{id}"));
state.route_error = None;
} else {
state.route_error = Some(format!("Unknown link: {}", action.link.url));
}
}
Keep both paths explicit. A denied permission, unavailable device, unsupported host, or cancelled prompt is not an exceptional mystery; it is normal product behavior the UI should explain.
4. Request the capability from a reducer
Call the capability from the reducer that handles the user's intent. The reducer queues a host effect and returns. The runtime later dispatches either the success reducer or the failure reducer.
DesktopApp::new(App)
.with_deep_link_config(
DeepLinkConfig::new()
.scheme("fission")
.domain("example.com")
.path_prefix("/document"),
)
.on_deep_link(reduce_with!(on_deep_link));
The call does not run the OS API directly. It records a typed host request. That keeps shared app logic portable across Fission targets where the capability applies, plus static-site previews and tests.
5. Render from the routed state
Deep links are inbound, so there is no button to wire for the route itself. The reducer stores route state, and normal widgets render from that state.
struct CurrentRoute;
impl From<CurrentRoute> for Widget {
fn from(component: CurrentRoute) -> Self {
let (ctx, view) = fission::build::current::<DeepLinkState>();
match view.state().pending_route.as_deref() {
Some(route) if route.starts_with("document:") => {
DocumentScreen::new(route.to_owned()).into()
}
Some(_) => Text::new("This link is not supported yet").into(),
None => HomeScreen.into(),
}
}
}
That is the important split: the shell accepts only configured URLs, the reducer turns an accepted URL into app state, and the widget tree renders from that state like any other route.
6. Provide and test the host behavior
Deep links use DeepLinkConfig, DeepLinkReceived, and the shell startup path rather than a provider trait. Use .with_startup_deep_link(...) in tests or custom launchers to simulate a cold-start route.
Test DeepLinkConfig::matches, cold-start links, warm links, and unknown routes. A reducer test should prove that a valid URL selects the expected screen and an invalid URL leaves the app in a safe state.
A useful first test creates the app with the memory host, dispatches the start action, feeds the queued effect through the host, and asserts that the reducer updates state from the returned payload. Add a second test for the error path before you ship the screen.
Mobile platforms require explicit scheme or associated-domain metadata before the system will launch the app. Desktop support depends on package registration. Web routes work when the server or static host serves the generated page for the requested path.
For a cross-platform summary, read the capability matrix. For exact request fields, provider traits, and generated configuration, see the Deep links reference.
Finished capability flow
A complete Deep links integration has all of these pieces in place:
| |
|---|
| fission.toml declares the capability and generated platform files contain the permission text, manifest entry, entitlement, domain, or protocol metadata the target needs. |
| App state records the in-flight flag, the successful value, and the user-facing error separately. |
| One reducer starts the host request, one handles success, and one handles denial, unsupported hosts, cancellation, or provider errors. |
| The shell registers a real provider for production and a memory provider for tests. |
| At least one test covers success and one covers permission denial or unsupported host behavior. |
Diagnose capability failures
Capability failures should be safe to retry and easy to explain. Start by checking fission.toml, then run fission doctor, then inspect the generated target files. If the provider is missing, the request should return an unsupported error. If the provider exists but the platform configuration is wrong, the OS or browser will usually deny the request before the provider can complete it. Keep those cases distinct in the UI so users know whether they can retry, change settings, connect hardware, or choose another path.