Current section

Files

Jump to
crosswake guides install.md
Raw

guides/install.md

# Crosswake Install Guide

Crosswake 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](support_matrix.md) 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](route_policy.md) for route-owner decisions and
> [guides/web_to_mobile_migration.md](web_to_mobile_migration.md) for an operational
> Phoenix route inventory pass.
> See [guides/adoption.md](adoption.md) for offline-sync architecture context and the rationale for the generated shell pattern.

1. `mix crosswake.install`
2. Declare at least one Phoenix route with `crosswake: [...]` policy metadata
3. `mix crosswake.doctor`
4. `mix crosswake.gen.shell ios|android` when the host starts claiming a native target
5. `bash script/verify_phase5_example_hosts.sh`
6. `mix 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](adopter_profiles.md)
for the Phoenix SaaS Portal, Selective Native Flow, and Local-First Study Flow profiles
and [guides/web_to_mobile_migration.md](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](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`:

```elixir
def deps do
  [
    {:crosswake, "~> 0.2"}
  ]
end
```

Then fetch it:

```sh
mix deps.get
```

## Step 1: Install Crosswake Into A Phoenix Host

Run:

```sh
mix crosswake.install
```

What it does:

- patches `lib/<app>_web/router.ex` with explicit Crosswake marker lines
- keeps LiveView router imports compatible with the Crosswake `live` macro
- patches `lib/<app>_web/endpoint.ex` with 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.json` so 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`, and `guides/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:

```elixir
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
end
```

The 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:

```elixir
# 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
```

```javascript
// 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`:

```elixir
~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.

```heex
<%!-- 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.

```elixir
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}
end
```

`attach/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](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:

```sh
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](native_shell.md) for
the shell contract and [guides/bridge.md](bridge.md)
for the bounded bridge contract.

## Native Evidence Labels

- `generated public-coordinate proof` is the default non-local shell generation path.
- `local-dev proof` is the explicit `--local` maintainer path.
- `checked-in public-coordinate proof` is 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:

```sh
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:

```sh
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:

```sh
mix crosswake.doctor --native-checks
```

With `--native-checks`, doctor executes the generated-host verification hooks:

- `script/verify_generated_ios_shell.sh`
- `script/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 only
- `core-only/no native rebuild` -> update core and rerun core contract + doctor/support proof
- `compatibility-bump only` -> confirm your compatibility window and run fail-closed compatibility fixtures
- `native 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](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.