Owner apps

An owner app runs your node from another device, such as your phone, with everything the wallet can do. The node's side is there: it pairs apps and answers them on an encrypted stream. This page shows how that works, for the day you build one.

No owner app exists yet, and the contract an app speaks is not frozen: it can change in any release, since today every client ships with the node. What's next

On this page
  1. Pairing
  2. Relayed streams
  3. What is not there yet

Pairing

An app pairs with a key of its own, its device key, and from then on acts as the owner until the owner revokes it. So only the owner, on the node's own machine, can start a pairing:

  • On the command line, with dassie pair, which prints a pairing code.
  • In the wallet, in the Paired devices card of its settings, which shows the code as a QR code. The node starts a pairing for a browser only when the browser reached it from the same machine, at localhost. See Paired devices.
$dassie pair
dassie-pair:<node ID>?v=1&secret=<hex>&direct=https%3A%2F%2Fnode.example
Enter this code in the app within five minutes. It works once, and `dassie pair` again replaces it. The wallet on localhost shows it as a QR code.

A code works for five minutes, once. Starting another replaces it, and a restart of the node forgets it. The node keeps it in memory only.

The code is a URI with these parts:

dassie-pair:<node ID>

The node to pair with. The app pins the node's key to this ID.

v

The code's version, 1 today. An app refuses a version it does not know, and ignores parameters it does not know, so a version can gain parameters without a new number.

secret

The pairing's secret, in hex. The app claims the pairing with it.

direct

The node's own URL, percent-encoded, when it has a domain name. Its QUIC endpoint answers at that host and port, over UDP.

relay

For a node without a domain name, once for each of its peers that is online and has a URL, a few at most: the peer's <node ID>@<URL>, percent-encoded, so the @ is %40. Each peer relays to the node over the connection between them. A node without a domain name and with no peer online gives a code with neither direct nor relay, which no app can use.

The relay a node keeps, its default or the one DASSIE_RELAY names, is listed only if it is also one of those peers. The dassiepay.net front door, the default relay of a node without a hostname, never is: it passes on Open Payments only.

The owner sees every paired device in the wallet, with when it was paired and last seen, and revokes one there. Revoking closes its streams at once. A node keeps a bounded list of devices; when it is full, a new device is refused until the owner revokes one.

Why only the owner on the node's machine can pair: a stolen browser session must not become a device that outlives it.

Relayed streams

An app talks to its node on a stream with the purpose owner, over QUIC, one of two ways:

  • Directly, to the node's QUIC endpoint, at the direct URL's host and port.
  • Through a relay, one of the relay entries: the app asks it to join a stream to the node by its ID. A relay reaches only nodes that hold a connection to it, so a node behind a router is reached through the nodes it is connected to.

Inside the stream, the app runs TLS 1.3 with the node itself, with the ALPN dassie-owner/1. The app pins the node's key, and proves its device key, an Ed25519 key it makes, with the SLH-DSA key it derives from it, just as a node proves its own. The device is known by the ID of those two keys. A relay sees only ciphertext, and cannot pass its own stream off as the app's.

On that stream, the app speaks the same RPC as the wallet and the dassie command: the dassie.owner.v1 protobuf contract, framed as on the IPC socket, each message with a 4-byte big-endian length in front, at most 1 MiB. transport.proto describes the envelopes, calls and errors.

  1. Claim the pairing. On the first stream, call ClaimPairing with the code's secret and the name the owner will see, 1 to 64 bytes. A device that is not paired may call only that and GetBasicState. NOT_FOUND means the secret is wrong, expired or used; RESOURCE_EXHAUSTED means the device list is full.

  2. Connect again. A stream opened before the pairing ends after a short while, even once the claim succeeds. Open a new one: it is the paired device's.

  3. Call as the owner. Every method is open to the device now, the live Subscribe* streams included, except starting a pairing: a device cannot pair another. The node checks the device on every call, so a revoked device is out at once.

In Rust, dassie_node::owner_app::connect opens such a stream, directly or through a relay, from a QUIC endpoint whose key is the device key. crates/dassie-node/tests/owner_app.rs pairs a device and calls the node through both routes. The crate is part of the node and is not published on its own.

The full rules for paired devices, and how relays carry streams.

What is not there yet

The node pairs apps and serves them. What an app needs beyond that is still to come:

  • the apps themselves: a remote mode of the desktop app, and a mobile app;
  • approving an app's grant inside the owner app as it connects;
  • finding the node on the local network;
  • finding the relays that reach a node from its ID alone, without a pairing code;
  • a frozen contract: once the first app ships apart from the node, the owner contract and the pairing code become surfaces that stay compatible across releases.

Until then, build against the contract of the release you run, and expect it to change. What's next

A kopje of granite blocks against the sun, in veld grass; one dassie climbs its slab, one keeps lookout on top inside the sun, and one sleeps on the next rock; on a wide screen, a pair sits on a kopje to the left