Crosswake Troubleshooting

View Source

Start with the copied finding or symptom, then fix the route owner that owns the decision. Crosswake failures are meant to stay explicit: the shell, bridge, offline island, or backend seam should say why a route could not continue instead of falling through to a generic web container.

Use this guide with route_policy.md, bridge.md, offline.md, adoption.md, native_shell.md, and the support-truth labels in support_matrix.md. The canonical doctor output is guarded by test/crosswake/doctor/doctor_test.exs.

Symptom And Finding Index

Finding or symptomRoute owner to checkFirst command
undeclared_capabilitybounded bridgemix crosswake.doctor --router Elixir.YourAppWeb.Router
unavailable_capabilitybounded bridge or backend/provider seammix crosswake.doctor --router Elixir.YourAppWeb.Router --native-checks
compatibility_mismatchPhoenix/LiveView route plus shell runtime linemix crosswake.doctor --router Elixir.YourAppWeb.Router --native-checks
pack_incompatiblenative screen or pack-backed routemix crosswake.doctor --router Elixir.YourAppWeb.Router --native-checks
external_entry_deniedPhoenix/LiveView route entry policymix crosswake.doctor --router Elixir.YourAppWeb.Router
gate_deniedbackend/provider seammix crosswake.doctor --router Elixir.YourAppWeb.Router
step_up_requiredbackend/provider seammix crosswake.doctor --router Elixir.YourAppWeb.Router
route unavailable or route_unavailablenative screen or manifest-first activationmix crosswake.doctor --router Elixir.YourAppWeb.Router --native-checks
native evidence label confusionsupport-truth labelsmix test test/crosswake/guides/native_evidence_drift_test.exs
rejected offline replayoffline islandcd examples/phoenix_host && npx playwright test e2e/offline_sync.spec.ts
conflict replay outcomeoffline islandmix crosswake.doctor --router Elixir.YourAppWeb.Router

Phoenix/LiveView Owner

Use this section when Phoenix still owns the route: rendering, auth posture, data authority, and the product action stay server-owned.

external_entry_denied

What you see: a deep link, notification open, or external activation is denied with external_entry_denied, or the shell stays on a route unavailable surface.

Who owns the fix: the Phoenix route owner. The route must explicitly allow external entry before the shell can activate it from outside the current app flow.

Run this:

mix crosswake.doctor --router Elixir.YourAppWeb.Router

Change this route policy, host setup, or proof command: update the route policy entry posture for the route that should be externally reachable, or keep it internal and update the caller to route through a manifest-known internal flow. Then rerun doctor and the relevant route proof.

Proof label: merge-blocking proof when the route policy and doctor contract pass.

What this does not prove: it does not prove native-screen support, device support, or that every external URL is safe. It only proves this manifest-known route has the declared entry posture.

compatibility_mismatch

What you see: activation or bridge execution fails with compatibility_mismatch, often after a manifest, bridge protocol, native runtime, or package version change.

Who owns the fix: the route owner and release owner together. Phoenix owns the route policy and manifest truth; the shell owner owns the compatible native runtime line.

Run this:

mix crosswake.doctor --router Elixir.YourAppWeb.Router --native-checks

Change this route policy, host setup, or proof command: check compatibility.md and native_shell.md. If the change is core-only/no native rebuild, keep the native runtime line stable and rerun doctor/support proof. If the change touches native code, generated shell projects, entitlements, permissions, or native dependencies, treat it as rebuild-required and rerun generated-shell verification.

Proof label: verification-required until the compatible route, manifest, and shell proof pass; promoted paths need the relevant merge-blocking proof.

What this does not prove: it does not prove the current device or simulator build is supported. Compatibility truth is separate from emulator evidence and device evidence.

Bounded Bridge Owner

Use this section when the route remains Phoenix-owned but asks native code for one low-frequency semantic action. The bridge never owns navigation, rendering, offline mutation, or backend authority.

undeclared_capability

What you see: a bridge call is denied with undeclared_capability, or a route-tour bridge payload names a capability the route did not declare.

Who owns the fix: the Phoenix route owner. The route policy must declare the capability family before the bounded bridge may run it.

Run this:

bash script/verify_bounded_bridge_proof.sh
mix crosswake.doctor --router Elixir.YourAppWeb.Router

Change this route policy, host setup, or proof command: add the capability to the specific route only if the user job really needs that bounded native affordance. For the checked-in proof, /bridge-proof declares share; ordinary LiveView routes should continue without native side effects.

