# Infrastructure

## Cloudflare Stack

| Service | Purpose | Free Tier |
|---------|---------|-----------|
| Workers | MCP server, auth endpoints | 100K requests/day |
| Durable Objects | Per-source SourceHub + per-user UserHub for daemon ↔ web UI real-time communication and save push | Requires Workers Paid ($5/mo); $0.15/M requests, $12.50/M GB-s duration |
| R2 (`savecraft-plugins`) | Plugin binaries and manifests | 10M reads, 1M writes/month |
| D1 | User accounts, source configs, source events, save metadata, section data, note content, FTS5 search index, plugin registry metadata | 5M rows read, 100K writes/day |
| Workers for Platforms | Dispatch namespace for reference plugin Workers (drop calculators, etc.) | $25/month flat (included in Workers Paid plan) |

**Cost projections:**
- 1K paying users ($5K/month revenue): infrastructure <$100/month
- 10K users: $200-500/month

### Why Cloudflare R2 over S3

- Zero egress fees (S3 egress adds up at scale, and MCP reads = egress)
- S3-compatible API (Go code unchanged if migrating later)
- Free tier has absurd headroom for early stage
- Workers in front for compute, same platform

## Security Model

### Principle: R2 is Private, Server Mediates All Access

The R2 plugins bucket has no public access. The Worker (with R2 bindings) is the only reader/writer. Plugin access is unauthenticated but read-only via the manifest API endpoint.

### Daemon → Cloud Push

- Daemon authenticates with source token (`sct_*`), verified via SHA-256 hash lookup in D1.
- Save data sent as binary protobuf `PushSave` messages over authenticated WebSocket.
- SourceHub DO writes save metadata and sections to D1 via `storePush`.
- Source token is issued at registration (`/ws/register` WebSocket) and persisted locally by the daemon.

### WASM Plugin Security

- **Daemon-side (parser):** wazero sandbox — plugins can only read stdin and write stdout/stderr. No filesystem, no network, no env vars.
- **Server-side (reference):** Workers for Platforms sandbox — each reference Worker has zero bindings (no KV, no R2, no D1). WASI shim provides only stdin/stdout. Pure computation.
- Ed25519 signature verification before loading any plugin.
- Community submits source → maintainer reviews → CI builds → release pipeline signs.

### MCP Server Hardening

- Rate limiting per user.
- Input validation on tool parameters (save_id and section validated against known values).
- No user-supplied query language — tools are fixed-function.
- OAuth token validation via KV lookup (no external call per request). Tokens are opaque, issued by our AS.

### Privacy Advantage

Save data flows through cloud and back via MCP. The AI never sees the user's local filesystem:
- Cannot request arbitrary files.
- Cannot discover what else is on the machine.
- Cannot see save directory paths.
- Only sees structured JSON that Savecraft chooses to serve.

Better privacy posture than a local MCP server with filesystem access.

### Daemon Sandboxing

The daemon runs with the minimum permissions necessary on each platform. Users can verify the sandbox configuration themselves.

**Linux / Steam Deck (systemd):**

The systemd user unit declares kernel-enforced restrictions:

The systemd unit is generated by `savecraftd install` (via `internal/svcmgr`), not by the install script. The generated unit includes security hardening:

```ini
[Unit]
Description=Syncs game saves to the cloud via Savecraft
After=network-online.target

[Service]
ExecStart=/home/user/.local/bin/savecraft-daemon run
Restart=always
RestartSec=5
EnvironmentFile=-/home/user/.config/savecraft/env

ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/home/user/.config/savecraft /home/user/.cache/savecraft /home/user/.local/bin

NoNewPrivileges=yes
PrivateTmp=yes

RestrictAddressFamilies=AF_INET AF_INET6

[Install]
WantedBy=default.target
```

These are kernel-level enforcement. Even if the daemon binary were compromised, it cannot write to save files, cannot access files outside its declared paths, cannot escalate privileges. The user can inspect the unit file: `cat ~/.config/systemd/user/savecraft-daemon.service`.

## Installation: Linux / Steam Deck

**Curl installer:**

```bash
curl -fsSL https://install.savecraft.gg | bash
```

