diff --git a/README.md b/README.md
index 03f02e540..ce2a1d349 100644
--- a/README.md
+++ b/README.md
@@ -168,6 +168,9 @@ The following flags (with their non-default values) can be passed to `cmake`:
> doxygen Doxyfile
> ```
+> [!TIP]
+> For a local Servatrice with no database, or a seeded MySQL development stack with real accounts and moderation fixtures, see [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
+
# Run
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
new file mode 100644
index 000000000..6fa9feacf
--- /dev/null
+++ b/docker-compose.dev.yml
@@ -0,0 +1,27 @@
+services:
+ mysql:
+ image: mysql:8.4
+ command:
+ - --sql_mode=
+ - --character-set-server=utf8mb4
+ - --collation-server=utf8mb4_unicode_ci
+ environment:
+ MYSQL_ROOT_PASSWORD: root
+ MYSQL_DATABASE: servatrice
+ MYSQL_USER: servatrice
+ MYSQL_PASSWORD: password
+ ports:
+ - "127.0.0.1:3306:3306"
+ volumes:
+ - cockatrice-dev-mysql:/var/lib/mysql
+ - ./servatrice/servatrice.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
+ - ./servatrice/dev/seed.sql:/docker-entrypoint-initdb.d/02-seed.sql:ro
+ healthcheck:
+ test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -proot --silent"]
+ interval: 3s
+ timeout: 5s
+ retries: 30
+ start_period: 20s
+
+volumes:
+ cockatrice-dev-mysql:
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
new file mode 100644
index 000000000..68877ed61
--- /dev/null
+++ b/docs/DEVELOPMENT.md
@@ -0,0 +1,155 @@
+# Local development servers
+
+This page covers the two supported ways to run a local Servatrice for
+manual client and server work:
+
+- **Guest profile** — no database, no mail server, two rooms and chat. Best for
+ client UI work, room/chat flows, and quick server regressions.
+- **Seeded MySQL stack** — real accounts, staff roles, moderation fixtures, and
+ SQL-backed rooms. Best for authentication, permissions, reports, replay, and
+ migration work.
+
+Both paths run the binaries from your local build. Neither changes production
+configuration.
+
+## Build the server and client
+
+From the repository root:
+
+```bash
+cmake -S . -B build/test -DCMAKE_BUILD_TYPE=Debug -DWITH_SERVER=1 -DTEST=1
+cmake --build build/test -j 24
+```
+
+`build/test` contains both `servatrice` and `cockatrice`. `dev-servatrice.sh`
+prefers `build/test`; `dev-client.sh` prefers the client-only `build/debug` tree
+and falls back to `build/test`.
+
+## Guest profile (no database)
+
+Start the server:
+
+```bash
+./scripts/dev-servatrice.sh
+```
+
+The script runs `servatrice/dev.ini`, which listens on `127.0.0.1:4747`, accepts
+guest logins, and exposes the **General** and **Testing** rooms with the
+`general`, `moderators`, `developer-news`, and `testing` channels.
+
+Start one or more clients in separate terminals:
+
+```bash
+./scripts/dev-client.sh
+./scripts/dev-client.sh --profile second --connect devplayer2@127.0.0.1:4747
+```
+
+Each profile gets isolated `XDG_DATA_HOME`, `XDG_CACHE_HOME`, and
+`XDG_CONFIG_HOME` directories under `build/dev/profiles/`, so profiles do not
+share settings or card databases. A second plain launch runs independently
+rather than handing off to the first. Delete a profile directory to reset it.
+
+Useful script options:
+
+```bash
+./scripts/dev-servatrice.sh --port 14747
+./scripts/dev-servatrice.sh --config /path/to/servatrice.ini
+./scripts/dev-client.sh --no-connect --profile scratch
+```
+
+Any argument the scripts do not recognise is passed through to the binary
+unchanged.
+
+## Seeded MySQL stack
+
+The stack is defined in `docker-compose.dev.yml`. On first start, MySQL applies
+`servatrice/servatrice.sql` and then `servatrice/dev/seed.sql`.
+
+```bash
+./scripts/dev-db.sh up
+./scripts/dev-servatrice.sh --mysql
+```
+
+Then connect with any seeded account. The password for every seeded account is
+`password`:
+
+| Account | Role |
+| ------------ | ----------------------------- |
+| `devadmin` | admin (implies moderator) |
+| `devmod` | moderator |
+| `devjudge` | judge |
+| `devdev` | developer |
+| `devplayer` | registered player |
+| `devplayer2` | registered player |
+
+`--mysql` exports `DATABASE_URL`, which Servatrice already honours: it selects
+MySQL, forces the `cockatrice` table prefix and SQL-backed rooms, and switches
+authentication to `sql`. No ini edit is needed.
+
+Seeded fixtures also include:
+
+- rooms `General` and `Testing`, their game types, and the same channels as the
+ guest profile;
+- an open report and a resolved report, with a moderator comment;
+- a warning against `devplayer2` and a permanent ban on `192.0.2.10`;
+- a buddy/ignore entry and one deck in `devplayer`'s deck storage;
+- two chat log rows in `General`.
+
+### Managing the database
+
+```bash
+./scripts/dev-db.sh up # start MySQL, wait for health
+./scripts/dev-db.sh seed # re-apply the fixtures
+./scripts/dev-db.sh reset # drop the volume and start fresh
+./scripts/dev-db.sh stop # stop containers, keep the volume
+./scripts/dev-db.sh url # print DATABASE_URL
+```
+
+The schema and seed are not re-run automatically on an existing volume. Use
+`reset` when `servatrice/servatrice.sql` changes. Do not apply the schema file a
+second time by hand: its `cockatrice_schema_version` insert is not idempotent.
+The fixture file itself is idempotent and can be re-applied while iterating.
+
+### External MySQL
+
+Set `COCKATRICE_DEV_DATABASE_URL` to skip the compose stack:
+
+```bash
+export COCKATRICE_DEV_DATABASE_URL='mysql://user:password@127.0.0.1/database'
+./scripts/dev-servatrice.sh --mysql
+```
+
+`up`, `seed`, and `reset` only manage the compose stack. For an external
+database, apply `servatrice/servatrice.sql` and `servatrice/dev/seed.sql`
+yourself.
+
+> [!NOTE]
+> Servatrice reads the host, database, user, and password from `DATABASE_URL`,
+> but not its port: MySQL connections use port 3306. Keep the compose stack on
+> 3306 or configure the database through the ini file instead.
+
+## Per-run configuration overrides
+
+`servatrice` accepts `--set key=value` (repeatable) and `--print-config`:
+
+```bash
+./scripts/dev-servatrice.sh --port 4747 -- --set security/max_users_per_address=50
+./build/test/servatrice/servatrice --config servatrice/dev.ini --print-config
+```
+
+Overrides live only in the running process; the ini file is never written.
+`--print-config` includes override-only keys and redacts values whose key
+contains `password`, `secret`, or `legacy_backup_key`.
+
+## Troubleshooting
+
+- **Port already in use** — pass `--port` to the server and `--connect` to the
+ clients, or stop the other instance.
+- **MySQL port 3306 already in use** — stop the other MySQL instance, or run the
+ server against it by setting `database/*` in a custom ini file. `DATABASE_URL`
+ does not carry a port.
+- **Client opens with stale data** — remove `build/dev/profiles//`.
+- **Server cannot open the database** — the compose volume may predate a schema
+ change; run `./scripts/dev-db.sh reset`.
+- **`servatrice` target missing** — the client-only `build/debug` tree does not
+ contain it; build `build/test` with `-DWITH_SERVER=1`.
diff --git a/scripts/dev-client.sh b/scripts/dev-client.sh
new file mode 100755
index 000000000..91d481985
--- /dev/null
+++ b/scripts/dev-client.sh
@@ -0,0 +1,127 @@
+#!/usr/bin/env bash
+#
+# Run a Cockatrice client with disposable, per-profile settings.
+#
+# Every profile gets its own XDG data, cache, and config directories under
+# build/dev/profiles//, so several clients can run side by side with
+# separate settings and card databases.
+#
+# Unrecognised arguments are passed through to the client unchanged.
+
+set -euo pipefail
+
+repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+profile="default"
+connect="${COCKATRICE_DEV_CONNECT:-devplayer@127.0.0.1:4747}"
+no_connect=0
+passthrough=()
+
+usage() {
+ cat <<'EOF'
+Usage: scripts/dev-client.sh [options] [-- client-options...]
+
+Options:
+ --profile NAME Isolated settings profile (default: default)
+ --connect URL Connect on startup (default: devplayer@127.0.0.1:4747)
+ --no-connect Start without connecting anywhere
+ -h, --help Show this help
+
+Environment:
+ COCKATRICE_BIN Client binary to run
+ COCKATRICE_DEV_HOME Base directory for profiles (default: build/dev)
+ COCKATRICE_DEV_CONNECT Default --connect value
+
+Examples:
+ scripts/dev-client.sh
+ scripts/dev-client.sh --profile second --connect devplayer2@127.0.0.1:4747
+ scripts/dev-client.sh --no-connect --profile scratch
+EOF
+}
+
+die() {
+ echo "dev-client: $*" >&2
+ exit 1
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --profile)
+ [[ $# -ge 2 ]] || die "--profile needs a name"
+ profile="$2"
+ shift 2
+ ;;
+ --profile=*)
+ profile="${1#*=}"
+ shift
+ ;;
+ --connect)
+ [[ $# -ge 2 ]] || die "--connect needs a value"
+ connect="$2"
+ shift 2
+ ;;
+ --connect=*)
+ connect="${1#*=}"
+ shift
+ ;;
+ --no-connect)
+ no_connect=1
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ --)
+ shift
+ passthrough+=("$@")
+ break
+ ;;
+ *)
+ passthrough+=("$1")
+ shift
+ ;;
+ esac
+done
+
+[[ "$profile" =~ ^[A-Za-z0-9._-]+$ ]] || die "profile name may only contain letters, numbers, '.', '_', and '-'"
+
+if [[ -n "${COCKATRICE_BIN:-}" ]]; then
+ binary="$COCKATRICE_BIN"
+elif [[ -x "$repo_root/build/debug/cockatrice/cockatrice" ]]; then
+ binary="$repo_root/build/debug/cockatrice/cockatrice"
+elif [[ -x "$repo_root/build/test/cockatrice/cockatrice" ]]; then
+ binary="$repo_root/build/test/cockatrice/cockatrice"
+else
+ die "no cockatrice binary found; build it with:
+ cmake -S . -B build/debug -DCMAKE_BUILD_TYPE=Debug -DWITH_SERVER=0 -DWITH_GUI_TEST=ON
+ cmake --build build/debug -j 24"
+fi
+
+base="${COCKATRICE_DEV_HOME:-$repo_root/build/dev}"
+profile_dir="$base/profiles/$profile"
+
+export XDG_DATA_HOME="$profile_dir/data"
+export XDG_CACHE_HOME="$profile_dir/cache"
+export XDG_CONFIG_HOME="$profile_dir/config"
+mkdir -p "$XDG_DATA_HOME" "$XDG_CACHE_HOME" "$XDG_CONFIG_HOME"
+
+args=()
+if [[ "$no_connect" -eq 0 ]]; then
+ has_connect=0
+ for arg in "${passthrough[@]}"; do
+ case "$arg" in
+ --connect|--connect=*|-c) has_connect=1 ;;
+ esac
+ done
+ if [[ "$has_connect" -eq 0 ]]; then
+ args+=(--connect "$connect")
+ echo "Connecting to: $connect"
+ fi
+else
+ args+=(--no-connect)
+fi
+args+=("${passthrough[@]}")
+
+echo "Client profile: $profile ($profile_dir)"
+
+exec "$binary" "${args[@]}"
diff --git a/scripts/dev-db.sh b/scripts/dev-db.sh
new file mode 100755
index 000000000..5d4a67cd1
--- /dev/null
+++ b/scripts/dev-db.sh
@@ -0,0 +1,117 @@
+#!/usr/bin/env bash
+#
+# Manage the seeded MySQL development database.
+#
+# The stack is defined in docker-compose.dev.yml. On a fresh volume MySQL runs
+# servatrice/servatrice.sql and then servatrice/dev/seed.sql, so the database
+# starts with accounts, rooms, channels, and moderation fixtures already in
+# place.
+
+set -euo pipefail
+
+repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+compose_file="$repo_root/docker-compose.dev.yml"
+seed_file="$repo_root/servatrice/dev/seed.sql"
+url="${COCKATRICE_DEV_DATABASE_URL:-mysql://servatrice:password@127.0.0.1:3306/servatrice}"
+
+usage() {
+ cat <<'EOF'
+Usage: scripts/dev-db.sh
+
+Commands:
+ up Start MySQL and wait until it is healthy
+ seed Re-apply servatrice/dev/seed.sql to the running database
+ reset Remove the container and volume, then start a fresh seeded database
+ stop Stop the container, keeping the database volume
+ url Print the DATABASE_URL servatrice expects
+
+Environment:
+ COCKATRICE_DEV_DATABASE_URL Use an external MySQL instead of the compose stack
+
+Examples:
+ scripts/dev-db.sh up
+ scripts/dev-db.sh seed
+ scripts/dev-db.sh reset
+EOF
+}
+
+die() {
+ echo "dev-db: $*" >&2
+ exit 1
+}
+
+compose=()
+detect_compose() {
+ if docker compose version >/dev/null 2>&1; then
+ compose=(docker compose)
+ elif command -v docker-compose >/dev/null 2>&1; then
+ compose=(docker-compose)
+ else
+ die "Docker Compose is not available; install the docker compose plugin or set COCKATRICE_DEV_DATABASE_URL"
+ fi
+}
+
+compose_cmd() {
+ "${compose[@]}" -f "$compose_file" "$@"
+}
+
+command="${1:-}"
+[[ -n "$command" ]] || {
+ usage
+ exit 1
+}
+
+case "$command" in
+ url)
+ echo "$url"
+ ;;
+ up)
+ if [[ -n "${COCKATRICE_DEV_DATABASE_URL:-}" ]]; then
+ echo "Using external database: $url"
+ echo "Apply the schema and seed yourself if the database is empty:"
+ echo " mysql < servatrice/servatrice.sql"
+ echo " mysql < servatrice/dev/seed.sql"
+ exit 0
+ fi
+ detect_compose
+ compose_cmd up -d --wait
+ echo
+ echo "MySQL is ready."
+ echo "DATABASE_URL=$(bash "$repo_root/scripts/dev-db.sh" url)"
+ echo "Start the server with: scripts/dev-servatrice.sh --mysql"
+ ;;
+ seed)
+ [[ -f "$seed_file" ]] || die "seed file not found: $seed_file"
+ if [[ -n "${COCKATRICE_DEV_DATABASE_URL:-}" ]]; then
+ die "seed is only automated for the compose stack; apply $seed_file manually for an external database"
+ fi
+ detect_compose
+ compose_cmd exec -T mysql mysql -uservatrice -ppassword servatrice < "$seed_file"
+ echo "Seed applied."
+ ;;
+ reset)
+ if [[ -n "${COCKATRICE_DEV_DATABASE_URL:-}" ]]; then
+ die "reset only manages the compose stack; reset the external database yourself"
+ fi
+ detect_compose
+ compose_cmd down -v
+ compose_cmd up -d --wait
+ echo
+ echo "Fresh seeded database ready."
+ echo "Start the server with: scripts/dev-servatrice.sh --mysql"
+ ;;
+ stop)
+ if [[ -n "${COCKATRICE_DEV_DATABASE_URL:-}" ]]; then
+ die "stop only manages the compose stack"
+ fi
+ detect_compose
+ compose_cmd down
+ ;;
+ -h|--help|help)
+ usage
+ ;;
+ *)
+ usage
+ die "unknown command: $command"
+ ;;
+esac
diff --git a/scripts/dev-servatrice.sh b/scripts/dev-servatrice.sh
new file mode 100755
index 000000000..c8e74282b
--- /dev/null
+++ b/scripts/dev-servatrice.sh
@@ -0,0 +1,148 @@
+#!/usr/bin/env bash
+#
+# Run a local Servatrice with the checked-in development profile.
+#
+# The default profile needs no database: it listens on localhost, accepts
+# guest logins, and exposes the General and Testing rooms from
+# servatrice/dev.ini. Pass --mysql to run against the seeded development
+# database managed by scripts/dev-db.sh.
+#
+# Any unrecognised argument is passed through to servatrice unchanged, so
+# options such as --set key=value or --test-hash keep working.
+
+set -euo pipefail
+
+repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+config="$repo_root/servatrice/dev.ini"
+database_url="${DATABASE_URL:-}"
+port=""
+log_to_console=1
+passthrough=()
+
+usage() {
+ cat <<'EOF'
+Usage: scripts/dev-servatrice.sh [options] [-- servatrice-options...]
+
+Options:
+ --config FILE Configuration file (default: servatrice/dev.ini)
+ --port PORT Listen on PORT instead of the configured port
+ --mysql[=URL] Use the seeded MySQL dev database
+ --database-url URL Use an explicit mysql:// URL
+ --no-log-to-console Do not force --log-to-console
+ -h, --help Show this help
+
+Environment:
+ SERVATRICE_BIN Servatrice binary to run
+ DATABASE_URL Database URL used by --mysql when no URL is given
+
+Examples:
+ scripts/dev-servatrice.sh
+ scripts/dev-servatrice.sh --port 14747
+ scripts/dev-db.sh up && scripts/dev-servatrice.sh --mysql
+EOF
+}
+
+die() {
+ echo "dev-servatrice: $*" >&2
+ exit 1
+}
+
+while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --config)
+ [[ $# -ge 2 ]] || die "--config needs a file"
+ config="$2"
+ shift 2
+ ;;
+ --config=*)
+ config="${1#*=}"
+ shift
+ ;;
+ --port)
+ [[ $# -ge 2 ]] || die "--port needs a value"
+ port="$2"
+ shift 2
+ ;;
+ --port=*)
+ port="${1#*=}"
+ shift
+ ;;
+ --mysql)
+ database_url="${DATABASE_URL:-$(bash "$repo_root/scripts/dev-db.sh" url)}"
+ shift
+ ;;
+ --mysql=*)
+ database_url="${1#*=}"
+ shift
+ ;;
+ --database-url)
+ [[ $# -ge 2 ]] || die "--database-url needs a value"
+ database_url="$2"
+ shift 2
+ ;;
+ --database-url=*)
+ database_url="${1#*=}"
+ shift
+ ;;
+ --no-log-to-console)
+ log_to_console=0
+ shift
+ ;;
+ -h|--help)
+ usage
+ exit 0
+ ;;
+ --)
+ shift
+ passthrough+=("$@")
+ break
+ ;;
+ *)
+ passthrough+=("$1")
+ shift
+ ;;
+ esac
+done
+
+[[ -f "$config" ]] || die "configuration file not found: $config"
+
+if [[ -n "${SERVATRICE_BIN:-}" ]]; then
+ binary="$SERVATRICE_BIN"
+elif [[ -x "$repo_root/build/test/servatrice/servatrice" ]]; then
+ binary="$repo_root/build/test/servatrice/servatrice"
+elif [[ -x "$repo_root/build/debug/servatrice/servatrice" ]]; then
+ binary="$repo_root/build/debug/servatrice/servatrice"
+else
+ die "no servatrice binary found; build it with:
+ cmake -S . -B build/test -DCMAKE_BUILD_TYPE=Debug -DWITH_SERVER=1 -DTEST=1
+ cmake --build build/test -j 24"
+fi
+
+args=(--config "$config")
+[[ "$log_to_console" -eq 1 ]] && args+=(--log-to-console)
+[[ -n "$port" ]] && args+=("--set" "server/port=$port")
+args+=("${passthrough[@]}")
+
+# Resolve the effective host and port through the binary itself, so a port in
+# the ini file and --set overrides are both reflected in the printed command.
+print_args=(--config "$config")
+[[ -n "$port" ]] && print_args+=(--set "server/port=$port")
+effective_config="$("$binary" "${print_args[@]}" --print-config 2>/dev/null || true)"
+effective_port="$(printf '%s\n' "$effective_config" | awk -F= '$1 == "server/port" { print $2; exit }')"
+effective_host="$(printf '%s\n' "$effective_config" | awk -F= '$1 == "server/host" { print $2; exit }')"
+effective_port="${effective_port:-${port:-4747}}"
+if [[ -z "$effective_host" || "$effective_host" == "any" ]]; then
+ effective_host="127.0.0.1"
+fi
+
+if [[ -n "$database_url" ]]; then
+ export DATABASE_URL="$database_url"
+ redacted_url="$(printf '%s' "$database_url" | sed -E 's#(://[^:/@]+):[^@]*@#\1:***@#')"
+ echo "Database: $redacted_url"
+fi
+
+echo "Servatrice listening on ${effective_host}:${effective_port}"
+echo "Connect with: scripts/dev-client.sh --connect devplayer@${effective_host}:${effective_port}"
+echo
+
+exec "$binary" "${args[@]}"
diff --git a/servatrice/dev.ini b/servatrice/dev.ini
new file mode 100644
index 000000000..23e297cb8
--- /dev/null
+++ b/servatrice/dev.ini
@@ -0,0 +1,104 @@
+; Servatrice development configuration.
+;
+; This profile needs no database and no mail server: it starts a
+; localhost-only server that accepts guest logins, two rooms, and chat
+; channels. Use scripts/dev-servatrice.sh to run it; the script can layer
+; per-run overrides on top without editing this file.
+;
+; For registered accounts, roles, moderation data, and SQL, see
+; docs/DEVELOPMENT.md and scripts/dev-db.sh instead.
+
+[server]
+name="Cockatrice dev server"
+id=1
+host=127.0.0.1
+port=4747
+number_pools=1
+websocket_number_pools=0
+statusupdate=0
+writelog=0
+; An empty logfile keeps the repository clean; console output still works.
+logfile=
+clientkeepalive=1
+max_player_inactivity_time=15
+idleclienttimeout=0
+officialwarnings="Abusive Language|1,Causing Drama|1,Cheating|2,Spamming|1,Targeted Harassment|2"
+
+[authentication]
+method=none
+regonly=false
+
+[registration]
+enabled=false
+
+[users]
+minnamelength=3
+maxnamelength=35
+allowlowercase=true
+allowuppercase=true
+allownumerics=true
+allowedpunctuation=_.-
+allowpunctuationprefix=false
+disallowedwords=""
+disallowedregexp=""
+minpasswordlength=6
+
+[database]
+type=none
+
+[rooms]
+method=config
+roomlist\size=2
+
+roomlist\1\name="General"
+roomlist\1\description="Play anything here."
+roomlist\1\permissionlevel=none
+roomlist\1\privilegelevel=none
+roomlist\1\autojoin=true
+roomlist\1\joinmessage=""
+roomlist\1\chathistorysize=100
+roomlist\1\game_types\size=1
+roomlist\1\game_types\1\name="Standard"
+roomlist\1\channels\size=3
+roomlist\1\channels\1\id=general
+roomlist\1\channels\1\displayname="General"
+roomlist\1\channels\1\access=public
+roomlist\1\channels\2\id=moderators
+roomlist\1\channels\2\displayname="Moderators"
+roomlist\1\channels\2\access=moderator
+roomlist\1\channels\3\id=developer-news
+roomlist\1\channels\3\displayname="Developer news"
+roomlist\1\channels\3\access=public
+roomlist\1\channels\3\writeaccess=developer
+roomlist\1\channels\3\banner="Official announcements from the developers."
+
+roomlist\2\name="Testing"
+roomlist\2\description="Scratch room for server and client work."
+roomlist\2\permissionlevel=none
+roomlist\2\privilegelevel=none
+roomlist\2\autojoin=true
+roomlist\2\joinmessage=""
+roomlist\2\chathistorysize=100
+roomlist\2\game_types\size=2
+roomlist\2\game_types\1\name="Standard"
+roomlist\2\game_types\2\name="Commander"
+roomlist\2\channels\size=2
+roomlist\2\channels\1\id=testing
+roomlist\2\channels\1\displayname="Testing"
+roomlist\2\channels\1\access=public
+roomlist\2\channels\2\id=moderators
+roomlist\2\channels\2\displayname="Moderators"
+roomlist\2\channels\2\access=moderator
+
+[game]
+max_game_inactivity_time=120
+store_replays=false
+allow_create_as_judge=false
+
+[security]
+max_users_per_address=20
+max_login_attempts_per_ip=0
+max_registrations_per_ip=0
+
+[smtp]
+enableinternalsmtpclient=false
diff --git a/servatrice/dev/seed.sql b/servatrice/dev/seed.sql
new file mode 100644
index 000000000..9a72b005f
--- /dev/null
+++ b/servatrice/dev/seed.sql
@@ -0,0 +1,159 @@
+-- Development fixtures for a local Servatrice database.
+--
+-- Accounts, rooms, channels, moderation data, and a deck are all keyed by
+-- stable names or fixed ids, so this file can be applied repeatedly while
+-- iterating. Passwords are the scrypt verifier for the literal password
+-- "password"; regenerate them with PasswordHasher if that ever changes.
+--
+-- This file is loaded automatically by docker-compose.dev.yml on a fresh
+-- volume, and can be re-applied with scripts/dev-db.sh seed.
+
+-- Accounts -----------------------------------------------------------------
+-- admin bitfield: 1 = admin (implies moderator), 2 = moderator,
+-- 4 = judge, 8 = developer.
+INSERT INTO cockatrice_users
+ (admin, name, realname, password_sha512, email, country, avatar_bmp,
+ registrationDate, active, clientid, adminnotes, privlevel,
+ privlevelStartDate, privlevelEndDate, passwordLastChangedDate)
+VALUES
+ (15, 'devadmin', 'Dev Admin', '$scrypt$32768$8$1$Y29ja2F0cmljZS1kZXYhIQ==$L1m5vF9LgraerY880GvTPNdNpuYzmE4/My4wa7q6wTa3+QuoYeTpYERPhnH9I2WXBNLUrrvdYaCrOZ6EuI8ofQ==', 'devadmin@example.invalid', 'US', '', '2026-01-01 00:00:00', 1, '', 'Seeded development administrator.', 'NONE', '2026-01-01 00:00:00', '2026-01-01 00:00:00', '2026-01-01 00:00:00'),
+ (2, 'devmod', 'Dev Moderator', '$scrypt$32768$8$1$Y29ja2F0cmljZS1kZXYhIQ==$L1m5vF9LgraerY880GvTPNdNpuYzmE4/My4wa7q6wTa3+QuoYeTpYERPhnH9I2WXBNLUrrvdYaCrOZ6EuI8ofQ==', 'devmod@example.invalid', 'US', '', '2026-01-01 00:00:00', 1, '', 'Seeded development moderator.', 'NONE', '2026-01-01 00:00:00', '2026-01-01 00:00:00', '2026-01-01 00:00:00'),
+ (4, 'devjudge', 'Dev Judge', '$scrypt$32768$8$1$Y29ja2F0cmljZS1kZXYhIQ==$L1m5vF9LgraerY880GvTPNdNpuYzmE4/My4wa7q6wTa3+QuoYeTpYERPhnH9I2WXBNLUrrvdYaCrOZ6EuI8ofQ==', 'devjudge@example.invalid', 'US', '', '2026-01-01 00:00:00', 1, '', 'Seeded development judge.', 'NONE', '2026-01-01 00:00:00', '2026-01-01 00:00:00', '2026-01-01 00:00:00'),
+ (8, 'devdev', 'Dev Developer', '$scrypt$32768$8$1$Y29ja2F0cmljZS1kZXYhIQ==$L1m5vF9LgraerY880GvTPNdNpuYzmE4/My4wa7q6wTa3+QuoYeTpYERPhnH9I2WXBNLUrrvdYaCrOZ6EuI8ofQ==', 'devdev@example.invalid', 'US', '', '2026-01-01 00:00:00', 1, '', 'Seeded development developer.', 'NONE', '2026-01-01 00:00:00', '2026-01-01 00:00:00', '2026-01-01 00:00:00'),
+ (0, 'devplayer', 'Dev Player', '$scrypt$32768$8$1$Y29ja2F0cmljZS1kZXYhIQ==$L1m5vF9LgraerY880GvTPNdNpuYzmE4/My4wa7q6wTa3+QuoYeTpYERPhnH9I2WXBNLUrrvdYaCrOZ6EuI8ofQ==', 'devplayer@example.invalid', 'US', '', '2026-01-01 00:00:00', 1, '', 'Seeded development player.', 'NONE', '2026-01-01 00:00:00', '2026-01-01 00:00:00', '2026-01-01 00:00:00'),
+ (0, 'devplayer2', 'Dev Player Two', '$scrypt$32768$8$1$Y29ja2F0cmljZS1kZXYhIQ==$L1m5vF9LgraerY880GvTPNdNpuYzmE4/My4wa7q6wTa3+QuoYeTpYERPhnH9I2WXBNLUrrvdYaCrOZ6EuI8ofQ==', 'devplayer2@example.invalid', 'US', '', '2026-01-01 00:00:00', 1, '', 'Seeded development player.', 'NONE', '2026-01-01 00:00:00', '2026-01-01 00:00:00', '2026-01-01 00:00:00')
+ON DUPLICATE KEY UPDATE
+ admin = VALUES(admin),
+ realname = VALUES(realname),
+ password_sha512 = VALUES(password_sha512),
+ email = VALUES(email),
+ active = VALUES(active),
+ adminnotes = VALUES(adminnotes);
+
+SET @devadmin_id = (SELECT id FROM cockatrice_users WHERE name = 'devadmin');
+SET @devmod_id = (SELECT id FROM cockatrice_users WHERE name = 'devmod');
+SET @devjudge_id = (SELECT id FROM cockatrice_users WHERE name = 'devjudge');
+SET @devdev_id = (SELECT id FROM cockatrice_users WHERE name = 'devdev');
+SET @devplayer_id = (SELECT id FROM cockatrice_users WHERE name = 'devplayer');
+SET @devplayer2_id = (SELECT id FROM cockatrice_users WHERE name = 'devplayer2');
+
+-- Rooms and channels -------------------------------------------------------
+INSERT INTO cockatrice_rooms
+ (id, name, descr, permissionlevel, privlevel, auto_join, join_message, chat_history_size, id_server)
+VALUES
+ (1, 'General', 'Play anything here.', 'NONE', 'NONE', 1, '', 100, 1),
+ (2, 'Testing', 'Scratch room for server and client work.', 'NONE', 'NONE', 1, '', 100, 1)
+ON DUPLICATE KEY UPDATE
+ name = VALUES(name),
+ descr = VALUES(descr),
+ permissionlevel = VALUES(permissionlevel),
+ privlevel = VALUES(privlevel),
+ auto_join = VALUES(auto_join),
+ join_message = VALUES(join_message),
+ chat_history_size = VALUES(chat_history_size),
+ id_server = VALUES(id_server);
+
+DELETE FROM cockatrice_rooms_gametypes WHERE id_room IN (1, 2) AND id_server = 1;
+INSERT INTO cockatrice_rooms_gametypes (id_room, name, id_server) VALUES
+ (1, 'Standard', 1),
+ (2, 'Standard', 1),
+ (2, 'Commander', 1);
+
+DELETE FROM cockatrice_rooms_channels WHERE id_room IN (1, 2) AND id_server = 1;
+INSERT INTO cockatrice_rooms_channels
+ (id_room, id_server, channel_id, display_name, access_level, write_access_level, banner_message)
+VALUES
+ (1, 1, 'general', 'General', 'public', NULL, ''),
+ (1, 1, 'moderators', 'Moderators', 'moderator', NULL, ''),
+ (1, 1, 'developer-news', 'Developer news', 'public', 'developer', 'Official announcements from the developers.'),
+ (2, 1, 'testing', 'Testing', 'public', NULL, ''),
+ (2, 1, 'moderators', 'Moderators', 'moderator', NULL, '');
+
+-- Social lists -------------------------------------------------------------
+INSERT IGNORE INTO cockatrice_buddylist (id_user1, id_user2) VALUES (@devplayer_id, @devmod_id);
+INSERT IGNORE INTO cockatrice_ignorelist (id_user1, id_user2) VALUES (@devplayer_id, @devplayer2_id);
+
+-- Moderation fixtures ------------------------------------------------------
+INSERT INTO cockatrice_reports
+ (id, reporter_id, reporter_name, reported_user_id, reported_user_name, game_id, room_id,
+ category, description, chat_log, created_at, resolution_time, status, assigned_to,
+ resolved_by, resolution_note, notified)
+VALUES
+ (9001, @devplayer_id, 'devplayer', @devplayer2_id, 'devplayer2', NULL, 1,
+ 'cheating', 'Seeded open report: suspected cheat in a test game.', NULL,
+ '2026-01-02 12:00:00', NULL, 'open', NULL, NULL, NULL, 0),
+ (9002, @devplayer2_id, 'devplayer2', @devplayer_id, 'devplayer', NULL, 1,
+ 'spam', 'Seeded resolved report: repeated messages in General.', NULL,
+ '2026-01-02 13:00:00', '2026-01-02 15:00:00', 'resolved', @devmod_id,
+ @devmod_id, 'Resolved during development seeding.', 0)
+ON DUPLICATE KEY UPDATE
+ reporter_id = VALUES(reporter_id),
+ reporter_name = VALUES(reporter_name),
+ reported_user_id = VALUES(reported_user_id),
+ reported_user_name = VALUES(reported_user_name),
+ room_id = VALUES(room_id),
+ category = VALUES(category),
+ description = VALUES(description),
+ created_at = VALUES(created_at),
+ resolution_time = VALUES(resolution_time),
+ status = VALUES(status),
+ assigned_to = VALUES(assigned_to),
+ resolved_by = VALUES(resolved_by),
+ resolution_note = VALUES(resolution_note);
+
+INSERT INTO cockatrice_report_comments
+ (id, report_id, author_name, author_id, comment_text, created_at, is_moderator, notified)
+VALUES
+ (9001, 9001, 'devmod', @devmod_id, 'Looking into this now.', '2026-01-02 12:30:00', 1, 0)
+ON DUPLICATE KEY UPDATE
+ comment_text = VALUES(comment_text),
+ created_at = VALUES(created_at),
+ is_moderator = VALUES(is_moderator);
+
+INSERT INTO cockatrice_warnings (user_id, user_name, mod_name, reason, time_of, clientid)
+VALUES (@devplayer2_id, 'devplayer2', 'devmod', 'Seeded warning: spamming the General channel.', '2026-01-03 12:00:00', '')
+ON DUPLICATE KEY UPDATE
+ mod_name = VALUES(mod_name),
+ reason = VALUES(reason);
+
+-- A permanent ban on a documentation-only address: it exercises the ban list
+-- without locking out 127.0.0.1 or any seeded account.
+INSERT INTO cockatrice_bans
+ (user_name, ip_address, id_admin, time_from, minutes, reason, visible_reason, clientid)
+VALUES
+ ('griefer', '192.0.2.10', @devmod_id, '2026-01-04 12:00:00', 0,
+ 'Seeded ban: abusive behaviour.', 'Abusive behaviour.', '')
+ON DUPLICATE KEY UPDATE
+ id_admin = VALUES(id_admin),
+ minutes = VALUES(minutes),
+ reason = VALUES(reason),
+ visible_reason = VALUES(visible_reason);
+
+DELETE FROM cockatrice_log WHERE log_time = '2026-01-05 12:00:00';
+INSERT INTO cockatrice_log
+ (log_time, sender_id, sender_name, sender_ip, log_message, target_type, target_id, target_name, channel)
+VALUES
+ ('2026-01-05 12:00:00', @devplayer_id, 'devplayer', '127.0.0.1', 'Anyone up for a test game?', 'room_channel', 1, 'General', 'general'),
+ ('2026-01-05 12:00:00', @devplayer2_id, 'devplayer2', '127.0.0.1', 'Sure, create it in Testing.', 'room_channel', 1, 'General', 'general');
+
+-- Deck storage -------------------------------------------------------------
+INSERT INTO cockatrice_decklist_folders (id, id_parent, id_user, name, is_public)
+VALUES (9001, 0, @devplayer_id, 'Dev decks', 0)
+ON DUPLICATE KEY UPDATE
+ id_user = VALUES(id_user),
+ name = VALUES(name),
+ is_public = VALUES(is_public);
+
+INSERT INTO cockatrice_decklist_files
+ (id, id_folder, id_user, name, upload_time, content, is_public)
+VALUES
+ (9001, 9001, @devplayer_id, 'Dev burn deck', '2026-01-06 12:00:00',
+ '\n\n Dev burn deck\n Seeded development deck.\n \n \n \n \n\n',
+ 0)
+ON DUPLICATE KEY UPDATE
+ id_folder = VALUES(id_folder),
+ id_user = VALUES(id_user),
+ name = VALUES(name),
+ upload_time = VALUES(upload_time),
+ content = VALUES(content),
+ is_public = VALUES(is_public);