Overview
LoopString uses Tailscale to give each Raspberry Pi a stable, private IP address and a public HTTPS hostname (via Tailscale Funnel) that the cloud dashboard can reach without port-forwarding or a static IP. Remote access is a paid feature available from the Hobby tier upward.
The remote access system has three layers:
Layer 1 — RTDB tunnel status. The Pi publishes its Tailscale online state to Firebase RTDB at /devices/{deviceId}/tailscale. The RtdbTransport subscribes to this path and surfaces the value in the Device Status Bar as "Tunnel active" or "Tunnel offline". This is a best-effort heartbeat; it reflects the last write from the Pi, not a live probe.
Layer 2 — Cloud Function status check. The getTailscaleStatus Firebase callable function queries the Tailscale Management API using the TAILSCALE_API_KEY secret. It looks up the Pi by Tailscale device ID (cached in Firestore) or by hostname (full list fallback), then checks whether lastSeen is within five minutes. A device is considered online if that threshold is met.
Layer 3 — Edge-first transport (optional). When edgeFirstEnabled is set on the Firestore device document, the useEdgeFirstTransport hook replaces the RTDB subscription with a direct WebSocket connection to the Pi over Tailscale: wss://<tailscaleHostname>/ws/telemetry. This path bypasses Firebase entirely for telemetry.
Hostname resolution follows this order in the Cloud Functions:
- Use the cached
tailscaleDeviceIdinusers/{uid}/devices/{deviceId}for a direct O(1) lookup. - If the cached ID is stale (404 from Tailscale API), fall back to listing all devices on the tailnet and matching by hostname.
- Cache the resolved Tailscale device ID in Firestore for the next call.
Issue: Tunnel Status Shows "Offline" in the Dashboard
The Device Status Bar shows "Tunnel offline" even though the Pi appears to be running.
Why this happens. The RTDB path /devices/{deviceId}/tailscale is written by the Pi's health publisher subflow. If Node-RED has not started, the Pi rebooted and Tailscale has not yet authenticated, or the health subflow has an error, this value either stays stale or is never written.
Resolution steps.
- SSH into the Pi (from a machine already on the same Tailscale tailnet, or from local network):
ssh loopstring@<pi-local-ip>orssh loopstring@<tailscale-hostname>. - Check that the Tailscale daemon is running:
sudo systemctl status tailscaled. If it is stopped or failed, runsudo systemctl start tailscaled. - Verify the Pi is authenticated to the tailnet:
tailscale status. The output should show the Pi's hostname and a100.x.x.xIP. If it shows "not logged in" or "needs reauth", see the auth key expiry section below. - Check Node-RED is running:
sudo systemctl status nodered. If stopped, runsudo systemctl start nodered. - Force a health publish by restarting Node-RED:
sudo systemctl restart nodered. The health subflow publishes on startup. - Wait 30 seconds, then refresh the dashboard. The tunnel status reads from RTDB which updates within seconds of the Pi writing it.
Issue: "Device Not Found on the Tailscale Network"
Clicking "Open Node-RED" in the Device Status Bar returns the error: "Device not found on the Tailscale network. Ensure the Pi has joined the tailnet."
Why this happens. The getTailscaleAccessLink Cloud Function could not find a device on the tailnet matching the hostname stored in Firestore. This happens when the Pi has never joined the tailnet, when the hostname in Firestore is wrong, or when the cached tailscaleDeviceId points to a device that was removed from the tailnet.
Resolution steps.
- Confirm the Pi is on the tailnet: run
tailscale statuson the Pi and note the exact hostname shown (the short hostname, not the full FQDN). - In the LoopString dashboard, open Device Settings for the affected device. Check the "Tailscale Hostname" field. It must match the short hostname exactly (case-insensitive). Partial matches are not used — the lookup is exact.
- If the hostname is wrong or blank, update it in Device Settings. The
syncTailscaleHostnameCloud Function writes this field automatically when the Pi boots and publishes to RTDB at/devices/{deviceId}/meta/tailscaleHostname, but this requires the Pi to have run at least one provisioning boot with network access. - If you recently removed and re-added the Pi to the tailnet (which changes its Tailscale device ID), the cached ID in Firestore is stale. The Cloud Function will detect this automatically on the next request and re-resolve by listing all devices, then cache the new ID. You do not need to clear it manually.
- Confirm the
TAILSCALE_API_KEYsecret is set in Firebase Cloud Functions. Navigate to the Firebase console, open Functions, and check thatTAILSCALE_API_KEYis present under Secret Manager. If it is missing, remote access will return "Tailscale API key not configured" to all users.
Issue: Can Reach the Pi on Local Network But Not via Tailscale
The Pi responds on the local network but the Tailscale-based remote access fails or times out.
Why this happens. Tailscale requires the daemon (tailscaled) to be running and authenticated. A firewall rule or router configuration may also be blocking UDP port 41641, which Tailscale uses for direct peer-to-peer connections. When UDP is blocked, Tailscale falls back to relayed traffic via DERP servers, which is slower but should still work. A complete failure usually points to the daemon itself.
Resolution steps.
- On the Pi, confirm the Tailscale daemon is running and authenticated:
tailscale status. Look for your Pi listed with a100.x.x.xaddress. - Run a Tailscale ping from another device on the tailnet:
tailscale ping <pi-hostname>. This tells you whether Tailscale is routing traffic at all, and whether it is using a direct connection or a relay. - If the ping succeeds but the Node-RED HTTP interface does not respond, confirm Node-RED is listening:
sudo ss -tlnp | grep 1880. Node-RED should be bound to0.0.0.0:1880. - Confirm Tailscale Funnel is enabled for the Pi if you are using the HTTPS Funnel URL. Run
tailscale funnel statuson the Pi. If Funnel is not configured, thefunnelUrlreturned bygetTailscaleStatuswill still be constructed from the FQDN, but requests to it will time out. In this case, the SSH access link (which uses the Tailscale IPv4 address directly) should still work from devices on the tailnet. - Check your router does not block outbound UDP 41641. Most consumer routers allow this by default. If your network has strict egress filtering, Tailscale will still function over TCP port 443 via DERP, though with higher latency.
Issue: Auth Key Expired — Pi Cannot Rejoin the Tailnet
After a factory reset or re-provisioning, the Pi fails to authenticate with Tailscale. The logs show "auth key expired" or "not logged in".
Why this happens. The Tailscale auth key used during Pi provisioning has a finite lifetime. Ephemeral keys issued during the LoopString first-boot wizard expire after use. If the Pi is re-imaged without using a new claim token and auth key, or if the auth key is reused after it has expired, the Pi cannot authenticate.
Resolution steps.
- Generate a new auth key in the Tailscale admin console at
login.tailscale.com/admin/settings/keys. Create a reusable key if you need to provision multiple Pis, or a one-time key for a single device. Enable "Ephemeral" if you want the device to be removed from the tailnet automatically when it goes offline for an extended period. - On the Pi, remove the existing Tailscale state:
sudo tailscale logout. - Re-authenticate with the new key:
sudo tailscale up --auth-key=<your-new-key>. - Verify the Pi appears in the tailnet:
tailscale status. - If you used the LoopString first-boot wizard to provision the Pi originally, the auth key was embedded in the Pi image or provided via QR code claim token. For a fresh re-provision, generate a new claim token from the LoopString dashboard under Device Settings → Provision New Pi.
Issue: "Remote Access Requires a Hobby Subscription"
Clicking "Open Node-RED" shows an upgrade prompt or returns a permission error.
Why this happens. The getTailscaleAccessLink Cloud Function checks the caller's subscription tier before returning an access URL. The free tier does not include remote access. The tunnel status check (getTailscaleStatus) is available on all tiers.
Resolution steps.
- Upgrade your LoopString subscription to Hobby or above. Open the account menu → Billing → Upgrade Plan.
- Once upgraded, the "Open Node-RED" button will become active in the Device Status Bar. The tier check is enforced server-side; a page refresh is not required.
- If you have already upgraded and still see this error, sign out and sign back in to refresh your subscription state, then try again.
Issue: Edge-First Transport (WebSocket) Cannot Connect
When edgeFirstEnabled is turned on for a device, the dashboard shows the device as offline or data stops updating.
Why this happens. The EdgeTransport class connects to wss://<tailscaleHostname>/ws/telemetry directly, bypassing Firebase RTDB. If the Tailscale hostname is wrong, the Pi's edge WebSocket server subflow is not running, or the Pi is offline, the connection fails and EdgeTransport schedules exponential backoff reconnects.
Resolution steps.
- Verify the
tailscaleHostnameon the Firestore device document matches the Pi's actual Tailscale hostname. This is the short hostname (e.g.,loopstring-pi) not the full FQDN. - Confirm the edge WebSocket server subflow is deployed and running. In the LoopString Configurator, check that the
edge-ws-serverflow template has been deployed. The subflow listens on port 1880 at the/ws/telemetrypath. - Check that the Pi's Node-RED instance is accessible over Tailscale by opening
https://<tailscale-fqdn>in a browser from a device on the tailnet. - Look at the browser console for
[EdgeTransport]log lines. Connection errors and auth failures are logged there. Anauth_errormessage from the Pi means the Firebase ID token was rejected by the edge auth subflow; try signing out and back in to get a fresh token. - If
edgeFirstEnabledis true buttailscaleHostnameis blank, theuseEdgeFirstTransporthook falls back to the default RTDB transport automatically. Check that the hostname field is populated under Device Settings.
Issue: DNS Resolution Fails for Tailscale Hostname
The browser cannot resolve the Pi's Tailscale hostname (FQDN) in a Funnel URL.
Why this happens. Tailscale Funnel hostnames are in the format <hostname>.<tailnet>.ts.net. These are publicly resolvable via standard DNS, but DNS propagation after a new device joins the tailnet can take a few minutes. If Funnel is not enabled on the Pi, the FQDN will not resolve at all from outside the tailnet.
Resolution steps.
- Wait two to five minutes after the Pi joins the tailnet and retry. New Tailscale Funnel hostnames take a short time to propagate through public DNS.
- Confirm the Pi has Funnel enabled:
tailscale funnel status. If not enabled, runtailscale funnel --bg 1880to expose port 1880 over Funnel. - Test DNS resolution directly:
nslookup <pi-hostname>.<tailnet>.ts.net. A valid response should return a100.x.x.xaddress or a CNAME into the Tailscale Funnel infrastructure. - If you are on a network that uses a split-horizon DNS or custom DNS resolver that intercepts
*.ts.netqueries, you may need to configure that resolver to forwardts.netqueries to a public DNS server (e.g., 1.1.1.1 or 8.8.8.8).
Checking Tailscale Status Manually
You can check the raw Tailscale online status for a device by calling the getTailscaleStatus Cloud Function directly from the browser console:
const fn = firebase.functions().httpsCallable('getTailscaleStatus')
const result = await fn({ deviceId: 'your-device-id' })
console.log(result.data)
// { online: true, lastSeen: '2026-03-20T10:00:00Z', ipv4: '100.x.x.x', funnelUrl: 'https://...' }The online field is true if lastSeen is within five minutes of the current time. funnelUrl is only present when the Pi's FQDN is a valid *.ts.net address.
The RTDB path /devices/{deviceId}/tailscale holds the last value written by the Pi's health publisher:
{ "online": true, "lastChecked": 1742468400000 }This updates on each health publisher cycle (default every 30 seconds on Hobby, every 5 seconds on Business/Enterprise). If the Pi is offline this value will be stale; the Cloud Function provides a fresher check by querying the Tailscale API directly.