The install script:
1. Detects architecture (amd64/arm64)
2. Downloads signed daemon binary to `~/.local/bin/savecraft-daemon`
3. Downloads signed tray binary to `~/.local/bin/savecraft-tray` (optional — failure warns but does not abort)
4. Verifies Ed25519 signatures against baked-in public key
5. Delegates service registration to `savecraftd install` (generates systemd unit with sandboxing)
6. Starts the service via `savecraftd start`
7. Daemon self-registers on first boot (connects to `/ws/register` WebSocket, sends `Register` proto), receives a source token and 6-digit link code
8. Prints link URL — user visits it to link the source to their account

**No root required.** Everything installs in `~/.local/bin/` and `~/.config/`. The daemon runs as a systemd user service under the current user. The tray binary is standalone — users can add it to their desktop environment's autostart (XDG autostart, GNOME Tweaks, etc.) if desired. `inotify` (used by fsnotify) only needs read permission on watched directories.

**Steam Deck specifics:** SteamOS is immutable — the root filesystem is read-only and resets on OS updates. But `~/.local/bin/` and `~/.config/` persist on the user partition. Systemd user services survive updates. Users need to switch to Desktop Mode to run the curl command via Konsole.

## Installation: Windows

**Signed MSI installer:**

Visit `https://install.savecraft.gg` from a Windows browser. The install Worker detects the Windows User-Agent and redirects to the signed MSI in R2.

