# Installing a Space Data Network node

This is the operator path for a fresh node: what to install, which ports to
open, what to change in the generated config before the node is reachable,
how to keep the keys, how to mirror a publisher, and how to publish and
archive. It describes v1.0.5-beta.70 (2026-09-18).

## 1. Get a build

```sh
# macOS or Linux
curl -fsSL https://spacedatanetwork.org/install.sh | bash
# Windows PowerShell
irm https://spacedatanetwork.org/install.ps1 | iex
```

The installers resolve the newest node release on GitHub (today
v1.0.5-beta.70: macOS Apple Silicon and Intel, Linux x86-64 and arm64, and
Windows x86-64), verify the checksum,
unpack the self-contained bundle under `~/.spacedatanetwork/bundles`, link
`spacedatanetwork` and `sdn` under `~/.spacedatanetwork/bin`, and initialize
the node identity. Other platforms build from source:

```sh
git clone https://github.com/DigitalArsenal/space-data-network.git
cd space-data-network
npm run install:wasmedge      # WasmEdge 0.16.4 runtime the Go server links against
npm run server:build          # Go 1.25; produces sdn-server/spacedatanetwork
```

A plain clone is enough. The dashboard is embedded in the binary; its source
(`sdn-js/spaceaware-ui`) is a private submodule that clones skip.

A source build loads `hd-wallet-wasi.wasm` for its HD identity from
`sdn-js/node_modules/hd-wallet-wasm/dist/` (after `npm ci` in `sdn-js`), or
from `/usr/local/lib/hd-wallet-wasi.wasm`; a bundle carries it under
`runtime/modules/`.

### Desktop app

The desktop app (`space-data-network-desktop-<version>-<os>-<arch>` on the
release page) ships for macOS on Apple Silicon and Intel, for Windows x64 as
both an installer and a portable exe, and for Linux x64 as an AppImage, a .deb
and a .tar.xz. It carries the same bundle, starts the node on launch, opens the
node's own dashboard in its window, and keeps the store and keys under the
app's user-data directory. Its menu shows the node's recovery phrase once, on
request. Nothing below applies to the app except sections 4 (ports, if you want
to be reachable from the internet) and 5 (keys).

#### First launch: the binaries are not signed

Beta builds carry no Apple Developer ID and no Authenticode certificate, so
both operating systems refuse them on first launch. This is the signature's
absence, not a finding about the software — but you have to get past it, and
the two systems differ in how hard they make that.

**macOS.** Gatekeeper shows "Apple could not verify … is free of malware",
and its only buttons are *Move to Trash* and *Done* — there is no "open
anyway" in that dialog. Two ways through:

    # either: strip the quarantine flag the browser attached
    xattr -dr com.apple.quarantine "/Applications/Space Data Network.app"

    # or: launch once, get the dialog, then
    #     System Settings -> Privacy & Security -> "Open Anyway"

The right-click → Open shortcut no longer works on macOS 15. The CLI tarball
is unaffected when expanded with `tar` from a terminal, which does not carry
the flag onto the extracted files.

**Windows.** SmartScreen shows "Windows protected your PC"; click *More info*
→ *Run anyway*. If you downloaded the zip with a browser, clear its
mark-of-the-web before expanding, or the extracted exe may refuse to start:

    Unblock-File .\spacedatanetwork-<version>-windows-amd64.zip

A managed box with SmartScreen set to Block will refuse outright, and
Defender may take several seconds to scan a 340 MB executable before the
first output appears.

### Docker

```sh
docker run -d --name sdn --hostname sdn --restart unless-stopped \
  -v sdn-data:/app/data -p 127.0.0.1:5001:5001 -p 4001:4001 -p 4001:4001/udp \
  dockerdigitalarsenal/space-data-network:v1.0.5-beta.70
```

The image (`linux/amd64`) carries the node, WasmEdge with a prewarmed AOT
engine, Kubo and the wallet sign-in assets; `config/full-docker.yaml` is baked
in as `/app/config/full-docker.yaml` and everything the node owns lives in the `/app/data`
volume (store, keys, auth, peers).

`--hostname` is required, and the node enforces it. With no
`SDN_KEY_PASSWORD` / `SDN_KEY_PASSWORD_FILE` set, the key-at-rest password is
derived from the container's hostname; Docker's auto-generated one is a fresh
container ID every time, so a recreated container could not open its own
identity. Rather than sealing into a name that will not come back, a first
start without `--hostname` refuses and prints the three ways forward — a fixed
hostname, a mounted password file, or `SDN_ALLOW_EPHEMERAL_HOSTNAME=1` for a
throwaway. Any stable value works; it is not a secret. A node already sealed
under an auto-generated hostname keeps running and warns on every start: re-seal
it with `spacedatanetwork key reseal` before recreating the container.

