12 KiB
Development conventions
Language and style
- Documentation and stable identifiers are English-first; UI strings are localizable from the start.
- Use idiomatic current stable Go, explicit errors and small interfaces at consumer boundaries.
- Prefer server-rendered
html/templateviews, progressive enhancement and small focused browser modules; do not introduce a large SPA framework without an accepted architectural reason. - Format and lint frontend and Go code with repository-pinned tools.
- Avoid hidden global state. Inject clock, ID generator and external interfaces for deterministic tests.
- Use UTC internally and IANA timezones at scheduling boundaries.
Repository shape
The implementation may refine this layout while preserving boundaries:
cmd/dogama/
cmd/dogama-agent/
internal/ non-public Go packages
web/ embedded UI source
internal/persistence/sqlite/schema.sql embedded current SQLite schema
specs/ schemas and normalized contracts
catalog/ reference templates
catalog/<game>/module/ optional template-local WASM adapters and sources
docs/
tests/integration/
Initial application development
Template-local integration modules
An administrator may create or import a local template as a self-contained directory:
my-game/
template.yaml
assets/
module/ # optional
manifest.yaml
custom.wasm
When present, declare the manifest in template.yaml with
module.path: module/manifest.yaml. The module directory is confined to the
template root: absolute paths, traversal and symbolic links are rejected. The
manifest and WASM artifact remain subject to the generic manifest schema and
the capability-limited WASM sandbox. No rebuild of DoGaMa or internal game
registry entry is needed for a local module.
The main application requires Go 1.25. SQLite uses the pure-Go modernc.org/sqlite driver, so neither cgo nor a system SQLite development library is required. Standard Compose supplies all internal bootstrap contracts. The agent generates its shared token and the application generates its master key independently. Direct developer execution may override DOGAMA_LISTEN_ADDRESS, DOGAMA_DATABASE_PATH, DOGAMA_AGENT_URL, DOGAMA_AGENT_TOKEN_FILE and DOGAMA_MASTER_KEY_FILE; lifecycle routes remain disabled when both agent overrides are absent. These are development controls, not public deployment settings. Run it with:
go run ./cmd/dogama
Browser sessions always use Secure, HttpOnly, and SameSite=Strict cookies. Place the application behind a trusted TLS reverse proxy for browser use, including development environments. The application does not accept forwarded client addresses as authoritative for authentication throttling.
Local HTTPS Compose environment
Use the root compose.yaml together with compose-dev.yml for local development, manual UI checks, Playwright and integration checks:
docker compose -f compose.yaml -f compose-dev.yml up -d
docker compose -f compose.yaml -f compose-dev.yml ps
The development overlay builds the two local image targets and adds Caddy 2.10.2 only. It removes the direct development publication of the DoGaMa HTTP port and publishes Caddy on https://dogama.lan (host TCP 443 by default; set DOGAMA_HTTPS_PORT only when 443 is unavailable). Production does not include Caddy and remains compatible with an administrator-operated reverse proxy.
deploy/dev/Caddyfile uses tls internal, so Caddy automatically creates a development-only local CA and certificate for dogama.lan. Its /data and /config directories use the separate caddy_data and caddy_config named volumes. Neither the CA nor a private key is committed. Caddy receives the app-facing dogama network only; it has no Docker socket or access to the private control network. Caddy provides the normal Host, X-Forwarded-For and X-Forwarded-Proto proxy request context; DoGaMa uses the scheme and host for its explicit web-access policy and secure cookies, but does not treat forwarded client addresses as authentication authority.
dogama.lan must resolve to the DevStation address from the browser machine. Use local DNS, or add an entry such as the following on each development client when no LAN DNS exists (replace the address):
192.0.2.10 dogama.lan
Verify it before browser testing with getent hosts dogama.lan. Do not substitute a localhost hostname.
The Caddy CA is not automatically trusted by client machines. To trust it for an interactive browser, copy its public root certificate after Caddy has started and install it in the operating system/browser trust store. For example on Debian/Ubuntu:
container=$(docker compose -f compose.yaml -f compose-dev.yml ps -q caddy)
docker cp "$container:/data/caddy/pki/authorities/local/root.crt" ./dogama-caddy-root.crt
sudo install -m 0644 ./dogama-caddy-root.crt /usr/local/share/ca-certificates/dogama-caddy-local.crt
sudo update-ca-certificates
rm ./dogama-caddy-root.crt
Do not commit the copied certificate. Automated Playwright tests may instead use ignoreHTTPSErrors: true; that exception is limited to local tests and must never be used for production verification. DevStation Chromium is available at /usr/bin/chromium-browser. To stop the stack while retaining data, run docker compose -f compose.yaml -f compose-dev.yml down; adding -v also deletes the Compose named volumes, including the Caddy PKI and the agent security state.
The restricted agent is a separate binary:
go run ./cmd/dogama-agent
It atomically creates a missing 32-byte token and fails closed when an existing token is invalid. The
standard deployment uses /var/lib/dogama-agent/secrets/token, confines paths to
/srv/game-servers and /srv/game-backups, and stores the authenticated
registry at /var/lib/dogama-agent/registry.json. Developer overrides remain
available for isolated tests. The configured roots must already exist
and are canonicalized with symlinks resolved. For normal deployment, use the
read-only shared token volume and private control network defined in compose.yaml; never publish
the agent port on the host.
The agent loads the same embedded validated catalog as the main application. Before Docker access it independently matches image, entrypoint, arguments, container ports, mount destinations, resource minimums and stop timeout against the pinned template snapshot. Mount sources are created one directory at a time below configured roots with symlinks refused. Docker containers always use the fixed restricted baseline; callers cannot provide labels, capabilities, devices, network modes or arbitrary Docker options.
Lifecycle API operations are authorized in the backend against the authenticated
identity and the target instance. Global administrators retain implicit access;
assigned users can inspect, view metrics, start and stop, while managers also
receive the documented operational baseline. Explicit deny overrides take
precedence over role baselines and allows. Install and container-only delete
remain administrator operations. Operations are serialized per instance and
recorded in instance_operations; desired and observed states are reconciled at
startup and every minute. Container-only delete removes neither the SQLite
intent nor host paths, so player data and backups remain untouched.
The authorization foundation exposes JSON APIs for local user creation, memberships, per-user overrides and installation requests. Mutations require the session CSRF token. User creation, membership changes, override changes and request review additionally require an administrator session authenticated in the previous ten minutes. Approving a request only records the decision and the requested values; it never creates a draft or contacts the restricted agent.
Game-data backups are written below DOGAMA_BACKUPS_ROOT (default
/srv/game-backups) and may only read instance mounts below
DOGAMA_SERVERS_ROOT (default /srv/game-servers). Untrusted uploads are
isolated below DOGAMA_IMPORTS_ROOT (default
/var/lib/dogama/imports/staging). These are bootstrap path boundaries, not
ordinary product settings. The same canonical server and backup roots are
mounted into the main application and restricted agent by compose.yaml.
The current backup engine conservatively stops a running instance before
archiving. Once the WebAssembly runtime is active, an enabled online_save
module can provide the documented flush-before-archive optimization without
moving traversal or archive ownership out of the main application. Archives
are finalized before SQLite marks them available; a metadata failure removes
the orphaned file. Scheduled retention considers only successful scheduled
backups. Restore verifies size, checksum, manifest and pinned template version,
creates a pre_restore backup, extracts into sibling staging and keeps the
instance stopped with intervention_required if readiness cannot be restored.
Validated imports are pinned to the selected template version. Import-backed
drafts require that opaque import ID, and installation atomically places the
normalized staged tree at the template-declared mount-relative destination
before the restricted agent creates the first container. Repeated installation
submission recognizes an already attached import instead of copying it twice.
At main-application startup, every embedded catalog/*/template.yaml is
validated against specs/template.schema.json, checked for cross-references and
referenced asset presence, canonicalized deterministically and synchronized into SQLite.
An existing template ID/version is immutable: changing its digest fails startup
instead of silently replacing the snapshot. Deployment previews pin that digest
and redact secret defaults before a draft instance can enter the registry.
Contract changes
Template schema, manifest schema, normalized module API and agent deployment plan are versioned contracts.
- Backward-compatible additions do not change existing meanings.
- Breaking schema changes may require a fresh development database until versioned production migrations are introduced.
- Released examples are updated or retained as compatibility fixtures.
- Instances pin immutable versions; runtime behavior never depends on a mutable catalog file.
Testing pyramid
- Unit: domain policies, permission evaluation, cron/timezones, state transitions, retention and redaction.
- Property/fuzz: paths, archive entries, schema inputs, agent plans and normalized module payloads.
- Integration: fresh SQLite schema initialization, disposable Docker agent, real archive workflows and WASM sandbox limits.
- End-to-end: bootstrap, Palworld dry/test fixture deployment, role views, backup/restore and update rollback.
Tests must include denial and interrupted-operation cases, not only happy paths. External game APIs use recorded or purpose-built fixtures in normal CI; live Palworld verification is a separate documented test.
SQLite compatibility
The development database is created from the embedded current schema. Existing databases from earlier revisions are intentionally unsupported and may need to be removed before local development. A versioned migration strategy will be introduced before production stabilization.
Dependencies and supply chain
Prefer standard-library or mature focused dependencies. Pin build tooling, review transitive changes and generate an SBOM/reproducible checksums for releases. Do not dynamically download executable code at runtime except administrator-installed validated WASM packages.
Observability
Structured technical logs use stable event names and redaction. Metrics are operationally small. Correlation/operation IDs connect HTTP, job and agent errors without storing request secrets.
Review checklist
- Correct trust boundary and backend authorization?
- Can input influence a host path, Docker plan, URL or secret?
- Are retries/idempotency and restart recovery defined?
- Could failure delete or corrupt player data?
- Do documentation, schema and examples agree?
- Are UI capability checks backed by server checks?
- Are audit and notifications appropriately minimal?