The MSI:
1. Installs daemon and tray binaries to `%LOCALAPPDATA%\Savecraft\`
2. Registers autostart via HKCU `Software\Microsoft\Windows\CurrentVersion\Run` registry key
3. Launches the tray app on completion for first-run pairing flow

**No admin required.** Per-user install to `%LOCALAPPDATA%\Savecraft\`. No UAC elevation prompt.

**Code signing:** Both `.exe` binaries and the `.msi` installer are Authenticode-signed via Azure Trusted Signing (Public Trust certificate profile). This provides immediate SmartScreen reputation — no "Windows protected your PC" warnings on first download. Certificates rotate every 3 days; all signatures include RFC 3161 timestamps for long-term validity.

## Development Tooling

### Protobuf + buf

The WebSocket protocol is defined in `proto/protocol.proto`. `buf generate` produces Go types (`internal/proto/`) and TypeScript types (`worker/src/proto/`) from the same source. No mirrored types, no drift.

GameState types (what plugins emit, what D1 stores, what MCP serves) use `google.protobuf.Struct` for section data in the proto wire format. Plugin output (JSON) converts to `structpb.NewStruct()` in Go, flows as binary proto over WebSocket, and the Worker converts to JSON for D1 storage via `Struct.toJSON()`.

### just

`just` is the command runner. All build/test/lint/generate targets are in the `Justfile`. `just --list` shows available targets. `just check` runs everything.

### Testing Strategy

**Unit tests (fast, run on every change):**
- Go: all external dependencies behind interfaces. Hand-written fakes for filesystem, WASM runtime, WebSocket client. No mock libraries.
- Worker: Vitest + Miniflare for local D1, R2, Durable Objects, WebSocket.
- Svelte: component tests with mock WebSocket and API responses.

**Integration tests (Docker Compose, CI + on-demand):**
- Daemon binary + Miniflare Worker + real WebSocket connections + real file events.
- End-to-end: write a save file → daemon detects → parses → pushes → MCP tool returns data.

### Development Environment

nix devenv + direnv. `devenv.nix` provides Go, Node, Wrangler, buf, just, and all development tools. `direnv allow` activates the environment automatically on `cd`.

## CI & Deployment Pipeline

CI and deployment are separate concerns. Push to `main` runs tests and deploys to **staging only**. Production deploys require explicit tags. All workflows live in `.github/workflows/`.

### CI (`ci.yml`)

Runs on every push to `main` and every PR. Uses `dorny/paths-filter` to skip jobs when irrelevant files change:

| Job | Triggers on | Action on main push |
|-----|-------------|---------------------|
| `go` | `internal/**`, `cmd/**`, `plugins/**`, `proto/**`, `go.mod`, `go.sum` | Lint, test, proto check |
| `worker` | `worker/**`, `proto/**` | Lint, test, **deploy to staging** |
| `web` | `web/**` | Lint, typecheck, test, **deploy to staging** |
| `plugins` | `plugins/*/plugin.toml`, `plugins/*/*.go`, `plugins/*/d2s/**` | Build, **upload to staging R2** |

A web-only PR skips Go and Worker checks entirely. Deploy jobs run only on `main` push and only if the relevant paths changed.

**Staging environments:**
- Worker: `staging-api.savecraft.gg` (D1: `savecraft-staging`)
- Web: `staging.savecraft.gg` (Cloudflare Pages `staging` branch)
- Plugins: `savecraft-plugins-staging` R2 bucket (unsigned, for testing)

### Unified Versioning

All four deploy workflows share a single version computation via `.github/actions/compute-version/action.yml`. The composite action takes a `tag_prefix` input and outputs `version` and `environment`:

- **Production:** When the git ref matches `{tag_prefix}*`, it extracts the semver from the tag.
- **Staging:** Otherwise, it computes `0.0.0-dev.{GITHUB_RUN_NUMBER}.{SHORT_SHA}`.

Versions are user-visible everywhere: daemon CLI (`--version`), install banner, MCP `serverInfo.version`, and `X-Savecraft-Version` response header on all API responses.

### Production Releases

Production deploys use tag-prefix namespaces. Each component has its own tag format, release workflow, GitHub Release with auto-generated changelog, and Discord notification.

| Component | Tag format | Workflow | Artifacts |
|-----------|-----------|----------|-----------|
| Daemon | `daemon-v*` | `deploy-daemon.yml` | Binaries + install.sh + checksums |
| Worker + Web | `cloud-v*` | `deploy-cloud.yml` | Changelog only (no binary artifacts) |
| Plugins | `plugin-{game_id}-v*` | `deploy-plugin.yml` | Changelog only (plugins uploaded to R2) |

**To release:**
```bash
# Deploy daemon v1.2.3
git tag daemon-v1.2.3 && git push --tags

# Deploy worker + web to production
git tag cloud-v1.0.0 && git push --tags

# Release a specific plugin to production
git tag plugin-d2r-v0.0.3 && git push --tags
```

**Changelog scoping:** All release workflows use `gh release create --generate-notes --notes-start-tag <previous-tag-of-same-prefix>`. This ensures changelogs only include commits since the last release of that component.

**`cloud-v*` re-runs tests** before deploying to production. `daemon-v*` and `plugin-*-v*` do not re-run tests.

### Daemon Deploy Pipeline (`deploy-daemon.yml`)

Triggered by `daemon-v*` tags. Four parallel-then-sequential jobs:

1. **Build:** Cross-compile daemon for 5 platforms (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64) and tray for Linux + Windows (macOS tray requires CGO and a macOS runner — deferred)
2. **Sign:** Ed25519 signature on each binary, SHA256 checksums
3. **Upload to R2:** Binaries + signatures + daemon manifest to `savecraft-install/daemon/`
4. **GitHub Release:** Create release with all artifacts, notify Discord

### Plugin Deploy Pipeline (`deploy-plugin.yml`)

Triggered by per-game tags (`plugin-{game_id}-v*`) or `workflow_dispatch` (manual, specify game_id). For each plugin: build parser WASM → sign with Ed25519 → generate manifest → upload to R2. Creates a GitHub Release and notifies Discord.

**Plugin versioning:** Version comes from git tags via the shared composite action. `plugin.toml` contains metadata (name, description, file extensions, etc.) but not version. The `cmd/plugin-manifest` tool accepts `--version` as a CLI argument to stamp the manifest.

### Reference Worker Deploy Pipeline

Reference plugin Workers are deployed to Workers for Platforms dispatch namespaces. The shared WASI shim (`reference/`) is deployed once per game with the game's `reference.wasm` baked in.

**Dispatch namespaces:**
- Production: `savecraft-reference-plugins`
- Staging: `savecraft-reference-plugins-staging`

**Deployment:** Reference Workers deploy via `wrangler deploy` with the `--dispatch-namespace` flag. The Worker name follows the pattern `{game_id}-reference` (e.g., `d2r-reference`). The main Worker dispatches to these via `env.REFERENCE_PLUGINS.get("d2r-reference")`.

**CI integration:** Reference Worker deploys are triggered alongside plugin deploys by `plugin-{game_id}-v*` tags. The deploy pipeline builds `reference.wasm`, copies it into the shared `reference/` directory, and runs `wrangler deploy --dispatch-namespace savecraft-reference-plugins`.

### Signing Key

Ed25519 keypair generated by `just keygen` (`cmd/savecraft-keygen/`). The public key (`internal/signing/signing_key.pub`) is checked into the repo and baked into daemon binaries and `install.sh`. The private key is base64-encoded and stored as `SIGNING_PRIVATE_KEY` in GitHub Actions secrets. Staging plugin deploys skip signing.