`docker exec sdn
/app/spacedatanetwork show-identity --config /app/config/full-docker.yaml --show-mnemonic`
prints the recovery phrase. A `docker load` archive of the same image ships
with each release. Its name spells the version with DOTS, not the tag's
hyphen — `spacedatanetwork-container-1.0.5.beta.70-linux-amd64.tar.gz`, not
`…-1.0.5-beta.70-…`, which 404s. Native Linux packages use the same dotted
spelling, for the same reason: a release asset name cannot carry SemVer's
hyphen.

### Relaying

A node that is publicly reachable relays for peers that are not. This is the
default: AutoNAT decides, and a node behind NAT never relays. It is how nodes
on corporate networks and CGNAT — which can connect but cannot be dialed —
reach anyone at all.

It is bounded (32 reservations, 8 circuits per peer, 30-minute TTL), well under
libp2p's own defaults, because this is spare capacity a node contributes rather
than a service it exists to provide. Set `network.enable_relay: never` to opt
out, or `always` to run it regardless of reachability with libp2p's larger
limits. Check what your node decided with
`curl -s localhost:5001/api/v1/status/reachability`.

## 2. Kubo

Content identifiers, pinning, dataset publication and archive restore all go
through a Kubo (go-ipfs) daemon.

- **From a bundle** the node runs its own Kubo: the bundled binary, a
  repository under the node's data directory, the RPC API on
  `127.0.0.1:5002` (off the admin listener's 5001) and the gateway on
  `127.0.0.1:8080` with network fetch turned off. Nothing to install.
- **From a source build**, or to use a Kubo you already run, point
  `admin.ipfs_api_url` at its API (and `admin.ipfs_gateway_url` at its
  gateway for the dashboard's archive links); the node leaves it alone. A
  bare binary can also name a Kubo binary with `SDN_KUBO_BINARY` to have
  the node run it.
- The node proxies `/ipfs/*` to the gateway for content it already holds
  only; it never fetches arbitrary CIDs for callers. If you expose Kubo's own
  gateway anywhere, set `Gateway.NoFetch=true` on it.

## 3. Initialize and edit the config

```sh
spacedatanetwork init --config /etc/space-data-network/config.yaml
```

`init` derives the node's HD identity, seals the mnemonic to this machine and
writes a complete config. Edit these before binding anything non-loopback:

| Key | Set it to |
|---|---|
| `storage.path` | a directory on the disk that will hold the records |
| `storage.max_size` | below the free space on that disk (the node refuses writes under 5 GiB free) |
| `asset_pins.kubo_repo_path` | your existing Kubo repo, or delete the line (the generated default names a production volume) |
| `admin.listen_addr` | `127.0.0.1:5001` for a loopback dashboard; a routable address only with TLS and authentication on |
| `admin.require_auth` | `true` (the default). Leave it on. |
| `admin.tls_mode` / `admin.tls_hosts` | `managed` plus your hostname for a public dashboard |
| `admin.ipfs_api_url` | leave unset from a bundle (managed Kubo); your Kubo's API otherwise |
| `status.allowed_origins` | your own origin, or delete the line |
| `network.listen` | keep the defaults unless a port is taken (see step 4) |
| `subscriptions.default_retention` | `replace-current` (the default: each publication replaces the lane's previous set) or `archive-all` (keep and pin every publication). A subscription can choose its own rule. |

Sign in to the dashboard with the node's own root key, or seed operators
under `users:` with their `signing_pubkey_hex`. A node whose root identity is
bound never mints an admin for a visiting wallet.

Do not run the daemon as root.

## 4. Ports

| Port | Protocol | Purpose |
|---|---|---|
| 4001 | tcp and udp (QUIC) | libp2p |
| 8080 | tcp | libp2p over WebSocket (browser peers) |
| 4003 | udp | libp2p WebRTC-direct |
| 5001 | tcp, loopback | admin API and dashboard (default) |
| 443 / 80 | tcp | only with `admin.tls_mode: managed` |

Open 4001, 8080 and 4003 inbound. Keep 5001 on loopback unless TLS and
authentication are on.

## 5. Keys

- `init` seals the mnemonic to this machine. Export it once and keep the
  export off the box: `spacedatanetwork key export --format backup`.
- Keep the Kubo peer identity recoverable: `spacedatanetwork key escrow create`.
- If you use `SDN_KEY_PASSWORD_FILE`, keep that file out of the box's backups.

## 6. Run

```sh
spacedatanetwork start    # persistent background service
spacedatanetwork daemon   # foreground
spacedatanetwork status   # local node summary
```

Probes on the admin listener: `GET /health` answers `ok` while the process
serves; `GET /ready` answers `ready`, or `503 not ready: <reason>` while the
store is linking, busy rebuilding, or the libp2p host is down; `GET /metrics`
is Prometheus text and needs an operator session.

## 7. Mirror a publisher

Add the publisher to both lists; trust alone never dials.

```yaml
peers:
  trusted_peers:
    - 16Uiu2HAmGjaPxkWFSXBbmhs9K5x1Zo6euJw95VjS6Jj2bcPpYr2U   # celestrak.eth
network:
  bootstrap:
    - /ip4/167.172.219.213/tcp/4001/p2p/16Uiu2HAmGjaPxkWFSXBbmhs9K5x1Zo6euJw95VjS6Jj2bcPpYr2U
```

The publisher needs no configuration. Once connected, the node materializes
the publisher's signed dataset publications; the dashboard's SOURCES page
lists them by origin with SYNC, EXPORT and ARCHIVE per lane.

## 8. Publish your own records

Records are size-prefixed FlatBuffers of a Space Data Standards schema.

```sh
# a batch for one lane
curl -X POST 'http://127.0.0.1:5001/api/v1/data/publish/batch/OMM.fbs?provider_id=<you>&source_name=<lane>&batch_id=<id>' \
  -H 'Content-Type: application/vnd.sdn.flatbuffers.stream' --data-binary @records.sdn
```

The publish lane needs an operator session, or a separate loopback socket for
a pipeline on the same host: set `publishing.local_publish_addr:
127.0.0.1:5011` and post to that address instead. The default quota is
100 MB per provider (`publishing.default_quota_bytes`).

To publish the lane to the network as a signed dataset (shard, index,
manifest, announcement), from the box:

```sh
curl -X POST http://127.0.0.1:5001/api/v1/admin/dataset-updates/publish \
  -H 'Content-Type: application/json' \
  -d '{"schema":"OMM.fbs","providerId":"<you>","sourceName":"<lane>"}'
```

or configure `publishing.auto_publish` lanes, which fire for records ingested
by modules. Verify with `/api/v1/log/OMM.fbs/heads` and `ipfs pin ls`.

The default publication scope is `batch`. For a source that ingests several
raw-resource batches, opt into a complete source snapshot:

```yaml
publishing:
  auto_publish:
    - schema: NCD.fbs
      provider_id: example-provider
      source_name: example-source
      publish_scope: source
      min_interval: 5m
```

Source scope requires an exact, case-sensitive provider and source. It
publishes all currently stored records for that tuple, preserving each
record's original batch provenance. Ingest during publication or the minimum
interval remains pending and triggers a later snapshot without another
ingest event. Failed snapshots remain pending with capped retry backoff.
Starting the daemon also schedules a catchup snapshot for every configured
source lane, including records stored before the restart.

Saved module inputs from the runtime `/inputs` API are reapplied before
startup cron scheduling. A module whose saved inputs cannot be applied stays
in an error state while other modules continue starting.

Modules using the generic HTTP capability can set `follow_redirects: false`
to receive a redirect response directly. Redirect following remains enabled
by default. Responses retain the first value of each header in `headers`
and expose all values in `header_values`, including separate `Set-Cookie`
values.

## 9. Archive and restore

With an operator session:

- `POST /api/v1/archive` with a `$QRP` request (schema, provider, source)
  writes a signed archive: shard, index and manifest, pinned in Kubo.
- `GET /api/v1/archives` lists them; each frame is the manifest and the
  `X-SDN-Archive-CIDs` header names them by CID.
- `POST /api/v1/archive/import` with the manifest CID re-imports one.

The dashboard's source page has the same three actions. Record the manifest
CIDs somewhere durable: nothing announces archives to other nodes yet, so
after a total disk loss a node recovers only what its trusted publishers
still hold.

## 10. Updates

Self-update needs the self-contained bundle layout with
`trust/update-roots.json`; bundles from the release carry the fleet roots.
The feed at `https://sdn.spaceaware.io/updates` publishes the `beta` channel
for linux/amd64 only; other platforms reinstall from the newest release, and
a `stable` channel is pending.

## Known gaps on this date

- A source build (not a bundle) still needs a Kubo of your own.
- Archives are not announced, and there is no restore-from-network.
- Publishing has no dashboard or CLI front; it is the API above.
