Crosswake Install Guide
View SourceCrosswake keeps one primary package surface: crosswake. Companion-ready and
docs-only surfaces stay explicit, but there is still one primary install path for the
Phoenix host and one scaffold-once path for host-owned native shells.
Current setup answer: mix.exs owns the package version and dependency ranges;
the generated support matrix is the canonical public projection
of support and proof posture. Install the package, declare one route owner, and run
doctor before claiming a native target.
Start with guides/route_policy.md for route-owner decisions and guides/web_to_mobile_migration.md for an operational Phoenix route inventory pass. See guides/adoption.md for offline-sync architecture context and the rationale for the generated shell pattern.
mix crosswake.install- Declare at least one Phoenix route with
crosswake: [...]policy metadata mix crosswake.doctormix crosswake.gen.shell ios|androidwhen the host starts claiming a native targetbash script/verify_phase5_example_hosts.shmix crosswake.doctor --native-checks
The generated Phoenix and native files are host-owned, reviewable, and editable
after generation. Crosswake owns the DSL, manifest contract, doctor surface, support
matrix, and release-boundary policy. It does not re-own your app files after scaffolding.
Package Surface
There is one primary crosswake package plus explicitly bounded companion-ready and
docs-only surfaces. Package class does not override route ownership.
If you are deciding whether your app pressure is mostly LiveView, one explicit native route, or one honest offline island, read guides/adopter_profiles.md for the Phoenix SaaS Portal, Selective Native Flow, and Local-First Study Flow profiles and guides/web_to_mobile_migration.md for the route inventory pass before drilling into the deeper shell, offline, or pack contracts. Use guides/route_policy.md as the owner-decision map.
Add Crosswake To Your Dependencies
Add crosswake to the deps in your Phoenix host's mix.exs:
def deps do
[
{:crosswake, "~> 0.2"}
]
endThen fetch it:
mix deps.get
Step 1: Install Crosswake Into A Phoenix Host
Run:
mix crosswake.install
What it does:
- patches
lib/<app>_web/router.exwith explicit Crosswake marker lines - keeps LiveView router imports compatible with the Crosswake
livemacro - patches
lib/<app>_web/endpoint.exwith the static plug block that serves the bridge hook - generates a host-owned policy module at
lib/<app>_web/crosswake/policy.ex - writes
priv/crosswake/install_manifest.jsonso later tooling can inspect what Crosswake created or reused - prints the layout wiring it deliberately does not patch (see Step 1b)
- points follow-up diagnostics at
mix crosswake.doctor,guides/compatibility.md,guides/support_matrix.md,guides/native_shell.md,guides/bridge.md, andguides/packs.md
Repeated installer runs are idempotent. Existing marker blocks are reused instead of duplicating router edits, and existing host-owned policy files are left alone.
The installer reads the declared MyAppWeb.Router module from the router source. It
does not derive module capitalization from the OTP app name, so an app created with
--module AcmeShop keeps AcmeShopWeb.Router and
AcmeShopWeb.Crosswake.Policy even when its OTP app is :acmeshop.
LiveDashboard in development
Crosswake and Phoenix.LiveDashboard.Router both wrap Phoenix route macros. The
dashboard macro imports its own get/4 and live/4 inside the development block, so
exclude the two conflicting Crosswake arities in that same lexical block:
if Application.compile_env(:my_app, :dev_routes) do
import Crosswake.Router, except: [get: 4, live: 4], warn: false
scope "/dev" do
pipe_through :browser
live_dashboard "/dashboard", metrics: MyAppWeb.Telemetry
end
endThe exclusion is local to the dashboard block. Product routes outside it continue to use Crosswake's route macros and policy metadata.
Step 1b: Wire The Bridge Hook
The client half of the bridge is one dependency-free ESM file that ships from
Crosswake's own priv/static/. It is library-owned on purpose: its single most
important job is reporting the "no native transport is reachable" fact, which is what
turns a missing shell into an honest typed denial instead of silence. Crosswake does
not generate a host-owned copy of it and does not publish it to npm — a second copy or
a second registry would open a second version axis on exactly the code that decides
whether the fail-closed contract holds.
mix crosswake.install patches the part that is mechanical (the endpoint plug) and
prints the part that is not. Run mix crosswake.gen.bridge_hook at any time to see all
three fragments again — it refuses to generate anything and prints the wiring instead:
# lib/<app>_web/endpoint.ex — patched for you by mix crosswake.install
# crosswake:install:start
plug(Plug.Static,
at: "/crosswake",
from: :crosswake,
gzip: false,
only: ~w(crosswake.esm.js tokens.css)
)
# crosswake:install:end// your layout's module script — yours to place
import {CrosswakeBridge} from "/crosswake/crosswake.esm.js";
const liveSocket = new LiveSocket("/live", Socket, {params, hooks: {CrosswakeBridge}});Phoenix's esbuild profile must preserve that application-served absolute import and
emit ESM. Add both arguments to the existing args list in config/config.exs:
~w(js/app.js --bundle --target=es2022 --outdir=../priv/static/assets \
--external:/crosswake/* --format=esm)Without --external:/crosswake/*, esbuild tries to resolve the URL as a source file.
Without --format=esm, the external absolute import cannot remain in the emitted
module in the form the browser expects.
<%!-- ONE element per page. Client events broadcast to EVERY mounted hook, so a
second element would post every bridge request to the shell twice. --%>
<div id="crosswake-bridge" phx-hook="CrosswakeBridge" phx-update="ignore"></div>If you genuinely need a host-owned copy, mix crosswake.gen.bridge_hook --eject writes
one carrying a protocol-version stamp. You own it from that moment on, and
mix crosswake.doctor warns when its stamp falls behind
Crosswake.Bridge.Contract.version/0.
You must attach the bridge before you push
This is the one new install-time failure surface, and every adopter hits it exactly
once. Crosswake.Bridge.push/3 raises Crosswake.Bridge.NotMountedError on a socket
that never called Crosswake.Bridge.attach/1. Crosswake never guesses a route id, so a
missing attach is a named, loud, first-push failure rather than a silent no-op.
def mount(_params, _session, socket) do
socket =
socket
|> assign(crosswake_manifest: MyAppWeb.Crosswake.Policy.manifest(), crosswake_route_id: "my-route")
|> Crosswake.Bridge.attach()
{:ok, socket}
endattach/1 requires :crosswake_manifest and :crosswake_route_id to already be
assigned. on_mount: Crosswake.Bridge works too, as long as it runs after whatever
hook assigns those two. See guides/bridge.md for push/3, the reply
handle_info/2 clause, and resolve/2.
mix crosswake.doctor greps your assets tree and your HEEx templates for the hook and
names it when it finds nothing. That grep is best-effort and never authoritative — it
cannot see a hook registered through a bundler alias. The authoritative detector is the
runtime wiring deadline: the server arms it on every push and delivers a
:shell_unreachable denial when no acknowledgement arrives.
Step 2: Generate Host-Owned Native Shells
Run one of:
mix crosswake.gen.shell ios
mix crosswake.gen.shell android
What it does:
- creates a host-owned shell project under
native/<platform>/crosswake_shell - bundles the canonical manifest, activation, route-unavailable, and declared-pack fixtures
- scaffolds manifest-first activation, explicit route unavailable UI, and the bounded bridge channel
What it does not do:
- it does not safely regenerate over host edits
- it does not turn unsupported routes into generic container fallback
- it does not widen support beyond explicit, proof-backed surfaces
Read guides/native_shell.md for the shell contract and guides/bridge.md for the bounded bridge contract.
Native Evidence Labels
generated public-coordinate proofis the default non-local shell generation path.local-dev proofis the explicit--localmaintainer path.checked-in public-coordinate proofis the label for the checked-in iOS and Android host proof surfaces.- None of those labels imply simulator, emulator, or physical-device support.
Step 3: Verify Host And Manifest Truth
Run after installation and the first managed route declaration:
mix crosswake.doctor
Doctor discovers the router from priv/crosswake/install_manifest.json. --router
remains an override for nonstandard or pre-installer hosts.
Before a native shell exists, doctor automatically reports native support as
not_claimed; missing iOS/Android proof hooks do not fail the command. Use
--native-targets none to make that posture explicit in CI. Once a shell directory or
proof hook exists, auto treats that platform as claimed and proof becomes blocking.
You can also select the claim directly with --native-targets ios, android, or
all.
Doctor still checks install state, manifest truth, bounded bridge truth, release policy posture, and fail-closed denial vocabulary in the pre-shell posture.
Step 4: Run The Public Proof Lane
Run:
bash script/verify_phase5_example_hosts.sh
This is the primary proof artifact class Crosswake publishes for adopters.
Step 5: Re-Run Local Generated-Host Verification
Run:
mix crosswake.doctor --native-checks
With --native-checks, doctor executes the generated-host verification hooks:
script/verify_generated_ios_shell.shscript/verify_generated_android_shell.sh
Do I need to rebuild?
Use the four public change classes first, before raw version numbers:
docs-only-> read the updated guidance and run docs integrity onlycore-only/no native rebuild-> update core and rerun core contract + doctor/support proofcompatibility-bump only-> confirm your compatibility window and run fail-closed compatibility fixturesnative or companion rebuild required-> rebuild the affected shell or companion and rerun generated-shell or companion verification lanes
The matching proof lanes are:
- docs integrity only
- core contract + doctor/support proof
- fail-closed compatibility fixtures
- generated-shell or companion verification lanes
The generated support matrix remains authoritative: guides/support_matrix.md
See the canonical action-class table at guides/support_matrix.md#action-classes
and Promotion rules at guides/support_matrix.md#promotion-rules.
Promotion rules keep advisory support explicit: StoreKit/Play Billing seams in v3.7 emit reconciliation evidence only, backend projection grants authority, provider/device proof remains advisory unless promotion criteria pass, Sigra session-authority route evaluation, Phase 55 handoff ticket/server-record contracts, Phase 56 step-up intent plus Plug/LiveView ceremony, Phase 57 OAuth/passkey/native auth-return boundary contracts, and Phase 58 telemetry/security closeout are shipped, refresh-token helpers, provider/device auth proof, provider templates, passkey SDK wrappers, direct shell/WebView token authority, and native auth UI are deferred, notification-token readiness is provider-snapshot only, and standalone native shell core packages are published through SwiftPM and Maven Central at the Crosswake package version.
compatibility-window narrowing is distinct from a native rebuild; it can be compatibility-bump only when the shipped native runtime line remains compatible.
Docs-Only Install Boundary
The install walkthrough is useful product guidance, but it is not first-class supported
as a separate package surface. It teaches the primary crosswake install path and the
boundary between host-owned artifacts and Crosswake-owned contract surfaces.
Route owner
Install guidance does not change route ownership. Phoenix routes remain Phoenix-owned unless a route policy explicitly declares otherwise.
Why not core/companion
This walkthrough explains package and artifact boundaries. It is not itself a runtime package, and it does not promote host scaffolding into a companion surface.
Host-owned responsibilities
The host reviews generated files, owns shell project edits after generation, reruns proof hooks, and decides when local rebuilds are needed.
Prerequisites
A Phoenix host, explicit router wiring, generated policy module, and the proof scripts referenced by the support matrix.
Denial behavior
If install or compatibility truth is incomplete, mix crosswake.doctor reports blocking issues instead of inferring that the host is correctly wired.
Fallback behavior
Crosswake falls back to explicit diagnostics and route-unavailable posture, not silent package promotion or generic shell behavior.
Native rebuild required
Only when your change lands in the native or companion rebuild required class. Docs-only install guidance and core-only policy changes do not automatically imply a native rebuild.