nova

Start here

Nova Operator Quickstart

This walkthrough takes you from a fresh clone to a running Nova node that serves an uploaded image over HTTPS. At the end you will have:

  • the full production stack (coordinator + embedded IPFS, Postgres, nginx two-vhost TLS front, certbot) running under Docker Compose;
  • a master key you have backed up, an operator account, and a rendered operator.yaml;
  • an image uploaded through the public API and served back at a content-addressed URL, with on-the-fly resizing;
  • the operator admin console reachable on the loopback admin vhost.

Everything below is copy-pasteable in order. Deep material — backups, key rotation, moderation, legal posture — is linked at the end, not duplicated here.

Prerequisites

  • A Linux host with Docker and the compose v2 plugin (docker compose version works; docker-compose v1 is not tested).
  • Roughly 5 GB of free disk for the image build (Go + Node multi-stage build cache) and volumes, plus whatever you intend to store.
  • A public DNS hostname pointing at the host — only if you choose the http-01 (Let's Encrypt) TLS mode. Every other mode works without one.
  • Nothing else. No Go, no Node, no libvips on the host — everything ships inside the coordinator image.

1. Clone and start setup mode

Clone the repository, seed the compose environment file, and set a real Postgres password:

git clone git@github.com:nova-archive/nova.git
cd nova
cp docker/.env.example docker/.env
sed -i "s/changeme/$(openssl rand -hex 16)/" docker/.env

Start the setup profile — Postgres, the coordinator in first-run setup mode, and a loopback-only nginx that fronts the wizard:

docker compose -f docker/docker-compose.yml --env-file docker/.env --profile setup up -d

The first run builds the coordinator image; expect a few minutes.

Setup is locked to whoever holds the node's bootstrap token, which the coordinator prints to its log on startup. Retrieve it:

docker compose -f docker/docker-compose.yml --env-file docker/.env logs coordinator | grep bootstrap_token

You will see a line like:

nova-coordinator | 2026/06/10 16:04:11 WARN coordinator SETUP MODE: bootstrap token (present as the X-Nova-Setup-Token header in the wizard) — copy it from this log bootstrap_token=aa9e9198cfec47c804350a72a1ba3aba

Copy the value after bootstrap_token=, then open the wizard at:

http://127.0.0.1:8444/setup/

The wizard is deliberately bound to loopback. If your node is a remote server, tunnel the port instead of exposing it:

ssh -N -L 8444:127.0.0.1:8444 you@your-server

2. The wizard, step by step

The wizard is a linear stepper; Next stays disabled until each step's requirements are met. You only do this once.

Bootstrap token. Paste the token you copied from the coordinator log. Every wizard request carries it, so nobody who merely reaches the port can configure your node.

Welcome. Enter the node's public hostname (e.g. nova.example.org), a contact email, and an optional display name. By continuing you agree to operate the node responsibly under the bundled terms.

The welcome step

Your master key. The wizard generates the key that encrypts all of your node's secrets, shows it once, and offers a backup download. This is the one unrecoverable step in the entire setup: if you lose the master key, your node's sealed secrets — and with them your stored content — are gone for good, federation-wide. Click Download backup, store nova-master-key.txt somewhere offline, then type the key's fingerprint into the confirm field. The readback exists precisely so you cannot click past this step without proving you captured the key; Next stays disabled until the typed fingerprint matches. Your IPFS swarm identity and content-signing keys are generated and sealed automatically alongside the master key — nothing to enter for those.

The master-key step with the fingerprint readback

Operator account. Email and password (12 characters minimum) for the first operator — this is the account you will use to sign in to the admin console at /admin.

TLS & certificates. Pick how Nova obtains its HTTPS certificate. Each mode shows its privacy cost inline — notably, http-01 publishes your hostname to public Certificate Transparency logs. See Choosing a TLS mode below; dev-self-signed is fine for kicking the tires. static additionally asks for your certificate and key paths. Load-bearing terms (fingerprint, Certificate Transparency) carry an ⓘ button with a plain-English explanation.

The TLS-mode step

Public uploads. Toggle whether anyone may upload through your node's public widget. If you enable it, a terms-of-service URL is required (a template lives at legal/TOS_TEMPLATE.md).

Privacy & hardening. A tri-state "Harden privacy (paranoid)" parent toggle over three individually exposed settings: whether to record uploader IPs (record_source_ip), how long to keep IP logs (1 day vs. 30), and whether to keep pinned CIDs off the public IPFS DHT (public_ipfs_dht). Each constituent shows its consequence inline; two additional rows (no outbound webhooks; metrics loopback-only) are informational. Fully checking all three commits paranoid: true in operator.yaml. ⓘ disclosures explain source-IP recording and the public IPFS DHT in plain language at the point of decision.

Review. Confirm your answers (secrets are not shown) and submit.

The review step

Commit. Committing writes operator.yaml, renders the two-vhost nginx config, creates your operator account, and seals the generated keys with your master key. This finalizes setup — make sure the master-key backup is safe before you click Commit & go live.

You're live. The orientation page shows the admin console link and the widget embed snippet (both repeated below). The coordinator exits cleanly behind the scenes and compose restarts it in normal mode.

The orientation page

3. Restart into production

Swap the setup profile for the prod profile, which brings up the real TLS front (wizard-rendered config) and the certbot sidecar:

docker compose -f docker/docker-compose.yml --env-file docker/.env --profile setup down
docker compose -f docker/docker-compose.yml --env-file docker/.env --profile prod up -d

Now live on the host:

Port What
8443 Public vhost — HTTPS: blob reads, uploads, transforms, the widget
8442 Public vhost — HTTP: redirect + ACME challenge (http-01)
127.0.0.1:8445 Admin vhost — HTTPS: admin console + admin API, loopback only

Host ports 80/443 are deliberately left free — Nova never grabs privileged ports or clobbers an existing web server. For a public deployment, forward :80 → :8442 and :443 → :8443 at your firewall or edge.

Check that everything is up:

docker compose -f docker/docker-compose.yml --env-file docker/.env --profile prod ps

All four services — postgres, coordinator, nginx, certbot — should report (healthy) within a minute or so. Then confirm the public vhost answers (with dev-self-signed TLS, -k accepts the self-signed chain and --resolve points your wizard hostname at the box; with a real certificate and DNS you need neither):

curl -ks --resolve "nova.example.org:8443:127.0.0.1" https://nova.example.org:8443/health

4. First upload

Option A — the upload widget

The orientation page already showed you the embed. Drop this on any page served from your public vhost:

<script src="/widget/nova-upload-widget.js" defer></script>
<div data-nova-upload-widget data-product="image"></div>

A ready-made demo page ships at https://<your-host>:8443/widget/ — open it and drag an image onto the widget. Anonymous widget uploads require public uploads to be enabled (the wizard toggle); otherwise mount the widget via JS with a getToken provider (see the operator checklist). By default those uploads are private (they join no collection); set uploads.default_collection_id to a public collection — see Option B — to make widget uploads publicly viewable without per-upload wiring.

Authenticated / off-origin embedding. To accept authenticated uploads — or to embed the widget on a different origin (e.g. your own site) without enabling anonymous public uploads — mint a scoped, revocable upload token and hand it to the widget:

novactl auth login                       # operator, against the admin vhost
novactl upload-token create --product image --label my-site
# prints a nova_ut_… secret ONCE — store it; serve it to your page from your backend
NovaUploadWidget.mount('#uploader', {
  product: 'image',
  getToken: async () => 'nova_ut_…',   // your backend supplies this
});

For a cross-origin host page, also set uploads.cors.enabled: true with your site in allowed_origins. Full flow, the security note about embedded tokens, and the uploads.limits.* backstops are in the operator checklist.

Option B — curl

One honest caveat first: uploads are private by default. A blob's visibility comes from the collections it belongs to, and a blob in no collection resolves to private — uploading works, but anonymous reads return 401/403. So first create a public collection (no raw SQL — novactl collection create is DB-direct, like novactl node):

novactl collection create --name "Public" --slug public --visibility public
# prints: created collection <COL_ID> (slug=public visibility=public owner=…)

--owner defaults to your sole operator account; pass --owner <user-uuid> if you have several. Now make uploads land in it, either:

  • Per upload — pass collection_id=<COL_ID> (curl below), or mint a token bound to it and hand it to the widget's getToken: novactl upload-token create --product image --collection <COL_ID>.
  • By default — set uploads.default_collection_id: <COL_ID> (via novactl config set uploads.default_collection_id <COL_ID>, the admin Settings screen, or operator.yaml). Then any upload with no explicit collection — including anonymous widget uploads — joins it automatically. Point it at a public collection only if you intend those uploads to be publicly viewable.

Upload an image — product=image is what makes the transform routes accept it (collection_id is optional once a default is set):

curl -ks --resolve "nova.example.org:8443:127.0.0.1" \
  -F "file=@photo.png;type=image/png" \
  -F "product=image" \
  -F "collection_id=<COL_ID>" \
  https://nova.example.org:8443/api/v1/blobs

You get a 201 with a JSON body containing the blob's cid. Serve it back, byte-identical, and resized:

curl -ks --resolve "nova.example.org:8443:127.0.0.1" -o roundtrip.png \
  https://nova.example.org:8443/blob/<cid>
curl -ks --resolve "nova.example.org:8443:127.0.0.1" -o thumb.png \
  https://nova.example.org:8443/i/<cid>/w320.png

Allowed transform widths are 320, 512, 1024, and 2048; anything else returns 400. Anonymous curl uploads, like the widget, require public uploads enabled — otherwise log in first (see below) and pass -H "Authorization: Bearer $TOKEN".

That's it — you are uploading and serving.

5. The admin console

The operator console lives on the admin vhost:

https://<your-host>:8445/admin

Sign in with the admin email and password you gave the wizard. From there you can browse and delete blobs, inspect audit results, and run key rotation.

The Settings screen (operator-only) tunes a running node against the same config backend as novactl config: live upload limits and CORS apply immediately; privacy posture (source-IP recording, retention, public-DHT) and public-uploads/ToS are restart-class and the screen says so per field. It also shows the full effective config read-only with live/restart/env badges. The long tail (auth, TLS, moderation) stays on novactl config set/apply — see below.

The admin vhost is bound to 127.0.0.1 on the host by default — it is not reachable from the network, on purpose. To use it remotely, prefer an SSH tunnel:

ssh -N -L 8445:127.0.0.1:8445 you@your-server
# then browse https://<your-host>:8445/admin locally

If you decide to expose it deliberately (e.g. on a management VLAN), copy docker/docker-compose.override.yml.example to docker/docker-compose.override.yml and add a ports entry for the nginx service bound to a private interface — never 0.0.0.0.

The same login works against the admin API directly, e.g. for scripted bearer tokens:

curl -ks --resolve "nova.example.org:8445:127.0.0.1" \
  -H 'Content-Type: application/json' \
  -d '{"username":"you@example.org","password":"your-password"}' \
  https://nova.example.org:8445/api/v1/auth/login

Next: the settings

Everything you can configure — TLS modes and their privacy trade-offs, headless/scripted setup, external OIDC, the federation block — is in docs/reference/operator-configuration.md.

This page deliberately stops at "it works".

Next steps

  • Read the operator checklistlegal/OPERATOR_CHECKLIST.md is the deep runbook. Non-negotiables first:
  • Back up the secrets volume. The wizard generated your master key, swarm key, and signing key into the nova-secrets volume; back it up out-of-band before you accept traffic. The paper backup the wizard made you download is the floor, not the ceiling.
  • Master-key rotation — the five-step runbook when you need to rotate.
  • Moderation and DMCA — ToS, designated-agent registration, and takedown procedure if you host public uploads.
  • Re-arming the wizard — deleting .bootstrap-complete from the config volume puts the node back in setup mode; that is the documented redo path, and a thing to be careful of.
  • Inspect and tune settings at runtime — once the node is up you do not need to edit operator.yaml by hand. Use novactl config:

sh novactl auth login # operator, against the admin vhost novactl config get --effects # print effective config + per-field source/effect novactl config set uploads.limits.max_concurrent_global 8 # live — no restart needed novactl config set auth.issuer_url https://idp.example/ # restart-required — flagged in response

Fields marked live take effect immediately in process. Fields marked restart are persisted and validated now but need a coordinator restart to apply — the response lists them in restart_required. See legal/OPERATOR_CHECKLIST.md for the full API reference, effect-class table, env-override behaviour, and optimistic-concurrency (If-Match) details.

  • See where the project is goingROADMAP.md covers the rest of Phase 1 and the Phase 2 federation work (collection APIs, donor storage nodes, streaming envelope).

Source: docs/quickstart.md