Proof label: merge-blocking proof for the bounded bridge contract; screenshots or native UI are only advisory evidence.

What this does not prove: it does not prove a native share sheet, high frequency events, local writes, or shell navigation authority.

unavailable_capability

What you see: the route declares a capability, but the manifest, shell, companion, provider, or support posture cannot supply it and returns unavailable_capability.

Who owns the fix: the route owner decides whether to keep the route Phoenix-owned without the affordance, add the required host/native setup, or move the flow to a native screen or backend/provider seam.

Run this:

mix crosswake.doctor --router Elixir.YourAppWeb.Router --native-checks

Change this route policy, host setup, or proof command: restore the missing capability family, generated shell verification, companion prerequisite, or provider setup named by doctor/support truth. If the capability is only advisory, document the fallback instead of treating it as required support.

Proof label: verification-required until the missing capability path has an explicit proof lane; advisory evidence cannot widen support truth by itself.

What this does not prove: it does not prove provider authority, native device support, or entitlement/session authority. It only proves the declared route can or cannot access the named capability.

Cached Read-Only Owner

Use this section when a route can show stale data but cannot create local canonical mutations.

cached read-only mistaken for offline mutation

What you see: a cached route is expected to replay edits, or support output shows stale/cached posture while product copy implies full offline behavior.

Who owns the fix: the route owner. Cached read-only is a degraded read; an offline island owns local mutation.

Run this:

mix crosswake.doctor --router Elixir.YourAppWeb.Router

Change this route policy, host setup, or proof command: keep the route as offline: :cached_read_only when stale display is enough. Move only the route that owns local mutation to runtime: :offline_island with an outbox or journal, then prove it with an end-to-end replay test.

Proof label: cached read-only posture can be covered by merge-blocking proof; it is not local-first mutation proof.

What this does not prove: it does not prove drafts, outbox replay, background sync, conflict resolution UI, or app-wide offline behavior.

Offline Island Owner

Use this section when one route owns local-first work with a durable outbox or journal and Phoenix/Ecto reconciliation.

rejected offline replay

What you see: /study/sync returns a rejected record, the browser keeps the local outbox entry, or the UI reports sync failed with queued records retained.

Who owns the fix: the offline-island route owner and Phoenix sync endpoint. The browser owns local queueing; Phoenix/Ecto owns validation and canonical persistence.

Run this:

cd examples/phoenix_host
npx playwright test e2e/offline_sync.spec.ts

Change this route policy, host setup, or proof command: inspect the semantic event fields, especially client_mutation_id, card_id, and rating. Fix the island event shape or the /study/sync validation path so rejected records stay visible and accepted records are deleted from IndexedDB only after Phoenix confirms them. See adoption.md.

Proof label: merge-blocking proof for the browser route-tour/offline-sync semantic path; screenshots are collateral only.

What this does not prove: it does not prove broad background sync, bridge mutation authority, or native/provider authority over offline work. Bridge mutation authority is not part of Crosswake's offline path.

conflict replay outcome

What you see: doctor or route-local status uses conflict, conflict_requires_attention, or conflict/replay language instead of accepting a queued event.

Who owns the fix: the offline-island route owner and backend reconciliation owner. Conflict is a product state that needs explicit user or backend handling.

Run this:

mix crosswake.doctor --router Elixir.YourAppWeb.Router

Change this route policy, host setup, or proof command: keep conflict outcomes visible. Add route-local reconciliation UI or backend rules that explain which canonical state wins, then prove accepted, rejected, and conflict outcomes separately.

Proof label: merge-blocking proof only when the route-local replay and conflict behavior are asserted semantically.

What this does not prove: it does not prove collaborative merge behavior, silent last-write-wins safety, background replay, or a general sync engine.

Native Screen Owner

Use this section when native code owns the session loop, or when a shell refuses activation before a web container loads.

pack_incompatible

What you see: a native-screen route or pack-backed bridge route fails with pack_incompatible.

Who owns the fix: the native-screen route owner and host shell owner. The route declared a pack requirement the shell cannot satisfy.

Run this:

mix crosswake.doctor --router Elixir.YourAppWeb.Router --native-checks

Change this route policy, host setup, or proof command: verify the route-local pack id/version in route policy, regenerate or rebuild the host shell when a native or companion pack changes, and rerun generated-shell verification. If the pack is not available, keep the route fail-closed instead of degrading into a generic web upload or generic bridge call.

