Administration Guide

Docker Deployment

docker-compose.yml

services:
  steward:
    build: .
    ports:
      - "${PORT:-3000}:3000"
    volumes:
      - ./data:/data
    environment:
      - STEWARD_DATA_DIR=/data
      - PORT=3000
    restart: unless-stopped

Running

# Start (builds if needed)
docker compose up -d

# Custom port
PORT=8080 docker compose up -d

# Rebuild after code changes
docker compose build && docker compose up -d

# View logs
docker compose logs -f steward

# Stop
docker compose down

Volumes

The ./data directory on the host is mounted to /data in the container. This is where the SQLite database file (steward.db) lives. The volume ensures data persists across container restarts and rebuilds.

Restart Policy

restart: unless-stopped means the container restarts automatically after crashes or host reboots, unless you explicitly stop it with docker compose down.


Bare-Metal Deployment

Production

# Build the frontend
cd client && npm install && npm run build && cd ..

# Install server dependencies
cd server && npm install --production && cd ..

# Start the server
cd server && npm start

The server serves the built frontend and API on the configured port (default 3000).

Behind a Reverse Proxy

Steward runs on a single port serving both the frontend and API. Point your reverse proxy (nginx, Caddy, etc.) to http://localhost:3000:

# nginx example
server {
    listen 80;
    server_name steward.example.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Environment Variables

Variable Default Description
PORT 3000 Server port
STEWARD_DATA_DIR server/data/ Directory for the SQLite database file

In Docker, STEWARD_DATA_DIR is set to /data by default.


Database

Storage

All data is stored in a single SQLite file: steward.db in the configured data directory.

  • Docker: ./data/steward.db on the host (mounted from /data in the container)
  • Bare-metal: server/data/steward.db by default

SQLite Configuration

  • WAL mode — Write-Ahead Logging for better concurrent read performance
  • Foreign keys — enforced for referential integrity

Schema

The database has 18 tables:

Table Purpose
campaigns Campaign metadata, attribute definitions, calendar, weather, encounter settings, rules settings, presets
characters PC and NPC definitions with base attributes. spawned_from_encounter_id tracks auto-spawned NPCs.
status_effect_definitions Effect templates with modifiers and duration
item_definitions Item templates with modifiers and properties
encounter_definitions Encounter templates with NPCs, loot, conditions. starts_combat enables auto-combat.
applied_effects Active effects on characters (junction table)
character_items Items in character inventories (junction table)
environment_state Current game world state (time, weather, location, combat state)
locations Map locations with properties and weather overrides
location_edges Connections between locations with travel data
session_log Timestamped log of all game actions (includes in-game game_time)
rule_definitions Automation rules with triggers, conditions, actions
rule_state Persistent state for rules (e.g., last rest time)
rule_action_log History of rule-applied changes (for undo)
notifications Rule-generated notifications
journal_notes DM journal entries with tags, starring, and wikilinks
random_table_definitions Random table metadata and type (weighted/sequential)
session_preps Session prep sheets with structured scenes, secrets, and location links

Migrations

Schema migrations run automatically on startup. New columns are added idempotently using ALTER TABLE with duplicate-column error handling.


Backup and Restore

File-Based Backup

The simplest backup is copying the database file:

# Docker
cp ./data/steward.db ./backups/steward-$(date +%Y%m%d).db

# Bare-metal
cp server/data/steward.db ./backups/steward-$(date +%Y%m%d).db

To restore, stop the server and replace the database file.

Export/Import Backup

Use the in-app export feature for portable, human-readable backups:

  1. Export each campaign from the UI (JSON file per campaign)
  2. To restore, create a new Steward instance and Import each JSON file

This approach is more portable (not tied to SQLite version) but doesn’t include notifications or rule action history.


Seed Data

Load example data to explore features:

# Docker
docker compose exec steward node server/src/seed.js

# Bare-metal
cd server && npm run seed

The seed script is idempotent — if “Example Campaign” already exists, it skips without error.

See Getting Started for details on what the seed data includes.


Updating

Docker

# Pull latest code
git pull

# Rebuild and restart
docker compose build && docker compose up -d

Data persists in the ./data volume across rebuilds.

Bare-Metal

# Pull latest code
git pull

# Rebuild frontend
cd client && npm install && npm run build && cd ..

# Update server dependencies
cd server && npm install --production && cd ..

# Restart
cd server && npm start

API Reference

All endpoints are prefixed with /api. Campaign-scoped endpoints use /api/campaigns/:id/....

Campaigns

Method Path Description
GET /api/campaigns List all campaigns
GET /api/campaigns/:id Get campaign with parsed config fields
POST /api/campaigns Create campaign (name required)
PUT /api/campaigns/:id Update campaign (partial update supported)
DELETE /api/campaigns/:id Delete campaign and all associated data

Characters

Method Path Description
GET /api/campaigns/:id/characters List characters (query: type, search, include_spawned, include_archived)
GET /api/campaigns/:id/characters/:charId Get character
GET /api/campaigns/:id/characters/:charId/computed Get computed stats with modifier breakdown
POST /api/campaigns/:id/characters Create character (name, type required)
PUT /api/campaigns/:id/characters/:charId Update character
DELETE /api/campaigns/:id/characters/:charId Delete character

Applied Effects

Method Path Description
POST /api/campaigns/:id/characters/:charId/effects Apply effect (status_effect_definition_id required)
DELETE /api/campaigns/:id/characters/:charId/effects/:effectId Remove applied effect

Both endpoints fire on_effect_change rules and check on_threshold rules.

Character Items

Method Path Description
POST /api/campaigns/:id/characters/:charId/items Assign item (item_definition_id required, quantity optional)
PATCH /api/campaigns/:id/characters/:charId/items/:itemId Update quantity (quantity required; 0 removes item)
DELETE /api/campaigns/:id/characters/:charId/items/:itemId Remove item

Item changes fire on_item_change rules.

Status Effect Definitions

Method Path Description
GET /api/campaigns/:id/status-effects List definitions (query: search, tag)
GET /api/campaigns/:id/status-effects/:effectId Get definition
POST /api/campaigns/:id/status-effects Create definition (name required)
PUT /api/campaigns/:id/status-effects/:effectId Update definition
DELETE /api/campaigns/:id/status-effects/:effectId Delete definition (cascades to applied instances)

Item Definitions

Method Path Description
GET /api/campaigns/:id/items List definitions (query: search, item_type)
GET /api/campaigns/:id/items/:itemId Get definition
POST /api/campaigns/:id/items Create definition (name required)
PUT /api/campaigns/:id/items/:itemId Update definition
DELETE /api/campaigns/:id/items/:itemId Delete definition (cascades to character items)

Encounter Definitions

Method Path Description
GET /api/campaigns/:id/encounters List encounters (query: search)
GET /api/campaigns/:id/encounters/:encId Get encounter
POST /api/campaigns/:id/encounters Create encounter (name required)
PUT /api/campaigns/:id/encounters/:encId Update encounter
DELETE /api/campaigns/:id/encounters/:encId Delete encounter
POST /api/campaigns/:id/encounters/:encId/start Start encounter (fires rules; auto-starts combat if starts_combat is set)
POST /api/campaigns/:id/encounters/:encId/end End encounter (fires rules, cleans up spawned NPCs, ends combat)

Environment

Method Path Description
GET /api/campaigns/:id/environment Get environment state (enriched with time_of_day, month_name, location_name)
PATCH /api/campaigns/:id/environment Update environment fields
POST /api/campaigns/:id/environment/advance Advance time (hours, minutes)

Locations

Method Path Description
GET /api/campaigns/:id/locations Get all locations and edges
GET /api/campaigns/:id/locations/:locId Get single location
POST /api/campaigns/:id/locations Create location (name required)
PUT /api/campaigns/:id/locations/:locId Update location
DELETE /api/campaigns/:id/locations/:locId Delete location
GET /api/campaigns/:id/locations/edges/list Get all edges
POST /api/campaigns/:id/locations/edges Create edge (from_location_id, to_location_id required)
PUT /api/campaigns/:id/locations/edges/:edgeId Update edge
DELETE /api/campaigns/:id/locations/edges/:edgeId Delete edge
POST /api/campaigns/:id/locations/travel Travel along edge (edge_id required)
PATCH /api/campaigns/:id/locations/position Set party position (location_id, or null to clear)

Rest

Method Path Description
POST /api/campaigns/:id/rest Take rest (rest_type: “short” or “long”)

Short rest = 1 hour, Long rest = 8 hours. Fires on_rest rules, advances time, records rest time per character.

Rules

Method Path Description
GET /api/campaigns/:id/rules List rules (query: search, tag, trigger_type)
GET /api/campaigns/:id/rules/:ruleId Get rule
POST /api/campaigns/:id/rules Create rule (name, trigger_type required)
PUT /api/campaigns/:id/rules/:ruleId Update rule
DELETE /api/campaigns/:id/rules/:ruleId Delete rule
PATCH /api/campaigns/:id/rules/:ruleId/toggle Toggle rule enabled/disabled
POST /api/campaigns/:id/rules/:ruleId/test Test rule (optional character_id in body)
GET /api/campaigns/:id/rules/templates Get all rule templates by category
POST /api/campaigns/:id/rules/templates/:name Import rule template
GET /api/campaigns/:id/rules/references Get rule references (query: entity_type, entity_name, entity_id)

Tag Presets

Method Path Description
GET /api/campaigns/:id/rules/tag-presets Get all presets (built-in + custom)
POST /api/campaigns/:id/rules/tag-presets/:name Import preset (creates attribute + rules)
POST /api/campaigns/:id/rules/tag-presets/custom Save custom preset
DELETE /api/campaigns/:id/rules/tag-presets/custom/:key Delete custom preset
GET /api/campaigns/:id/rules/tag-presets/custom/:key/export Export custom preset

Notifications

Method Path Description
GET /api/campaigns/:id/notifications Get notifications (query: unread_only, limit, offset)
GET /api/campaigns/:id/notifications/count Get unread count
PATCH /api/campaigns/:id/notifications/:nId/read Mark as read
PATCH /api/campaigns/:id/notifications/:nId/dismiss Dismiss notification
POST /api/campaigns/:id/notifications/:nId/apply Apply suggested actions
POST /api/campaigns/:id/notifications/:nId/undo Undo auto-applied actions
DELETE /api/campaigns/:id/notifications Clear all notifications

Session Log

Method Path Description
GET /api/campaigns/:id/session-log Get log entries (query: limit, offset, entry_type). Includes game_time with in-game timestamps.
POST /api/campaigns/:id/session-log Add manual entry (message required)
DELETE /api/campaigns/:id/session-log Clear all entries

Combat

Method Path Description
GET /api/campaigns/:id/combat Get combat state (round, turn index, combatants with stats/effects)
POST /api/campaigns/:id/combat Start combat (combatants array of {character_id, initiative} required)
DELETE /api/campaigns/:id/combat End combat and clear state
POST /api/campaigns/:id/combat/next-turn Advance to next turn (decrements round-based effects, fires rules on new round)
PATCH /api/campaigns/:id/combat Update combat settings (combatants, advance_time, time_per_round_seconds)

Combat state is stored in the environment_state table. When advance_time is enabled (default), the game clock advances by time_per_round_seconds (default 6) at the start of each new round.

Journal

Method Path Description
GET /api/campaigns/:id/journal List notes (query: search, tag, starred, limit, offset)
GET /api/campaigns/:id/journal/search-entities Search entities for wikilink autocomplete (query: q)
GET /api/campaigns/:id/journal/:noteId Get note
POST /api/campaigns/:id/journal Create note (title, content, tags, starred)
PUT /api/campaigns/:id/journal/:noteId Update note (partial update supported)
DELETE /api/campaigns/:id/journal/:noteId Delete note

Journal notes support wikilinks ([[Character Name]]) that link to campaign entities.

Random Tables

Method Path Description
GET /api/campaigns/:id/random-tables List tables (query: search)
GET /api/campaigns/:id/random-tables/:tableId Get table with entries
POST /api/campaigns/:id/random-tables Create table (name required, entries, table_type)
PUT /api/campaigns/:id/random-tables/:tableId Update table
DELETE /api/campaigns/:id/random-tables/:tableId Delete table
POST /api/campaigns/:id/random-tables/:tableId/roll Roll on table (result logged to session log)

Tables can be weighted (entries have weights) or sequential (equal probability).

Session Prep

Method Path Description
GET /api/campaigns/:id/session-preps List preps (query: status, limit, offset)
GET /api/campaigns/:id/session-preps/active Get the currently active prep
GET /api/campaigns/:id/session-preps/:prepId Get a prep
POST /api/campaigns/:id/session-preps Create prep (title, strong_start, scenes, secrets, notes, carry_forward)
PUT /api/campaigns/:id/session-preps/:prepId Update prep (partial update supported)
PUT /api/campaigns/:id/session-preps/:prepId/activate Set prep as active (completes any previously active prep)
PUT /api/campaigns/:id/session-preps/:prepId/complete Mark prep as completed
DELETE /api/campaigns/:id/session-preps/:prepId Delete prep

Scenes and secrets are JSON arrays with text, done/revealed flags, and optional location_ids for location linking. When the party arrives at a linked location, a notification fires.

Export / Import

Method Path Description
GET /api/campaigns/:id/export-import/export Export full campaign as JSON download
POST /api/campaigns/:id/export-import/import Import full campaign (creates new)
POST /api/campaigns/:id/export-import/import/preview Preview merge import with conflict detection
POST /api/campaigns/:id/export-import/import/merge Execute merge import with conflict decisions

Troubleshooting

Database Locked

If you see “database is locked” errors, ensure only one server instance is running. SQLite allows one writer at a time. WAL mode handles concurrent reads well but multiple processes writing simultaneously will conflict.

Port Already in Use

# Find what's using the port
lsof -i :3000

# Use a different port
PORT=8080 docker compose up -d

Container Won’t Start

# Check logs
docker compose logs steward

# Rebuild from scratch
docker compose build --no-cache && docker compose up -d

Data Directory Permissions

The data directory must be writable by the Node.js process. In Docker, the directory is created automatically. For bare-metal:

mkdir -p server/data
chmod 755 server/data

Missing Seed Data

The seed script skips if “Example Campaign” already exists. To re-seed, delete the existing campaign first (or delete the database file and restart).

Rules Not Firing

Common causes:

  • Engine disabled — check Settings → Rules Engine → Engine enabled
  • Rule disabled — check the toggle on the rule card
  • Conditions failing — use the Test panel to see which conditions fail and why
  • Name mismatch — rules reference effects and items by exact name (case-sensitive)
  • Wrong target mode — character conditions fail in Environment target mode
  • Cascade limit reached — deeply chained rules become suggestions instead of auto-applying

Import Failures

  • Version mismatch — imports expect the Steward export format with a version header
  • Missing prerequisites — imported rules may reference effects/items that don’t exist. Create them first or edit the rules after import.
  • ID conflicts — the import system remaps IDs automatically, but cross-references (like encounter NPC IDs) require matching entities to exist