Index: scripts/install.sh
===================================================================
--- scripts/install.sh	(revision f8820484d6ba9a506b8ec384fa3a6d745c4fddfb)
+++ scripts/install.sh	(revision 2dd1dc40db2f7dab2c095eeb309e5aaa02369b4b)
@@ -28,4 +28,10 @@
 KLONKT_BRANCH="${KLONKT_BRANCH:-stable}"
 KLONKT_DIR="${KLONKT_DIR:-/opt/klonkt}"
+# Where instance data lives, one directory per slug. The code in KLONKT_DIR is
+# shared; everything an instance writes stays under here.
+KLONKT_DATA_ROOT="${KLONKT_DATA_ROOT:-/var/lib/klonkt}"
+# Short name for this instance: its directory under the data root and its
+# systemd unit (klonkt@<slug>). Derived from the domain when left empty.
+KLONKT_SLUG="${KLONKT_SLUG:-}"
 KLONKT_USER="${KLONKT_USER:-klonkt}"
 KLONKT_PORT="${KLONKT_PORT:-3000}"
@@ -164,7 +170,42 @@
   git clone --depth 1 --branch "$KLONKT_BRANCH" "$KLONKT_REPO" "$KLONKT_DIR"
 fi
-mkdir -p "$KLONKT_DIR/storage/media" "$KLONKT_DIR/storage/audio"
 chown -R "$KLONKT_USER:$KLONKT_USER" "$KLONKT_DIR"
 ok "code in $KLONKT_DIR"
+
+# --- where this instance keeps its data -------------------------------------
+# New installs put data in /var/lib/klonkt/<slug> so the checkout stays free of
+# user data and can be shared by more instances later. An install that already
+# has its .env inside the checkout is left exactly as it is: re-running the
+# installer must never move a live database. Convert those deliberately with
+# scripts/klonkt-migrate-data.sh.
+if [ -z "$KLONKT_SLUG" ]; then
+  KLONKT_SLUG="$(printf '%s' "${KLONKT_DOMAIN:-default}" | sed 's/^www\.//' | cut -d. -f1 \
+                 | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9._-')"
+  [ -n "$KLONKT_SLUG" ] || KLONKT_SLUG=default
+fi
+# A database in the checkout but no .env is too ambiguous to guess at: refuse,
+# rather than start a fresh empty instance beside data nobody is reading.
+if [ ! -f "$KLONKT_DIR/.env" ] && [ -f "$KLONKT_DIR/storage/database.sqlite" ]; then
+  die "found $KLONKT_DIR/storage/database.sqlite but no .env next to it.
+   Put the .env back and re-run, or move the old storage/ aside first."
+fi
+if [ -f "$KLONKT_DIR/.env" ]; then
+  LAYOUT=legacy
+  ENV="$KLONKT_DIR/.env"
+  DATA_DIR="$KLONKT_DIR/storage"
+  SERVICE="klonkt"
+  mkdir -p "$DATA_DIR/media" "$DATA_DIR/audio"
+  chown -R "$KLONKT_USER:$KLONKT_USER" "$DATA_DIR"
+  ok "existing layout kept (data inside $KLONKT_DIR; split it with scripts/klonkt-migrate-data.sh)"
+else
+  LAYOUT=split
+  DATA_DIR="$KLONKT_DATA_ROOT/$KLONKT_SLUG"
+  ENV="$DATA_DIR/.env"
+  SERVICE="klonkt@${KLONKT_SLUG}"
+  mkdir -p "$DATA_DIR/media" "$DATA_DIR/audio"
+  chown -R "$KLONKT_USER:$KLONKT_USER" "$DATA_DIR"
+  chmod 750 "$DATA_DIR"
+  ok "data in $DATA_DIR (instance '$KLONKT_SLUG')"
+fi
 
 log "Installing dependencies (npm ci)…"
@@ -173,5 +214,4 @@
 
 log ".env…"
-ENV="$KLONKT_DIR/.env"
 if [ ! -f "$ENV" ]; then
   SECRET="$(openssl rand -hex 32)"
@@ -183,7 +223,10 @@
     echo "HOST=127.0.0.1"
     echo "SESSION_SECRET=${SECRET}"
