Skip to main content

Remote Access and Pairing

Skulk provides several ways to operate a cluster. Choose the connection that matches the client and the access your cluster owner has configured.

ConnectionRequirementsIntended use
Local dashboardA browser that can reach a dashboard-serving nodeOperate the cluster on its trusted network
Dashboard over TailscaleThe browser's device and node can reach each other through the authorized tailnetPrivate remote browser access
Native operator app through the relayA provisioned cluster gateway, relay service, and valid pairingRemote iOS/Android access without a VPN on the phone
Paired browser sessionThe intended gateway served through HTTPS or localhost, an owner invitation, and appropriate grantsScoped browser operations such as plugin management

A browser session is not the native app's relay carrier. Browser pairing binds the tab to the gateway serving its page; it does not create an internet tunnel. Do not expose the unauthenticated local API listener directly to the internet.

Prepare remote access​

A node registers its own relay route the first time you pair a phone: choose Pair a phone under Settings → Devices & pairing, or run skulk operator pair on a headless node. The node generates its own connector key and carrier credentials, sends the relay only their fingerprints, and starts remote access without a restart. Installing Skulk or opening Remote Access alone does not contact the relay.

The node where you first pair a phone becomes the cluster's pairing gateway: paired phones reach the cluster through it, so choose a machine that stays on. Other nodes' dashboards say which node manages pairing instead of offering a second, separate one, and skulk operator pair asks the node on its machine (--api-port, default 52415) and refuses for the same reason. The cluster's five-device limit is counted on that one gateway. There is no automatic failover to another node.

The gateway registers with Foxlight's relay, relay.foxlight.ai, unless skulk.yaml names another:

connectivity:
relay:
enabled: true # false keeps pairing on LAN and Tailscale
registration_url: https://relay.example.invalid # optional; omit for relay.foxlight.ai

The relay is content-blind. It learns the connector key's fingerprint, the two credential fingerprints, the gateway's public address, connection times, and byte counts. It never sees app traffic, which stays inside TLS that terminates on the gateway.

To turn remote access off, choose Turn off in Devices & pairing, or run skulk operator forget-relay. Phones paired through the relay lose remote access until you pair them again, and codes created for the relay stop working. Revoke the old device records to free their slots.

A self-hosted relay can supply a provisioning file instead: skulk operator configure-relay --provisioning-file <file> stores that route and its protected carrier credentials and generates the gateway's TLS identity.

Use the dashboard's Remote Access view to find local LAN and Tailscale connection options. Relay health shows in Devices & pairing: connecting, connected, relay unreachable (check the machine's internet connection), or refused (turn phone pairing off, then pair a phone again). Keep provider and gateway secrets out of screenshots and support messages.

Create and revoke invitations​

Open Settings → Devices & pairing on the pairing gateway from localhost or an authorized direct Tailscale connection. Ordinary LAN access and the public relay cannot administer pairing invitations.

Choose the validity period and allowed device count, then select Pair a phone. The first time, the node registers with the relay and connects before the code appears. The displayed code/QR is a bearer secret. Its on-screen visibility window is separate from the invitation's validity period: hiding the code does not revoke it. Review recent invitations and revoke any that should no longer admit devices. On the phone, choose Scan pairing code, allow camera access, and center the displayed QR code in the frame. Review the cluster identity before confirming. See the native app guide.

Invitation creation and revocation happen when their own controls are selected; they do not wait for the main Settings Save changes button.

Dashboard-generated pairing QR code, revoked after capture

This invitation was revoked immediately after capture. The pictured code cannot pair a device; generate your own invitation in Settings.

An invitation admits pairing; the resulting device has its own identity and credentials. Revoking an invitation prevents further use of that invitation. To remove an already paired device, revoke the device itself. Device credentials use short-lived access tokens with refresh rotation; successful pairing does not grant every administrative permission.

Five paired devices per cluster​

A cluster allows at most five paired devices at once. Devices & pairing shows how many of the five slots are in use and offers only as many devices as there are free slots. When all five are taken it stops generating codes, and a phone that scans an older code is told the invitation is no longer available. To pair another device, revoke one under Paired devices first. On a headless host, skulk operator devices list shows the slots and skulk operator devices revoke DEVICE_ID frees one.

A device whose app has not connected for 30 days no longer counts: its refresh credential expired, so it must pair again anyway. The dashboard marks it Expired; revoke it to tidy the list.

Pair a browser for plugin operations​

From Plugins, open Browser access:

  1. Paste the owner's invitation and select Review invitation.
  2. Verify the cluster name, fingerprint, and gateway displayed by the page.
  3. Give the tab a recognizable name and select Pair this browser.
  4. Give the displayed device ID to the owner if additional plugin access is needed.

Credentials remain in tab memory and disappear on reload or disconnect. Pairing does not automatically grant plugin privileges. The owner grants plugins:read, plugins:manage, and plugins:approve separately. Read access, configuration authority, and approval of a reviewed proposal are distinct permissions. Publisher-trust and credential-destination setup remains a direct owner operation even for a browser with plugin-management grants.

How the native relay path works​

The designated operator gateway connects outward to the relay. The app connects to that relay and establishes a separate, pinned TLS connection through it to the cluster gateway. Application TLS terminates at the cluster, so the relay forwards encrypted bytes without reading prompts, answers, model names, commands, or canonical API bearer credentials.

Each request or stream has an independent connection lane. The on-demand connector keeps a control connection and opens bounded data lanes when requests arrive; the legacy provisioning format uses a bounded pool of waiting lanes. These are provisioning choices, not different model APIs.

The relay still handles connection-routing metadata and aggregate resource usage. Content encryption does not mean the relay sees no network metadata. The gateway enforces cluster authentication and route permissions after termination. If the designated gateway or relay is unavailable, remote app access is unavailable; local cluster operation can continue.

Troubleshooting​

SymptomCheck
Invitation generation is refusedUse the configured gateway through localhost or its authorized direct Tailscale connection
Pairing is refusedCheck invitation expiry, the five-device limit (revoke a device to free a slot), revocation, and cluster identity; obtain a current invitation
App is paired but offlineCheck gateway availability, relay configuration, and network connectivity; remembered identity is not live health
Browser loses access after reloadPair the tab again; browser credentials intentionally stay in memory
Plugin controls are unavailableAsk the owner to verify the exact plugin grants and manager readiness
An operation's response is uncertainObserve its retained operation ID before trying another mutation; a disconnect is not proof that the operation failed

For endpoint contracts, scopes, and response codes, see the API guide.