# Troubleshooting

Each entry is one failure an unassisted integration actually hits, with the symptom, the cause, and
the fix. They are drawn from real first-run friction, not hypotheticals.

## `weir_app_doctor` reports the `sdk-use` check as failed

**Symptom.** `weir app-doctor --appRoot .` returns `ready: false` with a failed
`flow:<id>:sdk-use` check: "adapter exists but no Weir.configure/present/WeirView call was found."

**Cause.** The `integrationFile` declared in `weir/app.json` exists but the doctor cannot find
one of the native entry-point names it knows. A stub should fail, but a pass only proves a matching
symbol exists; it does not prove registry setup or a working presentation.

**Fix.** For SwiftUI, verify the file really registers components, installs the generated registry,
calls `Weir.configure`, and presents `WeirFlowView`/`Weir.present` with a v4 config root. For
RN/Expo, the doctor recognizes `WeirCore.fetchConfig` and `<WeirFlow>`; also verify
`WeirRegistry.setGeneratedRegistry` and the full device journey. For native Compose, verify the
real `WeirFlow` journey rather than treating a source-symbol match as device evidence.

## `weir_app_doctor` refuses before running any check

**Symptom.** The tool errors on the `config` check and runs nothing else.

**Cause.** `weir/app.json` is missing, unparseable, or fails the schema. The doctor needs a valid
config before it can check anything.

**Fix.** Confirm the file exists at the `--appRoot` you passed and inspect the validation issues.
The current parser expects `version: 1`, platform `ios|android|reactNative`, a non-empty
`flows` array, and one of the documented `delivery.updateConfig` values. Do not change a valid
app contract merely to silence a different check; fix the named path or field and rerun doctor.

## `weir_app_doctor` reports `baseline-config` or `baseline-freshness`

**Symptom.** The doctor reports a missing, invalid, or stale native baseline config or asset.

**Cause.** The directory declared by `flows[].baselineBundle` has no `config.json`, its flow id
does not match, its normalized bytes differ from the current source spec, or a referenced
`customAssets[].path` file is missing or stale.

**Edit → verify loop.** Edit `weir/flows/<flowId>.json`, then run `weir app-doctor --appRoot .`.
The `baseline-freshness` check now fails because the embedded config no longer matches the flow.
Run `weir embed --appRoot . --flowId <flowId>` to re-embed it, then re-run `weir app-doctor --appRoot .`;
the check passes once the baseline matches again.

**Fix.** Run `weir embed --appRoot . --flowId <id>` to rebuild the native baseline from the
authoritative app contract, rerun doctor, then inspect the built app and prove the packaged config
and assets render on a device. Do not add `index.html` or `manifest.json`; those are retired
browser-preview artifacts, not native input.

## `weir_conform` looks hung

**Symptom.** `weir conform` prints nothing for minutes; a short command timeout kills it.

**Cause.** Conform runs a real headless-browser persona walk. Expected runtime is about **3–5
minutes** for a ten-screen flow. It is progressing, not hung.

**Fix.** Do not set a timeout under about six minutes. Watch stderr — conform streams
`screen N/total: <screenId> ok` per screen as it walks, so you can confirm progress. It exits 0
only when every contract check passes.

## `weir_conform` fails on the event stream or a variable

**Symptom.** Conform exits non-zero reporting an event-stream mismatch, an unset declared variable,
or a native-feel check.

**Cause.** The browser-preview artifact's behavior diverges from what the spec implies — a screen the
walk cannot complete, a variable no screen sets, a literal currency string instead of a real
`products.list` value, or network on the paint path.

**Fix.** Run `weir walk` first (see [Sandbox preview & branch verification](/docs/sandbox-preview/))
and read `walk-report.json`: `reachedComplete` must be true and `variables` must have every
declared variable set. Fix the spec so every variable is written on every path and every persona
reaches `complete()`, then re-run conform. This proves the authoring contract, not the native app
renderer; follow it with SwiftUI/RN component and device tests.

