"Pairing" is Apus's explicit access approval step. It is used in two places:

  1. DM pairing (who is allowed to talk to the bot)
  2. Node pairing (which devices/nodes are allowed to join the gateway network)

Security context: Security

1) DM pairing (inbound chat access)

DM pairing applies to channels that implement Apus's pairing API. With DM policy pairing, unknown senders get a short code and their message is not processed until you approve.

Default DM policies are documented in: Security

dmPolicy: "open" is public only when the effective DM allowlist includes "*". Setup and validation require that wildcard for public-open configs. If existing state contains open with concrete allowFrom entries, runtime still admits only those senders, and pairing-store approvals do not widen open access.

Pairing codes:

Approve from the Control UI

Open Settings → Channels → DM access requests. The queue combines pending requests from every configured channel account whose DM policy is pairing. Filter by channel or account, review the sender ID and metadata, then choose Approve.

Approval grants direct-message access only. It does not grant group access. The approval dialog also offers these explicit options when supported:

Choose Dismiss to remove a pending request without approving it. Dismissal is not a permanent block; the sender can request access again later.

Approve from the CLI

apus pairing list telegram
apus pairing approve telegram <CODE>

Add --notify to tell the requester on the same channel. Multi-account channels take --account <id>.

Unlike the Control UI's explicit checkbox, the CLI automatically bootstraps commands.ownerAllowFrom when no command owner is configured, using an entry such as telegram:123456789. This gives first-time setups an explicit owner for privileged commands and exec approval prompts. After an owner exists, later pairing approvals only grant DM access; they do not add more owners.

Manually allowlisted senders are not automatically command owners. If an authorized sender has no owner access, owner-only commands reply with the exact apus config set commands.ownerAllowFrom command for the operator to run.

Set up an owner without DM pairing

Run apus channels add and complete the channel setup. When no command owner exists, the wizard offers Set up my operator account separately from chat access. Enter your personal user ID and confirm the exact account that may administer this installation. Skip for now leaves ownership unchanged.

This also works for Discord servers and other group channels with DMs disabled. An owner can use /update, restart the Gateway, change configuration, and approve commands. Ownership does not grant chat access: existing channel and group access rules still apply. The wizard never promotes chat allowlists automatically or replaces an existing owner.

WhatsApp's login QR links a WhatsApp account to Apus. DM access requests approve people who message that account. These are separate flows.

Supported channels include: discord, feishu, googlechat, imessage, irc, line, matrix, mattermost, msteams, nextcloud-talk, nostr, signal, slack, sms, synology-chat, telegram, twitch, whatsapp, zalo, zalouser.

Installed external plugins can also support DM pairing if they implement Apus's pairing API. Check the plugin's documentation for version-specific limitations.

Reusable sender groups

Use top-level accessGroups when the same trusted sender set should apply to multiple message channels or to both DM and group allowlists.

Static groups use type: "message.senders" and are referenced with accessGroup:<name> from channel allowlists:

{
  accessGroups: {
    operators: {
      type: "message.senders",
      members: {
        discord: ["discord:123456789012345678"],
        telegram: ["987654321"],
        whatsapp: ["+15551234567"],
      },
    },
  },
  channels: {
    telegram: { dmPolicy: "allowlist", allowFrom: ["accessGroup:operators"] },
    whatsapp: { groupPolicy: "allowlist", groupAllowFrom: ["accessGroup:operators"] },
  },
}

Access groups are documented in detail here: Access groups

Where the state lives

For channels that use Apus's pairing API, state is stored in the shared SQLite database at ~/.apus/state/apus.sqlite:

Account scoping behavior:

Older gateways wrote <channel>-pairing.json and <channel>-<accountId>-allowFrom.json under ~/.apus/credentials/. apus doctor --fix imports those files into SQLite and removes each source after a successful import. Normal Gateway startup leaves these legacy files unchanged. Treat the SQLite database as sensitive because these rows gate access to your assistant.

The pairing allowlist store is for DM access. Group authorization is separate. Approving a DM pairing code does not automatically allow that sender to run group commands or control the bot in groups. First-owner bootstrap is separate config state in commands.ownerAllowFrom, and group chat delivery still follows the channel's group allowlists (for example groupAllowFrom, groups, or per-group or per-topic overrides depending on the channel).

2) Node device pairing (iOS/Android/macOS/headless nodes)

Nodes connect to the Gateway as devices with role: node. The Gateway creates a device pairing request that must be approved.

Pair from the Control UI (recommended)

Use an already connected Control UI session with operator.admin access:

  1. Open the Control UI and go to Settings → Devices.
  2. On the Devices page, click Pair device.
  3. Keep Full access (recommended), or select Limited access to omit administrative Gateway controls.
  4. Click Create setup code.
  5. On your phone, open the Apus app → Settings → Gateway.
  6. Scan the QR code or paste the setup code, then connect.

Official Apus iOS and Android apps are approved automatically when their setup-code metadata matches. If Pending approval shows a request (for example, for a non-official client or mismatched metadata), review its role and scopes before approving it.

