# Runbook

> Attempt #2's only setup instructions lived in a plan file describing an architecture
> that had already been deleted, so they were actively wrong. Keep this file honest:
> if a command here doesn't work, fix it or delete it.

## Status

The app boots and serves. Everything marked **(#2)** is carried from the previous
attempt and has **not** been re-verified against Topcoat — treat it as a sketch.

## Requirements

- **rustc ≥ 1.95** — Topcoat 0.5 requires it, and on an older toolchain `cargo add
  topcoat` silently resolves to an empty `topcoat v0.0.0` placeholder rather than
  failing. Verified on 1.97.1.
- `git` on `PATH` (from Milestone 3 — `git init --bare` creates repos, and from
  Milestone 4 `git http-backend` serves the protocol). **`cargo test` needs it too**:
  `DiskGitStorage`'s tests run real `git init`, so a machine without `git` fails the
  suite, not just the app.

## Dev setup

```bash
cargo run                     # serves on http://127.0.0.1:3000
cargo test
```

```bash
cargo install topcoat-cli     # dev server: watch, rebuild, asset bundling
topcoat dev                   # working
```

`topcoat dev` builds, bundles assets, watches sources, and live-reloads pages that
include `topcoat::dev::script()`. Press `r` to force a rebuild.

## Configuration

`STEID_`-prefixed env vars via `dotenvy` + `envy`, read into `AppConfig`. Every value
has a default, so a bare `cargo run` works with no environment at all.

Live now:

```
STEID_DATABASE_URL=sqlite:steid.db?mode=rwc   # default
STEID_DATA_DIR=./data                         # default; bare repos, read from M3
STEID_INSECURE_COOKIES=false                  # default; see below
```

There is deliberately **no owner password in configuration** — the owner is created
through the claim flow instead. See
[0002](decisions/0002-first-run-claim-not-config-bootstrap.md).

The bind address is **not** a `STEID_` variable — Topcoat owns it:

```bash
HOST=0.0.0.0 PORT=8080 cargo run
```

That supersedes attempt #2's `STEID_LISTEN_ADDR`.

### `STEID_INSECURE_COOKIES` — development only

Topcoat's session cookie is `__Host-` prefixed and `Secure`. `Secure` means the browser
only keeps it over a trustworthy origin, and browsers disagree about whether
plain-HTTP `localhost` qualifies. Where it doesn't, **the failure is completely
silent**: the server issues a session and records the row, the browser discards the
cookie, and every page renders signed out with no error anywhere. This cost an
afternoon; the symptom looks exactly like broken auth logic.

Setting `STEID_INSECURE_COOKIES=true` swaps in `InsecureCookieTokenStore` — the same
cookie without `Secure` and without the prefix, named `steid-dev-session` so it can
never be confused with a hardened one. `HttpOnly` and `SameSite=Lax` are kept. Boot
prints a warning while it's on.

**Never set this on a deployed instance.** Without `Secure` the session cookie travels
unencrypted and anyone on the network path can lift it and become that user. Behind
TLS, leave it unset.

A gitignored `.env` in the repo root sets it for local work. Keep `.env` and
`.env.prod` out of git — both are gitignored.

## First run

```bash
cargo run          # or: topcoat dev
```

An unclaimed instance prints a setup token and redirects every route to `/auth/setup`.
Paste the token, choose a handle, email, and password, and the owner is created and
signed in.

The token is **held in memory only**, so every restart mints a new one — including
each rebuild under `topcoat dev`. Use the most recent one printed. Once claimed, no
token is minted at all and `/auth/setup` redirects away.

Sign in at `/auth/login` with the **email**, not the handle.

## Repo layout on disk (Milestone 3)

Bare repos at `{STEID_DATA_DIR}/{handle}/{name}.git`, created with no template (so no
`.sample` hooks) and `HEAD` pinned to `refs/heads/main` regardless of the host's
`init.defaultBranch`. See [0006](decisions/0006-git-binary-behind-narrow-ports.md).

Created empty — no initial commit and no branch, like GitHub.

Creating one refuses rather than reusing a directory that already exists, so an orphan
left by a create that died mid-way blocks that name until it is removed by hand.

## Git transport (Milestone 4)

Smart HTTP, delegated to `git http-backend`, authenticated with personal access tokens
over HTTP Basic — see [0001](decisions/0001-git-over-http-not-ssh.md). No SSH, no host
keys, no `authorized_keys`.

```bash
git clone http://host/{handle}/repos/{name}.git
```

Fill in the token workflow and the `body_limit` setting once this is built.

## Manual verification checklist (#2)

Attempt #2 verified these by hand each milestone but never wrote down the steps. They
are the smoke test for Milestones 4–5. The auth rows assumed SSH keys; the shape of
the check still holds with tokens substituted:

- [ ] Create a repo via the web UI → bare repo appears at
      `{data_dir}/{org}/{repo}.git`
- [ ] `git clone` an empty repo → succeeds
- [ ] `git clone` a repo with history → succeeds
- [ ] `git clone` a non-existent repo → clean error, not a hang or panic
- [ ] First push to an empty repo → succeeds
- [ ] Push to a repo with history → succeeds
- [ ] Push a repo large enough to exercise the `body_limit` cap → succeeds
- [ ] Clone with no credentials → rejected, and the prompt is comprehensible
- [ ] Clone with a valid token → succeeds
- [ ] Revoke the token → subsequent clone rejected at auth
- [ ] Clone a private repo as a non-member → rejected
- [ ] Push as a non-owner member → rejected

Worth automating as an integration test rather than re-running by hand a fourth time.

## `.gitignore`

Applied: `/target`, `/data`, `*.db*`, `.env`, `.env.prod`.

**Do not add `/plans`.** Attempt #2 did, and that is why these docs had to be
hand-carried between repos.
