diff --git a/.gitattributes b/.gitattributes
index 512a317..1b506bb 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -5,8 +5,8 @@
#
# The scripts below are why that matters. A shell script committed with CRLF
# fails on Linux with `bad interpreter: /bin/bash^M`, and the two places it would
-# surface are the least convenient possible: CI, and `scripts/install.sh`, which
-# users run. Nothing is broken today (every script is LF in the index); this is
+# surface are the least convenient possible: CI, and `ops/bootstrap/pg-init.sh`,
+# which the Postgres container runs on an operator's machine. Nothing is broken today (every script is LF in the index); this is
# what keeps it that way.
* text=auto eol=lf
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 76f9c5f..73ec356 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1764,6 +1764,24 @@ under the pre-1.0 conventions in [VERSIONING.md](docs/governance/VERSIONING.md):
A header rather than a redirect: four doors sit in front of this route, and
`/01/{gtin}` beside it does not redirect either.
+### Removed
+
+- **`scripts/install.sh`, the one-click installer, which could not install
+ anything.** Every source it fetched returned `404` — the script itself, the
+ compose file (saved as a 404 page, since the download had no `--fail`) and the
+ CLI binary, because no GitHub Release exists. The images it would pull are not
+ public. Its `.env` lacked both database passwords and the admin credentials
+ compose requires, and the compose file it placed at the install root reads
+ `../.env` and bind-mounts `../ops/bootstrap/`, so role provisioning would
+ silently never have run. It printed a locally generated `odal_sk_…` as the
+ "API Key", which the node never issued. And when `cargo` was present it ran
+ `cargo install odal-cli`, **a crate name nobody owns** — whoever registered it
+ would have run code on every machine that followed the installer.
+
+ The maintained path is `docs/guides/OPERATOR-SETUP.md`: build `odal`, then
+ `odal init` and `odal up`. A replacement installer is tracked for before 1.0,
+ once the images and the CLI are published for it to install (#397).
+
## [0.13.0] - 2026-09-13
### Breaking
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 5f65e45..4162394 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -59,7 +59,7 @@ dpp-engine/
Cargo.toml # Workspace root — 10 member crates
LICENSE # BSL-1.1
docker/ # Dockerfiles + compose (dev infra + prod stack)
- scripts/ # install.sh (curl|bash bootstrap)
+ scripts/ # the check scripts `just check` runs
ops/
pg/ # PostgreSQL DDL migrations (0001–0012_*.sql)
demo/ # CSV/XLSX import fixtures + JSON samples
diff --git a/docs/guides/OPERATOR-SETUP.md b/docs/guides/OPERATOR-SETUP.md
index 2b6d810..6e6e2bb 100644
--- a/docs/guides/OPERATOR-SETUP.md
+++ b/docs/guides/OPERATOR-SETUP.md
@@ -12,7 +12,7 @@ bare metal to a live, passport-issuing node.
| **Docker ≥ 24** (with Compose plugin) | `docker compose version` to verify |
| **A domain name** | Required for `did:web` — passports are signed against it |
| **TLS termination** | Reverse proxy (nginx, Caddy) in front of port 8001 recommended for production |
-| **`odal` binary** | Build: `cargo build -p dpp-cli --release` or download a release |
+| **`odal` binary** | Build: `cargo build -p dpp-cli --release` — no prebuilt binary is published yet |
---
diff --git a/justfile b/justfile
index 88bff33..ff5b875 100644
--- a/justfile
+++ b/justfile
@@ -32,10 +32,9 @@ REDOCLY_VERSION := "2.46.2"
# environment, and compose interpolation reads the env file it was given, not
# the environment it inherits.
#
-# The installer path was never affected — `scripts/install.sh` writes
-# `docker-compose.yml` and `.env` into the same directory, so they line up
-# there. This was only ever the in-repo developer path, which is the one
-# CLAUDE.md tells you to use.
+# `odal up` was never affected — it passes `--env-file` itself
+# (`cli/src/core/infra.rs`). This was only ever the in-repo developer path,
+# which is the one CLAUDE.md tells you to use.
COMPOSE := "docker compose --env-file .env"
# ---------------------------------------------------------------------------
diff --git a/scripts/README.md b/scripts/README.md
index 7ffe972..4598cf5 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -1,91 +1,19 @@
-# Odal Node Installer
+# scripts/
-**License:** BSL-1.1
+The checks `just check` runs, and the few helpers the `justfile` calls. Each
+script's header comment says what it enforces and why; `just --list` shows which
+recipe runs it. A `*.test.sh` beside a script proves that gate actually fails —
+a check that has never been seen red proves nothing.
----
+These are repository tooling, not something an operator runs.
-## What This Is
+## Installing a node
-A one-click setup script that scaffolds a complete self-hosted Odal Node stack
-using Docker Compose.
+There is no one-click installer. The one that lived here fetched a script, a
+compose file and a CLI binary that are not published anywhere, and pulled images
+that are not public, so it could not install anything; it was removed rather than
+repaired. A replacement is tracked for before 1.0, once there is something
+published for it to install: #397.
----
-
-## Usage
-
-```bash
-curl -sSL https://odal-node.io/install.sh | bash
-```
-
-### Options
-
-| Flag | Default | Description |
-|---|---|---|
-| `--no-cli` | — | Skip installing the `odal` CLI |
-| `--port PORT` | `8001` | Override the node port |
-| `--dir DIR` | `~/.odal` | Installation directory |
-| `--version VERSION` | `latest` | Odal Node image tag to install |
-
-Example — custom port, no CLI:
-```bash
-curl -sSL https://odal-node.io/install.sh | bash -s -- --port 9001 --no-cli
-```
-
----
-
-## What the Script Does
-
-1. Checks prerequisites (`docker`, `docker compose`)
-2. Creates `~/.odal/` (or `--dir`)
-3. Downloads `docker-compose.yml`
-4. Generates a random PostgreSQL password, KeyStore passphrase, and API key
-5. Writes `.env` with the generated secrets
-6. Runs `docker compose up -d`
-7. Polls `/health` endpoints until all services are healthy
-8. Optionally installs the `odal` CLI binary
-9. Prints a summary: node URL, resolver URL, API key, next steps
-
-The script is **idempotent** — re-running it against an existing installation
-will skip secret generation and reuse the existing `.env`.
-
----
-
-## What Gets Deployed
-
-| Service | Port | Description |
-|---|---|---|
-| `dpp-node` | 8001 | MVP binary (vault + identity + integrator) |
-| `dpp-resolver` | 8003 | Public QR resolution |
-| `postgres` | 5432 | PostgreSQL database |
-
-The node exposes sub-routes: `/vault/*`, `/identity/*`, `/integrator/*`.
-
----
-
-## Files
-
-| File | Description |
-|---|---|
-| `install.sh` | One-click installer script |
-| `docker-compose.yml` | Production Docker Compose file |
-| `README.md` | This file |
-
----
-
-## Manual Setup
-
-If you prefer not to pipe from the internet:
-
-```bash
-git clone https://github.com/odal-node/dpp-engine
-cd dpp-engine/installer
-cp ../. env.example .env
-# Edit .env with your preferred passwords
-docker compose up -d
-```
-
----
-
-## Architecture
-
-See [../docs/](../docs/) for full architecture documentation.
+To run a node today, follow [docs/guides/OPERATOR-SETUP.md](../docs/guides/OPERATOR-SETUP.md):
+build `odal` from this repository, then `odal init` and `odal up`.
diff --git a/scripts/install.sh b/scripts/install.sh
deleted file mode 100644
index 5c37a03..0000000
--- a/scripts/install.sh
+++ /dev/null
@@ -1,220 +0,0 @@
-#!/usr/bin/env bash
-# ─────────────────────────────────────────────────────────────────────────────
-# Odal Node — One-Click Installer
-# Usage:
-# curl -sSL https://odal-node.io/install.sh | bash
-# curl -sSL https://odal-node.io/install.sh | bash -s -- --no-cli --port 8001
-#
-# BSL-1.1 License — Copyright (c) 2026 Aleksandar Temelkov (Odal Node)
-# ─────────────────────────────────────────────────────────────────────────────
-set -euo pipefail
-
-# ── Defaults ─────────────────────────────────────────────────────────────────
-INSTALL_DIR="${HOME}/.odal"
-NODE_PORT=8001
-RESOLVER_PORT=8003
-INSTALL_CLI=true
-COMPOSE_FILE_URL="https://odal-node.io/docker-compose.yml"
-ODAL_VERSION="${ODAL_VERSION:-latest}"
-
-# ── Colours ───────────────────────────────────────────────────────────────────
-RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BOLD='\033[1m'; RESET='\033[0m'
-
-info() { echo -e "${GREEN}[odal]${RESET} $*"; }
-warn() { echo -e "${YELLOW}[warn]${RESET} $*"; }
-error() { echo -e "${RED}[error]${RESET} $*" >&2; exit 1; }
-
-# ── Argument parsing ──────────────────────────────────────────────────────────
-while [[ $# -gt 0 ]]; do
- case $1 in
- --no-cli) INSTALL_CLI=false; shift ;;
- --port) NODE_PORT="$2"; shift 2 ;;
- --dir) INSTALL_DIR="$2"; shift 2 ;;
- --version) ODAL_VERSION="$2"; shift 2 ;;
- -h|--help)
- echo "Usage: install.sh [--no-cli] [--port PORT] [--dir DIR] [--version VERSION]"
- exit 0 ;;
- *) warn "Unknown flag: $1"; shift ;;
- esac
-done
-
-# ── Banner ────────────────────────────────────────────────────────────────────
-echo -e "${BOLD}"
-echo " ██████╗ ██████╗ █████╗ ██╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗"
-echo "██╔═══██╗██╔══██╗██╔══██╗██║ ████╗ ██║██╔═══██╗██╔══██╗██╔════╝"
-echo "██║ ██║██║ ██║███████║██║ ██╔██╗ ██║██║ ██║██║ ██║█████╗ "
-echo "██║ ██║██║ ██║██╔══██║██║ ██║╚██╗██║██║ ██║██║ ██║██╔══╝ "
-echo "╚██████╔╝██████╔╝██║ ██║███████╗ ██║ ╚████║╚██████╔╝██████╔╝███████╗"
-echo " ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝"
-echo -e "${RESET}"
-echo -e " Digital Product Passport Infrastructure — version ${BOLD}${ODAL_VERSION}${RESET}"
-echo
-
-# ── Prerequisite checks ───────────────────────────────────────────────────────
-info "Checking prerequisites..."
-
-command -v docker >/dev/null 2>&1 || error "Docker is not installed. See https://docs.docker.com/get-docker/"
-
-if ! docker compose version >/dev/null 2>&1; then
- error "Docker Compose v2 is required. Update Docker Desktop or run: apt install docker-compose-plugin"
-fi
-
-DOCKER_VERSION=$(docker version --format '{{.Server.Version}}' 2>/dev/null || echo "unknown")
-info "Docker ${DOCKER_VERSION} — OK"
-info "Docker Compose — OK"
-
-# ── Create install directory ──────────────────────────────────────────────────
-if [[ -d "${INSTALL_DIR}" ]]; then
- warn "Directory ${INSTALL_DIR} already exists — checking for existing installation..."
- IDEMPOTENT=true
-else
- mkdir -p "${INSTALL_DIR}"
- IDEMPOTENT=false
-fi
-
-cd "${INSTALL_DIR}"
-
-# ── Download docker-compose.yml ───────────────────────────────────────────────
-if [[ -f "docker-compose.yml" && "${IDEMPOTENT}" == "true" ]]; then
- warn "docker-compose.yml already exists — skipping download"
-else
- info "Downloading docker-compose.yml..."
- if command -v curl >/dev/null 2>&1; then
- curl -sSL "${COMPOSE_FILE_URL}" -o docker-compose.yml
- elif command -v wget >/dev/null 2>&1; then
- wget -qO docker-compose.yml "${COMPOSE_FILE_URL}"
- else
- error "Neither curl nor wget is available. Install one and retry."
- fi
-fi
-
-# ── Generate secrets ──────────────────────────────────────────────────────────
-generate_secret() {
- if [[ -r /dev/urandom ]]; then
- tr -dc 'A-Za-z0-9' .env </dev/null 2>&1; then
- echo -e " ${GREEN}OK${RESET}"
- return 0
- fi
- printf "."
- sleep "${delay}"
- (( attempt++ ))
- done
- echo -e " ${RED}TIMEOUT${RESET}"
- return 1
-}
-
-NODE_URL="http://localhost:${NODE_PORT:-8001}"
-RESOLVER_URL="http://localhost:${RESOLVER_PORT:-8003}"
-
-info "Waiting for services to become healthy..."
-poll_health "node" "${NODE_URL}/health" || error "Node failed to become healthy. Run: docker compose logs node"
-poll_health "resolver" "${RESOLVER_URL}/health" || warn "Resolver health check timed out (may still be starting)"
-
-# ── Install odal CLI (optional) ─────────────────────────────────────────────
-if [[ "${INSTALL_CLI}" == "true" ]]; then
- if command -v odal >/dev/null 2>&1; then
- info "odal CLI already installed ($(odal --version 2>/dev/null || echo 'unknown version'))"
- else
- info "Installing odal CLI..."
- if command -v cargo >/dev/null 2>&1; then
- cargo install odal-cli --quiet 2>/dev/null && info "odal CLI installed via cargo" || warn "cargo install failed — skipping CLI"
- else
- ARCH=$(uname -m)
- OS=$(uname -s | tr '[:upper:]' '[:lower:]')
- BIN_URL="https://github.com/odal-node/dpp-engine/releases/download/${ODAL_VERSION}/odal-${OS}-${ARCH}"
- if command -v curl >/dev/null 2>&1; then
- if curl -sSLf "${BIN_URL}" -o /tmp/odal 2>/dev/null; then
- chmod +x /tmp/odal
- sudo mv /tmp/odal /usr/local/bin/odal 2>/dev/null || mv /tmp/odal "${HOME}/.local/bin/odal"
- info "odal CLI installed to /usr/local/bin/odal"
- else
- warn "Pre-built binary not available for ${OS}/${ARCH} — install manually with: cargo install odal-cli"
- fi
- fi
- fi
- fi
-fi
-
-# ── Summary ───────────────────────────────────────────────────────────────────
-echo
-echo -e "${BOLD}═══════════════════════════════════════════════════════${RESET}"
-echo -e "${BOLD} Odal Node installation complete!${RESET}"
-echo -e "${BOLD}═══════════════════════════════════════════════════════${RESET}"
-echo
-echo -e " Node API : ${GREEN}${NODE_URL}${RESET}"
-echo -e " Vault : ${NODE_URL}/vault"
-echo -e " Identity : ${NODE_URL}/identity"
-echo -e " Integrator: ${NODE_URL}/integrator"
-echo -e " Resolver : ${GREEN}${RESOLVER_URL}${RESET}"
-echo -e " Install dir : ${INSTALL_DIR}"
-echo
-if [[ -n "${API_KEY:-}" ]]; then
- echo -e " ${BOLD}API Key${RESET}: ${YELLOW}${API_KEY}${RESET}"
- echo -e " (saved to ${INSTALL_DIR}/.env — keep this secret)"
- echo
-fi
-echo -e " Next steps:"
-echo -e " odal status — verify all services are healthy"
-echo -e " odal import data.csv — import product passports"
-echo
-echo -e " Docs: https://docs.odal-node.io"
-echo