-    echo "DATABASE_PATH=./storage/database.sqlite"
-    echo "MEDIA_PATH=./storage/media"
-    echo "AUDIO_PATH=./storage/audio"
+    # Absolute, so the app does not depend on its working directory and the
+    # data can sit outside the checkout. Media subdirectories (avatars,
+    # post-images, ...) follow MEDIA_PATH by themselves.
+    echo "DATABASE_PATH=${DATA_DIR}/database.sqlite"
+    echo "MEDIA_PATH=${DATA_DIR}/media"
+    echo "AUDIO_PATH=${DATA_DIR}/audio"
     echo "PUBLIC_BASE_URL=https://${KLONKT_DOMAIN}"
     [ -n "$KLONKT_LANG" ] && echo "KLONKT_DEFAULT_LANG=${KLONKT_LANG}"
@@ -201,5 +244,20 @@
 log "systemd service…"
 NODE_BIN="$(command -v node)"
-cat > /etc/systemd/system/klonkt.service <<EOF
+if [ "$LAYOUT" = split ]; then
+  # One template, one service per instance. Adding a site later is a data
+  # directory plus an .env, with no second copy of the code.
+  sed -e "s#^User=klonkt\$#User=${KLONKT_USER}#" \
+      -e "s#^Group=klonkt\$#Group=${KLONKT_USER}#" \
+      -e "s#^WorkingDirectory=/opt/klonkt\$#WorkingDirectory=${KLONKT_DIR}#" \
+      -e "s#^EnvironmentFile=/var/lib/klonkt/%i/.env\$#EnvironmentFile=${KLONKT_DATA_ROOT}/%i/.env#" \
+      -e "s#^ReadWritePaths=/var/lib/klonkt/%i\$#ReadWritePaths=${KLONKT_DATA_ROOT}/%i#" \
+      -e "s#^ExecStart=/usr/bin/node src/server.js\$#ExecStart=${NODE_BIN} src/server.js#" \
+      "$KLONKT_DIR/deploy/klonkt@.service" > /etc/systemd/system/klonkt@.service
+  chmod 0644 /etc/systemd/system/klonkt@.service
+  systemctl daemon-reload
+  systemctl enable --now "klonkt@${KLONKT_SLUG}"
+  ok "klonkt@${KLONKT_SLUG} running on 127.0.0.1:${KLONKT_PORT}"
+else
+  cat > /etc/systemd/system/klonkt.service <<EOF
 [Unit]
 Description=Klonkt
@@ -222,7 +280,8 @@
 WantedBy=multi-user.target
 EOF
-systemctl daemon-reload
-systemctl enable --now klonkt
-ok "klonkt.service running on 127.0.0.1:${KLONKT_PORT}"
+  systemctl daemon-reload
+  systemctl enable --now klonkt
+  ok "klonkt.service running on 127.0.0.1:${KLONKT_PORT}"
+fi
 
 if [ -z "$NO_CADDY" ]; then
@@ -261,6 +320,19 @@
   runuser -u ${KLONKT_USER} -- env HOME="\$D" bash -c "cd '\$D' && npm ci --omit=dev"
 fi
-systemctl restart klonkt
-echo "Klonkt updated (\$A) + restarted."
+# Restart every instance. Each directory under the data root with an .env is one
+# instance sharing this checkout. An install that has not been split yet has no
+# such directories and still runs the single klonkt.service.
+N=0
+for d in ${KLONKT_DATA_ROOT}/*/; do
+  [ -f "\$d/.env" ] || continue
+  s=\$(basename "\$d")
+  systemctl restart "klonkt@\$s" && N=\$((N+1))
+done
+if [ "\$N" = 0 ]; then
+  systemctl restart klonkt
+  echo "Klonkt updated (\$A) + restarted."
+else
+  echo "Klonkt updated (\$A) + restarted \$N instance(s)."
+fi
 EOF
 chmod +x /usr/local/bin/klonkt-update
@@ -286,6 +358,13 @@
 echo "  • First run:       go to /auth/register and create your admin account."
 echo
-echo "  Manage:  systemctl status klonkt · journalctl -u klonkt -f · klonkt-update"
+echo "  Manage:  systemctl status ${SERVICE} · journalctl -u ${SERVICE} -f · klonkt-update"
 echo "  Lost password: cd ${KLONKT_DIR} && runuser -u ${KLONKT_USER} -- env HOME=${KLONKT_DIR} npm run reset-admin"
+if [ "$LAYOUT" = split ]; then
+  echo
+  echo "  Code:  ${KLONKT_DIR}          shared, nothing of yours lives here"
+  echo "  Data:  ${DATA_DIR}   database, uploads and .env — back up this one"
+  echo "  Another site on this server, sharing the same code:"
+  echo "      sudo bash ${KLONKT_DIR}/scripts/klonkt-add-instance.sh <slug> <domain>"
+fi
 echo
 echo "  DNS: make sure A + AAAA of ${KLONKT_DOMAIN} point to this server."
