Skip to main content
Everything below is something that is fine in development and wrong in production. Work down the list before you ship.

Pairing

Replace autoApprove with a real prompt.
pairing.autoApprove: true is a development convenience. The policy still caps it at low risk, so it cannot silently hand out write access, but a shipped app must show a human the device name and the requested capabilities.
Show the SAS digits and make the user compare them.
req.sas is nine digits, shown identically on both devices. That comparison is the whole defense against a machine-in-the-middle during pairing. A dialog with only an “Approve” button throws it away.
Set a policy, not just a hook.
The policy is evaluated before your approval hook, so a bug in the hook cannot exceed it.

Secrets and identity

Know where your keys are stored.
The host picks the strongest backend available: the OS keychain, then an AES-256-GCM encrypted file with a machine-derived key, then plaintext. Check which one you got:
Do not ship secrets.allowPlaintextFallback: true.
It exists for headless CI. In production it silently downgrades key storage to a readable file.
Persist the storage directory.
Identity, paired devices and the remembered listen port live in .crosslink-data/<appId>/. Wiping it unpairs every device and changes the host fingerprint, so every phone must re-scan. In a container, that directory must be a volume.

Networking

Pin the port if anything forwards to it.
The listen port is remembered across restarts, but a hand-written router forward should point at an explicit lan.port rather than a remembered one.
Choose the network mode deliberately.
See Connection Modes.
Check the remote diagnostics rather than trusting the mode.
A VPN or secure-DNS client holding the default route produces the worst failure mode there is: the router forwards, the reply leaves through the tunnel, and the phone hangs on a blank page instead of getting an error. vpnSuspected is set when STUN and the router disagree about the public address, which is that situation. See Remote Access.
Give installed clients a stable address.
A home IP changes when the ISP renews its lease, which breaks every endpoint a phone already stored. Set remote.publicHost to a dynamic-DNS name, or pairing.bootstrapUrl to an origin you control.
Bind deliberately.
lan.bind: "all" is required for another device to reach the host, and is what remote mode sets for you. "loopback" produces no lan endpoint at all — which is the right answer for a host that is not meant to be reachable.

Operations

Expose device management.
listDevices(), revokeDevice(id), revokeAllDevices() and setDeviceCaps(id, caps) are the whole surface. A user who loses a phone needs revokeDevice reachable from your UI, not from a REPL.
Log, with a real logger.
logger defaults to a no-op, so the SDK never writes to your stdout uninvited. Pass a sink of your own, or consoleLogger() from @crosslink/core, or you will be debugging a production pairing failure with nothing to read.
Subscribe to connectivity.
Reachability changes while the app runs — a lease lapses, a network changes. status.message is written to be shown to a user as-is.
Rate-limit and cap.
The defaults are sane; maxDevices is the one that is unlimited unless you set it.

Services, if you run them

Do not run signaling or relay open.
Set relayToken / signalingToken (or CROSSLINK_RELAY_TOKEN / CROSSLINK_SIGNALING_TOKEN) on both the services and the clients. @crosslink/dev-tokens generates per-machine tokens so the defaults are not open to anyone who finds the port. Deployment detail in Self-Hosting. Neither service can read your traffic — the relay forwards ciphertext and holds no keys — but an open one is capacity someone else can spend.

Validate everything

Every exposed method has inputSchema or validate.
An unvalidated handler trusts a remote device to send well-formed data. That is the one trust assumption Crosslink’s design does not make for you.
No idempotent: true on anything that appends, charges or sends.
It is a licence for the client to replay the call after a reconnect.

Security Overview

Invariants the design guarantees

Threat Model

What is and is not defended against

Troubleshooting

When it does not work