## `weir_release` fails — tell a credential error from a build error

**Symptom.** `weir release` fails and it is unclear whether the flow or the server is at fault.

**Cause.** `weir release` does two different things that fail two different ways: the local
build/conform gate, and the publish call to the delivery API.

**Fix.** Separate them. Run `weir release --appRoot . --flowId onboarding --dryRun` first — the dry
run does the whole gate **except** the API call and needs no `WEIR_API_URL`/`WEIR_API_TOKEN`. If
the dry run fails, it is a build/conform problem (fix the spec). If the dry run passes but the real
release fails, it is a delivery problem: check the server is reachable, and that `WEIR_API_TOKEN`
matches this app's id if the server enforces tokens. If no operator has issued a token yet, stop
after the dry run — the server configuration is the token-issuance authority. For fully local runs
you need neither variable; see [Run everything locally](/docs/local/).

## The kill switch does not revert the app to native

**Symptom.** `weir_onboarding_enabled` is off in Remote Config, but the app still shows (or fails
to show) the Weir flow.

**Cause.** The kill switch lives on Firebase Remote Config by design, and its keys must match
exactly. A mismatched or missing `weir_onboarding_enabled` / `weir_manifest_url` /
`weir_manifest_public_key` / `weir_ingest_url` / `weir_ingest_token` breaks the revert path.

**Fix.** Confirm all five Remote Config keys are present and spelled exactly as above, and that the
app reads them at runtime — not from `weir/app.json`, which holds no secrets or endpoints. With the
flag off, the app must return to `nativeFallback` without the delivery origin being reachable at
all. If the flag is on but delivery is unavailable, the app-embedded v4 config is the SDK's offline
config tier. See
[Concepts → Kill switch](/docs/concepts/#kill-switch).

## Android app: a debug flag override never turns the flow on

**Symptom.** You set an environment variable to force `weir_onboarding_enabled` on for a local
proof, but the app ignores it.

**Cause.** An `am`-launched Android app process (how `adb` and the launcher start your app) does
**not** inherit adb-set environment variables, so `System.getenv(...)` can never be toggled from
`adb` on a device or emulator.

**Fix.** If the host app deliberately implements this testing seam, use an adb-settable system
property, which the app process does see. Wire a debug-only override that reads
`debug.weir_onboarding`, then force it on with:

```
adb shell setprop debug.weir_onboarding true
```

This is host-app code, not a promise of a supported native Compose package. Rebooting the emulator
clears debug props, which is a quick way to confirm the app's override no longer wins.

## iOS: a local `http://localhost` fetch fails silently

**Symptom.** The iOS demo app points at `http://localhost:8787` but never loads the config, with no
clear error.

**Cause.** Default App Transport Security blocks plaintext `http://`. The demo `Info.plist` has no
`NSAppTransportSecurity` exception, so the request is denied before it leaves the app.

**Fix.** Add an `NSExceptionDomains` block for `localhost` and `127.0.0.1` (with
`NSExceptionAllowsInsecureHTTPLoads`) to the demo's `Info.plist`. Android already permits cleartext
for local hosts and needs no change. See [Run everything locally](/docs/local/).

## The dashboard shows production data (or nothing) instead of your local server

**Symptom.** You run the dashboard against a local API, but it shows the live box's data, or empty
views.

**Cause.** The dashboard's API base already defaults to `http://localhost:8787`, so this usually
means something else is overriding it: a stale `localStorage` override from a previous session
(under `weir_dashboard_api_base`), or `VITE_API_BASE` was set to the live API at build time (as a
production build's own `.env.production` intentionally does).

**Fix.** Check for a stale override first: in the running app, open "Advanced: sign in with a bearer
token" and look at the "API base URL override (local dev only)" field — clear it or set it to
`http://localhost:8787`. If you built with `VITE_API_BASE` set, rebuild without it (or set it to
`http://localhost:8787` explicitly). See [Run everything locally](/docs/local/).
