|
| 1 | +# `mantle init` Connection Recovery & Quickstart Fix |
| 2 | + |
| 3 | +**Date:** 2026-03-24 |
| 4 | +**Issue:** [#7 — Get Running in 5 Minutes](https://github.com/dvflw/mantle/issues/7) |
| 5 | +**Status:** Draft |
| 6 | + |
| 7 | +## Problem |
| 8 | + |
| 9 | +The landing page quickstart tells users to run `docker compose up -d` after installing via `go install`. There's no `docker-compose.yml` when you install that way — step 2 immediately fails. The `mantle init` command needs to handle the "no database yet" case gracefully. |
| 10 | + |
| 11 | +## Design |
| 12 | + |
| 13 | +### Connection Recovery Flow |
| 14 | + |
| 15 | +`mantle init` already loads config and calls `db.Open`. The change adds a recovery path when the connection fails: |
| 16 | + |
| 17 | +``` |
| 18 | +mantle init |
| 19 | + ├─ db.Open succeeds → run migrations → done |
| 20 | + └─ db.Open fails |
| 21 | + ├─ host is NOT loopback → print error with details, offer [R]etry or [Q]uit |
| 22 | + └─ host IS loopback → offer Docker auto-provisioning |
| 23 | + ├─ user accepts |
| 24 | + │ ├─ docker available → start container, wait for ready, run migrations → done |
| 25 | + │ └─ docker unavailable → show message, offer [R]etry or [C]onnection string |
| 26 | + └─ user declines → offer [R]etry or [C]onnection string |
| 27 | +``` |
| 28 | + |
| 29 | +### Loopback Detection |
| 30 | + |
| 31 | +Parse the host from the configured database URL. Treat as loopback if the host is: |
| 32 | +- `localhost` |
| 33 | +- `127.0.0.1` |
| 34 | +- `::1` |
| 35 | + |
| 36 | +Use `net/url` to parse the connection string and extract the host. |
| 37 | + |
| 38 | +### Docker Auto-Provisioning |
| 39 | + |
| 40 | +When the user accepts Docker provisioning: |
| 41 | + |
| 42 | +1. Check Docker availability: exec `docker info` and check exit code |
| 43 | +2. Run the container: |
| 44 | + ``` |
| 45 | + docker run -d \ |
| 46 | + --name mantle-postgres \ |
| 47 | + -p 5432:5432 \ |
| 48 | + -e POSTGRES_USER=mantle \ |
| 49 | + -e POSTGRES_PASSWORD=mantle \ |
| 50 | + -e POSTGRES_DB=mantle \ |
| 51 | + -v mantle-pgdata:/var/lib/postgresql/data \ |
| 52 | + postgres:16-alpine |
| 53 | + ``` |
| 54 | +3. Wait for readiness: poll `db.Open` with backoff (up to ~15s) |
| 55 | +4. On success: continue to migrations |
| 56 | +5. On timeout: error with "Container started but Postgres isn't accepting connections" |
| 57 | + |
| 58 | +Use `os/exec` to run Docker commands. The container config matches the existing defaults in `config.go` so no config persistence is needed. |
| 59 | + |
| 60 | +If the container name `mantle-postgres` already exists (stopped), remove it first and start fresh. If it's already running, skip straight to the readiness check. |
| 61 | + |
| 62 | +### Fallback: No Docker / User Declined |
| 63 | + |
| 64 | +Present two options: |
| 65 | +``` |
| 66 | +Can't auto-provision — Docker isn't installed or isn't running. |
| 67 | +
|
| 68 | + [R] Retry (install or start Docker first) |
| 69 | + [C] Enter a Postgres connection string |
| 70 | +
|
| 71 | +Choice [R/c]: |
| 72 | +``` |
| 73 | + |
| 74 | +- **Retry**: loop back to Docker availability check |
| 75 | +- **Connection string**: prompt for URL, validate with `db.Open`, on success continue to migrations, on failure show the error and re-prompt |
| 76 | + |
| 77 | +### Non-Loopback Failure |
| 78 | + |
| 79 | +When the configured URL points to a remote host and the connection fails: |
| 80 | +``` |
| 81 | +Failed to connect to database at db.example.com:5432 |
| 82 | +
|
| 83 | + Error: connection refused |
| 84 | +
|
| 85 | + [R] Retry (fix the issue and try again) |
| 86 | + [Q] Quit |
| 87 | +
|
| 88 | +Choice [R/q]: |
| 89 | +``` |
| 90 | + |
| 91 | +Include the underlying error from `db.Open` (timeout, auth failure, TLS, DNS resolution, etc.) so the user can diagnose without guessing. |
| 92 | + |
| 93 | +- **Retry**: re-reads the config (picks up env var or config file changes made while waiting) and retries `db.Open`. This lets the user fix a typo, adjust a firewall rule, or start their database without restarting `mantle init`. |
| 94 | +- **Quit**: exit 1 |
| 95 | + |
| 96 | +### Interactive Input |
| 97 | + |
| 98 | +Follow the existing pattern from `login.go`: use `fmt.Fscanln(cmd.InOrStdin(), &input)` for prompts. No new dependencies needed. |
| 99 | + |
| 100 | +When stdin is not a terminal (piped input, CI), skip all interactive prompts and return the connection error directly. Detect with `os.Stdin.Stat()` checking for `ModeCharDevice`. |
| 101 | + |
| 102 | +## Constant Extraction |
| 103 | + |
| 104 | +Before implementing the new init flow, extract shared constants that are currently duplicated across the codebase. This keeps the new code referencing a single source of truth. |
| 105 | + |
| 106 | +### `internal/dbdefaults/dbdefaults.go` — shared database & Docker defaults |
| 107 | + |
| 108 | +| Constant | Value | Current duplication | |
| 109 | +|----------|-------|---------------------| |
| 110 | +| `PostgresImage` | `"postgres:16-alpine"` | 7 test files + docker-compose.yml | |
| 111 | +| `TestUser` | `"mantle"` | 6 testcontainers setups | |
| 112 | +| `TestPassword` | `"mantle"` | 6 testcontainers setups | |
| 113 | +| `TestDatabase` | `"mantle_test"` | 6 testcontainers setups | |
| 114 | +| `ContainerName` | `"mantle-postgres"` | new (Docker provisioning) | |
| 115 | + |
| 116 | +### `internal/netutil/loopback.go` — loopback detection |
| 117 | + |
| 118 | +| Function/Const | Purpose | Current duplication | |
| 119 | +|----------------|---------|---------------------| |
| 120 | +| `IsLoopback(host string) bool` | Returns true for localhost, 127.0.0.1, ::1 | config.go SSL warning + new init.go recovery | |
| 121 | + |
| 122 | +### `internal/budget/budget.go` — reset mode constants |
| 123 | + |
| 124 | +| Constant | Value | Current duplication | |
| 125 | +|----------|-------|---------------------| |
| 126 | +| `ResetModeCalendar` | `"calendar"` | config.go validation + budget logic + tests | |
| 127 | +| `ResetModeRolling` | `"rolling"` | config.go validation + budget logic + tests | |
| 128 | + |
| 129 | +These already live in the budget package conceptually; just promote the string literals to exported constants. |
| 130 | + |
| 131 | +## Files Changed |
| 132 | + |
| 133 | +### Modified |
| 134 | + |
| 135 | +| File | Change | |
| 136 | +|------|--------| |
| 137 | +| `internal/cli/init.go` | Add connection recovery flow, Docker provisioning, interactive prompts | |
| 138 | +| `internal/config/config.go` | Use `netutil.IsLoopback` for SSL warning, use `budget.ResetMode*` constants | |
| 139 | +| `internal/budget/budget.go` | Add `ResetModeCalendar` / `ResetModeRolling` constants, use them in existing logic | |
| 140 | +| `internal/auth/auth_test.go` | Use `dbdefaults` constants for testcontainers setup | |
| 141 | +| `internal/workflow/store_test.go` | Use `dbdefaults` constants | |
| 142 | +| `internal/db/migrate_test.go` | Use `dbdefaults` constants | |
| 143 | +| `internal/secret/store_test.go` | Use `dbdefaults` constants | |
| 144 | +| `internal/engine/test_helpers_test.go` | Use `dbdefaults` constants | |
| 145 | +| `internal/budget/store_test.go` | Use `dbdefaults` constants | |
| 146 | +| `internal/connector/postgres_test.go` | Use `dbdefaults.PostgresImage` | |
| 147 | +| `site/src/components/GetStarted.astro` | Remove `docker compose up -d` from step 2, simplify to just `mantle init` | |
| 148 | +| `site/src/content/docs/getting-started/index.md` | Update quickstart to remove Docker prerequisite, explain `mantle init` handles DB setup | |
| 149 | + |
| 150 | +### New |
| 151 | + |
| 152 | +| File | Purpose | |
| 153 | +|------|---------| |
| 154 | +| `internal/dbdefaults/dbdefaults.go` | Shared Postgres image, test credentials, container name constants | |
| 155 | +| `internal/netutil/loopback.go` | `IsLoopback` function for host classification | |
| 156 | +| `internal/netutil/loopback_test.go` | Tests for loopback detection | |
| 157 | +| `internal/cli/docker.go` | Docker availability check, container start, readiness polling | |
| 158 | +| `internal/cli/init_test.go` | Tests for connection recovery flow, non-interactive fallback | |
| 159 | +| `internal/cli/docker_test.go` | Tests for Docker command construction, container name conflict handling | |
| 160 | + |
| 161 | +## Non-Goals |
| 162 | + |
| 163 | +- **Config file generation**: `mantle init` does not create `mantle.yaml`. The defaults work with the Docker container. |
| 164 | +- **Docker Compose**: we use `docker run`, not `docker compose`. No dependency on a compose file. |
| 165 | +- **Custom port/user/password in Docker flow**: always matches defaults. Users who need custom config can use the connection string prompt. |
| 166 | +- **Container lifecycle management**: `mantle init` starts the container; it doesn't stop or remove it. Users manage that themselves. |
| 167 | + |
| 168 | +## Testing Strategy |
| 169 | + |
| 170 | +- **Loopback detection**: unit test `isLoopback` with localhost, 127.0.0.1, ::1, remote hosts, IPv6 |
| 171 | +- **Non-interactive detection**: unit test that piped stdin skips prompts and returns error |
| 172 | +- **Docker command construction**: verify the exact `docker run` args match defaults |
| 173 | +- **Integration**: testcontainers already covers the migration path; the new code paths are the interactive/Docker shell-out portions which are unit-tested with mocked exec |
| 174 | +- **Site content**: manual verification that quickstart steps are accurate |
0 commit comments