Custom URL schemes that open the app (MOB-379): config :mob_dev, url_schemes: ["myapp"] in mob.exs. mob delivers the URL to the BEAM as
{:link, %{url: url, source: :launch | :running}}; this module only makes the
OS route the URL to the app.
The build stamps the setting into native files every time, so a changed or removed setting takes effect on the next native build of new and existing apps alike:
- Android — a managed
<intent-filter>(android.intent.action.VIEW, categoriesDEFAULT+BROWSABLE, one<data android:scheme>per scheme) inside the launcher activity ofandroid/app/src/main/AndroidManifest.xml, fenced bymob:url-schemesmarkers (MobDev.Plugin.ManagedBlock). The dev build (mix mob.deploy --native) andmix mob.release --androidregenerate it; unset,nilor[]removes it. - iOS — a
CFBundleURLTypesentry (CFBundleURLName= the iOS bundle id,CFBundleTypeRoleViewer) appended to the built bundle's Info.plist by the simulator and device builds andmix mob.release --ios.ios/Info.plistis never rewritten.
A scheme the project already routes to the app is left to it. On Android
that is a VIEW + DEFAULT + BROWSABLE intent filter on the launcher
activity, outside the managed block, declaring the scheme with no host,
path or other narrowing. A scheme in a VIEW filter on another activity is
still added to the launcher, with a build warning: Android may then ask
which activity opens the link. On iOS it is a scheme in any existing
CFBundleURLTypes entry, compared case-insensitively as iOS does. The app's
own entries are never changed.
Schemes must be lowercase RFC 3986 schemes. http and https are refused:
verified App Links and universal links need a host and domain verification,
which a bare scheme can't express.
The launcher activity must be android:launchMode="singleTask" (or
singleInstance) when url_schemes is set; the build refuses anything
else rather than rewrite launchMode. Otherwise a link opened from another
app's task starts a second MainActivity there, and two activities drive
one BEAM. mob_new's template uses singleTop, since singleTask also
finishes the activities stacked above MainActivity whenever the app is
reopened from its icon, so an app that takes deep links opts in.
See decisions/2026-10-03-url-schemes.md.
Summary
Types
One mix mob.doctor row.
Functions
Applies merge_manifest/2 with mob.exs's schemes to the manifest at
path, writing only when it changed. With url_schemes unset the merge
still runs, which removes a block an earlier build added. Raises Mix.Error
on an invalid setting, when schemes are set and path doesn't exist, or
when schemes are set and launch_mode_error/2 objects to the launcher
activity.
Adds mob.exs's schemes to the Info.plist at path with PlistBuddy
(macOS), under CFBundleURLName url_name. Leaves the file untouched when
url_schemes is unset or every scheme is already declared. Raises when any
command fails.
The mix mob.doctor row for mob.exs's url_schemes and the text of the
project's AndroidManifest.xml (nil when there is none): none when
url_schemes is unset or empty, :ok listing the schemes, or :fail on a
value the build refuses or a launcher activity it refuses
(launch_mode_error/2).
plist_commands/3 for mob.exs's schemes and the Info.plist at path (any
plist format; read with macOS plutil). [] without reading the file when
url_schemes is unset or empty. Raises Mix.Error on an invalid setting.
The schemes merge_manifest/2 adds that a VIEW intent filter on another
activity also declares. Android may then ask the user which activity opens
the link, so the build warns about each.
Why the launcher activity in manifest (the text of the file at path)
can't take deep links, or nil when it can: its android:launchMode must
be singleTask or singleInstance. For an <activity-alias> launcher
that is the launch mode of its android:targetActivity, matched against
<activity android:name> as Android resolves names (.X, X and
<package>.X name the same activity; an exact spelling wins). A manifest
with no launcher activity, or an alias with no or an undeclared target, is
an error too.
Regenerates the managed deep-link <intent-filter> inside the launcher
activity (the one with a MAIN + LAUNCHER intent filter; the first, if
several), just before its </activity>. A scheme the launcher activity
already routes in full outside the managed block is skipped: one in a
VIEW filter with the DEFAULT and BROWSABLE categories whose <data>
elements set no android: attribute besides android:scheme. A filter
narrowed by a host, port, path, ssp or MIME type, one missing a category,
or one on another activity doesn't count. With nothing left to declare the
block is removed.
Idempotent. Raises Mix.Error when schemes is non-empty and there is no
launcher activity, or its </activity> shares a line with other markup (the
managed block occupies whole lines).
The PlistBuddy commands that add schemes to an Info.plist whose XML text
is xml: one new CFBundleURLTypes entry named url_name, role Viewer
(Apple requires CFBundleTypeRole in each entry), appended after the
plist's own entries (creating the array when absent), holding the schemes
the plist doesn't already declare. [] when there is nothing to
add. Every command must succeed. Raises Mix.Error when xml isn't an XML
property list or its CFBundleURLTypes isn't an array.
Validates mob.exs url_schemes. Unset, nil (read as unset, like
MobDev.IosLayoutPlist's keys) and [] are {:ok, []} (no deep-link
scheme); duplicates are dropped, order kept.
Types
One mix mob.doctor row.
Functions
Applies merge_manifest/2 with mob.exs's schemes to the manifest at
path, writing only when it changed. With url_schemes unset the merge
still runs, which removes a block an earlier build added. Raises Mix.Error
on an invalid setting, when schemes are set and path doesn't exist, or
when schemes are set and launch_mode_error/2 objects to the launcher
activity.
Adds mob.exs's schemes to the Info.plist at path with PlistBuddy
(macOS), under CFBundleURLName url_name. Leaves the file untouched when
url_schemes is unset or every scheme is already declared. Raises when any
command fails.
The mix mob.doctor row for mob.exs's url_schemes and the text of the
project's AndroidManifest.xml (nil when there is none): none when
url_schemes is unset or empty, :ok listing the schemes, or :fail on a
value the build refuses or a launcher activity it refuses
(launch_mode_error/2).
plist_commands/3 for mob.exs's schemes and the Info.plist at path (any
plist format; read with macOS plutil). [] without reading the file when
url_schemes is unset or empty. Raises Mix.Error on an invalid setting.
The schemes merge_manifest/2 adds that a VIEW intent filter on another
activity also declares. Android may then ask the user which activity opens
the link, so the build warns about each.
Why the launcher activity in manifest (the text of the file at path)
can't take deep links, or nil when it can: its android:launchMode must
be singleTask or singleInstance. For an <activity-alias> launcher
that is the launch mode of its android:targetActivity, matched against
<activity android:name> as Android resolves names (.X, X and
<package>.X name the same activity; an exact spelling wins). A manifest
with no launcher activity, or an alias with no or an undeclared target, is
an error too.
Regenerates the managed deep-link <intent-filter> inside the launcher
activity (the one with a MAIN + LAUNCHER intent filter; the first, if
several), just before its </activity>. A scheme the launcher activity
already routes in full outside the managed block is skipped: one in a
VIEW filter with the DEFAULT and BROWSABLE categories whose <data>
elements set no android: attribute besides android:scheme. A filter
narrowed by a host, port, path, ssp or MIME type, one missing a category,
or one on another activity doesn't count. With nothing left to declare the
block is removed.
Idempotent. Raises Mix.Error when schemes is non-empty and there is no
launcher activity, or its </activity> shares a line with other markup (the
managed block occupies whole lines).
The PlistBuddy commands that add schemes to an Info.plist whose XML text
is xml: one new CFBundleURLTypes entry named url_name, role Viewer
(Apple requires CFBundleTypeRole in each entry), appended after the
plist's own entries (creating the array when absent), holding the schemes
the plist doesn't already declare. [] when there is nothing to
add. Every command must succeed. Raises Mix.Error when xml isn't an XML
property list or its CFBundleURLTypes isn't an array.
Validates mob.exs url_schemes. Unset, nil (read as unset, like
MobDev.IosLayoutPlist's keys) and [] are {:ok, []} (no deep-link
scheme); duplicates are dropped, order kept.