The button is disabled when the current Control UI session does not have administrator access. Use the CLI approval flow below from the Gateway host in that case.

Pair via Telegram

If you use the device-pair plugin, you can do first-time device pairing entirely from Telegram:

  1. In Telegram, message your bot: /pair
  2. The bot replies with two messages: an instruction message and a separate setup code message (easy to copy/paste in Telegram).
  3. On your phone, open the Apus iOS app → Settings → Gateway.
  4. Scan the QR code (/pair qr) or paste the setup code and connect.
  5. The official mobile app connects automatically. If /pair pending shows a request, review its role and scopes before approving it.

The setup code is a base64-encoded JSON payload that contains:

  • url: the Gateway WebSocket URL (ws://... or wss://...)
  • urls: when available, the ordered LAN/Tailnet routes the mobile app can try
  • bootstrapToken: a single-use bootstrap token for the initial pairing handshake; the Gateway expires it after 10 minutes

Run /pair cleanup to invalidate unused setup codes once pairing finishes.

That bootstrap token carries the built-in pairing bootstrap profile:

  • a secure wss:// setup (or same-host loopback) defaults to node plus full native-mobile operator access
  • the handed-off node token stays scopes: []
  • the default handed-off operator token includes operator.admin, operator.approvals, operator.read, operator.talk.secrets, and operator.write
  • Control UI Limited access and apus qr --limited omit operator.admin while keeping the other operator scopes
  • plaintext LAN ws:// setup automatically uses the same limited profile; configure wss:// or Tailscale Serve and generate a new code for full access
  • later token rotation/revocation remains bounded by both the device's approved role contract and the caller session's operator scopes

Treat the setup code like a password while it is valid.

The iOS and Android Settings → Gateway pages show Full or Limited access. To upgrade a limited phone, first configure a secure wss:// or Tailscale Serve route, then generate a new full-access setup code, scan or paste it in that settings page, and reconnect.

For Tailscale, public, or other remote mobile pairing, use Tailscale Serve/Funnel or another wss:// Gateway URL. Plaintext ws:// setup codes are accepted only for loopback, private LAN addresses, .local Bonjour hosts, and the Android emulator host. Non-loopback plaintext routes receive limited access. Tailnet CGNAT addresses, .ts.net names, and public hosts still fail closed before QR/setup-code issuance.

Apus advertises Tailscale setup URLs only when it owns the route through gateway.tailscale.mode=serve|funnel. Legacy external Serve routes that proxy a gateway.bind=lan listener are not advertised because the ordinary listener rejects Tailscale-shaped proxy ingress. Run apus doctor to inspect the route; Doctor leaves the configuration unchanged because it cannot prove route ownership. If you confirm it is a stale route from an older Apus release, remove only its root handler with tailscale serve --yes --https=443 --set-path=/ off or tailscale funnel --yes --https=443 --set-path=/ off, then configure gateway.bind=loopback and gateway.tailscale.mode=serve manually and restart the Gateway. If another service owns the route, leave managed Tailscale ingress off and configure the explicit gateway.trustedProxies compatibility path. Custom Serve ports and Tailscale Services require manual migration. For a retired gateway.tailscale.serviceName config, Doctor disables managed ingress and prints the command needed to clear the retained Service route.

Approve a node device

apus devices list
apus devices approve <requestId>
apus devices reject <requestId>

When an explicit approval is denied because the approving paired-device session was opened with pairing-only scope, the CLI retries the same request with operator.admin. This lets an existing admin-capable paired device recover a new Control UI/browser pairing without editing the pairing store by hand. The Gateway still validates the retried connection; tokens that cannot authenticate with operator.admin remain blocked.

If the same device retries with different auth details (for example different role/scopes/public key), the previous pending request is superseded and a new requestId is created.

An already paired device does not get broader access silently. If it reconnects asking for more scopes or a broader role, Apus keeps the existing approval as-is and creates a fresh pending upgrade request. Use apus devices list to compare the currently approved access with the newly requested access before you approve.

Optional trusted-CIDR node auto-approve

Device pairing remains manual by default. For tightly controlled node networks, you can opt in to first-time node auto-approval with explicit CIDRs or exact IPs:

{
  gateway: {
    nodes: {
      pairing: {
        autoApproveCidrs: ["192.168.1.0/24"],
      },
    },
  },
}

This only applies to fresh role: node pairing requests with no requested scopes. Operator, browser, Control UI, and WebChat clients still require manual approval. Role, scope, metadata, and public-key changes still require manual approval.

Node pairing state storage

Stored in the shared SQLite state database at ~/.apus/state/apus.sqlite:

  • pending device pairing requests (short-lived; they expire after 5 minutes)
  • paired devices + tokens

Older gateways kept this state in ~/.apus/devices/*.json. Stop the Gateway and run apus doctor --fix to import those files into SQLite and archive them with a .migrated suffix. Normal startup leaves legacy files unchanged.

Notes

Related docs