Proof label: verification-required until the native/pack proof lane passes; changes that touch native packages are rebuild-required.

What this does not prove: it does not prove camera support, media-upload support, provider authority, or physical-device support.

route_unavailable or route unavailable screen

What you see: the shell opens a route unavailable screen, or doctor reports route unavailable=yes while checking manifest-first activation.

Who owns the fix: the native shell host owner and route owner. The shell is correct to fail closed when the route is missing, externally denied, pack incompatible, inactive, or runtime-incompatible.

Run this:

mix crosswake.doctor --router Elixir.YourAppWeb.Router --native-checks
bash script/verify_phase5_example_hosts.sh

Change this route policy, host setup, or proof command: confirm the route id exists in the manifest, the entry posture is right, packs are compatible, and the native screen is intentionally owned by native code. Do not bypass the shell by loading a generic web container for a route whose owner is :native_screen.

Proof label: merge-blocking proof for fail-closed manifest-first behavior; checked-in public-coordinate proof only says checked-in hosts resolve published coordinates by default.

What this does not prove: it does not prove simulator, emulator, physical device, app-store, camera, or provider support.

native evidence label confusion

What you see: a screenshot, simulator run, emulator run, or checked-in host path is described as native support without separating coordinate mode, execution environment, proof class, and limitation.

Who owns the fix: the docs/evidence owner and native host owner.

Run this:

mix test test/crosswake/guides/native_evidence_drift_test.exs
mix test test/crosswake/guides/evidence_manifest_test.exs

Change this route policy, host setup, or proof command: use the labels from support_matrix.md literally. Checked-in native hosts are checked-in public-coordinate proof; generated hosts are generated public-coordinate proof; --local is local-dev proof. Simulator or emulator capture is advisory evidence unless promotion criteria explicitly move it. Use emulator evidence or device evidence only for named runs that actually occurred.

Proof label: native collateral starts as advisory evidence; browser route-tour correctness remains the merge-blocking proof.

What this does not prove: advisory native collateral does not prove physical-device support, camera support, media-upload support, provider authority, app-store readiness, or merge-blocking native support.

Backend/Provider Seam Owner

Use this section when the device or provider emits evidence, but Phoenix/backend remains authority for sessions, access, commerce, notification opens, or entitlements.

gate_denied

What you see: route activation fails with gate_denied, often because a RouteGate predicate, feature gate, kill switch, entitlement, or policy check blocked access.

Who owns the fix: the backend/provider seam owner. The shell should not grant access around backend policy.

Run this:

mix crosswake.doctor --router Elixir.YourAppWeb.Router

Change this route policy, host setup, or proof command: fix the backend gate predicate, route policy, entitlement projection, or feature flag source. Keep the denial explicit until backend authority says the user may enter.

Proof label: merge-blocking proof for fail-closed gate behavior.

What this does not prove: it does not prove provider/device evidence can grant authority. Device and provider signals are evidence until backend projection accepts them.

step_up_required

What you see: an auth-sensitive route denies entry with step_up_required or a doctor finding such as auth.step_up_required_contract.

Who owns the fix: the backend/session authority owner. Step-up is a server-owned ceremony; bridge state or native cache state cannot skip it.

Run this:

mix crosswake.doctor --router Elixir.YourAppWeb.Router

Change this route policy, host setup, or proof command: keep auth_posture, auth_return, recent-auth windows, and step-up intent handling route-local and backend-owned. Prove handoff, step-up completion, auth-return replay, and telemetry through the existing auth contract tests before relaxing access.

Proof label: merge-blocking proof for session-authority contract behavior.

What this does not prove: it does not prove native auth UI, passkey/provider SDK support, refresh-token helpers, direct shell token authority, or device-owned session authority.

backend/provider evidence stays non-authoritative

What you see: a purchase, restore, media capture, notification token, or provider callback is available, but access or workflow state remains pending, awaiting verification, rejected, or stale.

Who owns the fix: the backend/provider seam owner.

Run this:

mix crosswake.doctor --router Elixir.YourAppWeb.Router --native-checks

Change this route policy, host setup, or proof command: project provider or device evidence through backend-owned reconciliation before changing authority. Use support_matrix.md and capabilities.md to keep provider adapters, notification tokens, media evidence, and entitlement snapshots evidence-only until the backend accepts them.

Proof label: provider/device collateral is advisory evidence until a specific backend-owned promotion rule and proof lane pass.

What this does not prove: it does not prove entitlement authority, session authority, media availability, push delivery, or physical-device support.