@crvouga/atlas 0.17.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +52 -8
  3. package/dist/core/src/cli.d.ts +1 -1
  4. package/dist/core/src/cli.js +1 -1
  5. package/dist/reports/src/garbage.d.ts +39 -0
  6. package/dist/reports/src/garbage.js +82 -0
  7. package/dist/reports/src/report-index.d.ts +6 -0
  8. package/dist/reports/src/report-index.js +14 -0
  9. package/dist/schema/src/index.d.ts +1 -0
  10. package/dist/schema/src/index.js +1 -0
  11. package/dist/schema/src/server-config.d.ts +7 -49
  12. package/dist/schema/src/server-config.js +8 -21
  13. package/dist/schema/src/user-state.d.ts +115 -0
  14. package/dist/schema/src/user-state.js +54 -0
  15. package/dist/server/src/app.d.ts +16 -2
  16. package/dist/server/src/app.js +62 -0
  17. package/dist/server/src/config.d.ts +16 -21
  18. package/dist/server/src/config.js +48 -57
  19. package/dist/server/src/dev.d.ts +1 -1
  20. package/dist/server/src/dev.js +20 -7
  21. package/dist/server/src/index.d.ts +5 -1
  22. package/dist/server/src/index.js +5 -1
  23. package/dist/server/src/main.d.ts +23 -7
  24. package/dist/server/src/main.js +72 -26
  25. package/dist/server/src/openapi.js +24 -5
  26. package/dist/server/src/projects.js +89 -14
  27. package/dist/server/src/settings.d.ts +23 -7
  28. package/dist/server/src/settings.js +50 -60
  29. package/dist/server/src/storage.d.ts +68 -0
  30. package/dist/server/src/storage.js +125 -0
  31. package/dist/server/src/user-state.d.ts +31 -0
  32. package/dist/server/src/user-state.js +74 -0
  33. package/docs/agents.md +1 -1
  34. package/docs/deploying.md +38 -39
  35. package/docs/reference/cli.md +12 -7
  36. package/docs/reference/http-api.md +30 -2
  37. package/docs/reference/reports.md +71 -0
  38. package/docs/reference/schema.md +102 -16
  39. package/docs/reference/server.md +263 -41
  40. package/docs/report-backends.md +0 -144
  41. package/docs/report-contract.md +1 -1
  42. package/docs/runners.md +1 -1
  43. package/docs/server.md +141 -36
  44. package/docs/statecharts.md +1 -1
  45. package/docs/ui.md +1 -1
  46. package/docs/workspaces.md +5 -2
  47. package/llms-full.txt +231 -234
  48. package/llms.txt +1 -1
  49. package/package.json +2 -2
  50. package/packages/reports/STORAGE.md +1 -1
  51. package/packages/schema/schemas/gc-policy.schema.json +36 -0
  52. package/packages/schema/schemas/gc-run-result.schema.json +106 -0
  53. package/packages/schema/schemas/gc-snapshot.schema.json +122 -0
  54. package/packages/schema/schemas/server-config.schema.json +0 -41
  55. package/packages/schema/schemas/server-editable-settings.schema.json +0 -41
  56. package/packages/schema/schemas/server-settings-snapshot.schema.json +0 -82
  57. package/packages/schema/schemas/user-state.schema.json +140 -0
  58. package/packages/server/README.md +11 -6
  59. package/packages/server/openapi.json +510 -126
  60. package/packages/visualizer/src/admin/SettingsPage.tsx +183 -88
@@ -1,144 +0,0 @@
1
- # Report backends and live snapshots
2
-
3
- > [!NOTE]
4
- > Atlas has two kinds of backend, and this page is about the first.
5
- >
6
- > - **Run sources** (this page): where the client finds *statechart runs* (`manifest.json` per run)
7
- > and live snapshots of a run in progress: a directory, HTTP files, or an HTTP API.
8
- > - **Layer stores**: where *layers of evidence for any kind of spec* are kept, an `ObjectStore`
9
- > such as a directory or a bucket. Any number of them are one report. See
10
- > [the report contract](report-contract.md#backends) and [integrating Atlas](integrating.md).
11
- >
12
- > Publish a run as a layer (`atlas report publish --manifest`) to have it combine with other suites,
13
- > environments and branches.
14
-
15
- Atlas maps the product's areas, screens, events and journeys from a spec, then overlays evidence
16
- of what the product did. Specs and reports are independent. Local development, CI, device labs
17
- and hosted runners can supply reports to the same product atlas simultaneously. The client works
18
- before the first report exists.
19
-
20
- ## Connect sources
21
-
22
- The default source is the directory passed to `--runs`. Add sources with a JSON file:
23
-
24
- ```json
25
- {
26
- "schemaVersion": 1,
27
- "sources": [
28
- { "id": "mobile", "label": "Mobile runs", "type": "directory", "runs": "./mobile-runs" },
29
- { "id": "ci", "label": "CI reports", "type": "http", "baseUrl": "https://reports.example.com/atlas-data/" },
30
- { "id": "runner", "label": "Hosted runner", "type": "api", "baseUrl": "https://runner.example.com/atlas/", "eventsUrl": "events", "pollMs": 10000 }
31
- ]
32
- }
33
- ```
34
-
35
- ```sh
36
- atlas client dev --specs ./specs --runs ./atlas-runs --sources ./atlas.sources.json
37
- atlas client build --specs ./specs --runs ./atlas-runs --sources ./atlas.sources.json --out ./atlas-site
38
- atlas client publish-data --specs ./specs --runs ./atlas-runs --sources ./atlas.sources.json --out ./atlas-data
39
- ```
40
-
41
- Directory paths resolve against the configuration file. Source ids are unique, stable names
42
- containing letters, numbers, underscores or hyphens. `workspace` is reserved for `--runs`.
43
- `ATLAS_SOURCES_FILE` is the environment equivalent. Restart dev after changing the file.
44
- Credentials must not appear in this file or URLs: descriptors are sent to the browser and
45
- included in static output.
46
-
47
- Remote services must allow requests
48
- from the client's origin. For private services, use an authenticated same-origin gateway. Atlas
49
- does not download private GitHub artifacts or sign object-storage requests itself: expose their
50
- published data over HTTP or implement a gateway using the contract below.
51
-
52
- ## Transport contracts
53
-
54
- | Backend | Index | Snapshot | Media and other reports | Updates |
55
- | --- | --- | --- | --- | --- |
56
- | Directory | Generated from run directories | `<run>/manifest.json` | Files beside the manifest | File watcher and polling fallback |
57
- | HTTP files | `runs/index.json` | `runs/<id>/manifest.json` | `runs/<id>/<path>` | Polling; optional server events |
58
- | HTTP API | `runs` | `runs/<id>/manifest` | `runs/<id>/files/<path>` | Polling; optional server events |
59
-
60
- Locations are relative to `baseUrl`. The index follows the Atlas runs-index contract:
61
-
62
- ```json
63
- {
64
- "runs": [{
65
- "id": "build-123",
66
- "startedAt": "2026-10-09T11:59:00Z",
67
- "progress": "running",
68
- "hasManifest": true,
69
- "execution": {
70
- "totalPaths": 8,
71
- "completedPaths": 2,
72
- "activePaths": ["journey-3"],
73
- "updatedAt": "2026-10-09T12:00:00Z"
74
- }
75
- }]
76
- }
77
- ```
78
-
79
- Each manifest is a full snapshot of everything received so far, rather than a patch or unfinished
80
- JSON document. Records can be absent; malformed records are reported individually. Set
81
- `run.finishedAt` to `null` while running, then a timestamp when complete. Its absence preserves
82
- the meaning of older completed reports. Optional `run.progress` has the same shape as `execution`
83
- above. Paths can declare `progress: queued | running | complete`.
84
-
85
- Unvisited `not-reached` records appear pending during an active run. Passed and failed records
86
- appear immediately. A PNG can arrive before its optional WebP thumbnail. Promised media can
87
- declare `screenshotState: processing` or `clip.state: processing` until ready.
88
-
89
- The runner publishes an initial snapshot, each reached screen and completed step, each completed
90
- path and run completion. File replacements are atomic. Active outcomes stay out of
91
- `outcomes.json`, so merging and rerunning reuse only completed paths. Results retain deterministic
92
- plan order across workers.
93
-
94
- ## Server events
95
-
96
- An optional `eventsUrl`, relative to `baseUrl`, serves `text/event-stream`. Send ordinary messages
97
- or named `atlas:change` events:
98
-
99
- ```text
100
- event: atlas:change
101
- id: 124
102
- data: {"scope":"runs","runIds":["build-123"]}
103
-
104
- ```
105
-
106
- These invalidate the source's index and named snapshots. Empty `runIds` refreshes cached snapshots
107
- for this source. Reconnection does the same to recover missed updates. Malformed notices are
108
- ignored. Polling remains available if a stream disconnects or EventSource is unavailable.
109
-
110
- Configured source indexes poll every ten seconds by default (`pollMs` overrides this); selected
111
- running snapshots poll every two seconds. `VITE_ATLAS_POLL_MS` controls spec polling. Pause live
112
- updates to keep a snapshot steady; manual Refresh still works. Resuming refreshes immediately.
113
-
114
- ## Identity, failures and publishing
115
-
116
- Client identities combine source ids and native run ids. Cache keys include the backend location
117
- and transport. Matching run names cannot mix results or media. Shared links retain the qualified
118
- identity; old links containing only a run id still resolve to workspace reports.
119
-
120
- One unavailable source leaves other sources and the spec usable. Failed refreshes retain the
121
- last valid index and snapshot, with the failure recorded in data issues. A missing first snapshot
122
- is a waiting state for a running report. Latest follows the newest report, including active runs;
123
- choosing a particular report keeps it selected as newer reports arrive.
124
-
125
- Publishing includes privacy-checked running snapshots and finished runs. Extra directory sources
126
- are copied under `data/backends/<id>/`; `sources.json` uses relative URLs. Remote descriptors stay
127
- remote. Missing or failed privacy checks exclude the run, and temporary files and work directories
128
- are excluded. `--keep` applies per directory source. Publish updated data to the same host while
129
- a run is active for static viewers to receive its snapshots through polling.
130
-
131
- `VITE_ATLAS_BASE_URL` sets the primary spec/data location; `VITE_ATLAS_SOURCES_URL` can point to a
132
- descriptor file hosted elsewhere. Published descriptors translate directory sources into HTTP
133
- URLs and never expose the directory paths.
134
-
135
- ## Another transport
136
-
137
- `src/data/adapters/report-backend.ts` defines `ReportBackend`: stable identity and cache key,
138
- report listing, snapshot loading, media resolution and optional change subscription. The query
139
- layer consumes this separately from the spec adapter. An embedded client can supply a backend
140
- through `ReportSourcesContext`; a new built-in transport can add a factory alongside
141
- `createReportBackend`. Screens, maps and journeys require no transport-specific code.
142
-
143
- The generated JSON Schemas include `report-sources`, `atlas-change`, `run-info` and `run-summary`
144
- alongside the existing manifest and record schemas.
@@ -211,7 +211,7 @@ are part of the contract:
211
211
  | **Partition independence** | Splitting the same layers across any number of backends, machines or branches, with any overlap, gives the same report. This is what makes partial runs on separate nodes safe. |
212
212
  | **Idempotence** | A layer present twice, in one backend or several, counts once. Publishing the same checks again writes the same layer. |
213
213
  | **Monotonic** | A layer changes only the slots it fills. Adding a layer never changes a result it did not record. |
214
- | **Deletion-free** | Nothing in the contract removes or rewrites a layer. Retention is the operator's choice. |
214
+ | **Deletion-free** | Nothing in the contract removes or rewrites a layer. Retention is the operator's choice: Atlas deletes only under a garbage collection policy an operator saved in the UI, and then only layers that no longer decide any result ([the server](server.md#user-state-and-garbage-collection)). |
215
215
  | **Unknown is not a pass** | A layer that cannot be read, a backend that is down, and a check that is `running` or `not-run` never count as passing. The gate fails closed. |
216
216
  | **Aggregation by copying** | Layers are immutable and never share a key with different bytes, so combining stores is copying files (`atlas report merge`, `cp -R`, `rsync`, a CI artifact download). Copying is idempotent and commutative. |
217
217
 
package/docs/runners.md CHANGED
@@ -64,6 +64,6 @@ Playwright, Vitest, Bun, WebDriver/Appium and Detox already have Atlas statechar
64
64
 
65
65
  ## Operate the singleton
66
66
 
67
- Run `atlas server` once for the projects the node hosts; point local and CI publishers at that URL and set each project's `ATLAS_PROJECT`. Configure backends using environment variables or open **Settings** in the client (`/admin`). Authentication and listening addresses remain deployment configuration. Settings changes are validated and saved atomically for the next server restart. Environment variables and flags override saved values. A backend change does not migrate historical data: retain the old backend as a read source when switching the upload destination.
67
+ Run `atlas server` once for the projects the node hosts; point local and CI publishers at that URL and set each project's `ATLAS_PROJECT`. The node keeps everything in the S3 bucket named by the required `ATLAS_S3_*` variables ([required S3 storage](server.md#required-s3-storage)); open **Settings** in the client (`/admin`) to define the garbage collection policy and report settings, which are saved as JSON in that bucket. Authentication, listening addresses and storage remain deployment configuration. Settings changes are validated and saved for the next server restart. Environment variables and flags override saved values.
68
68
 
69
69
  Native reporters send a heartbeat every 5 seconds. The server interrupts invocations after 30 seconds of silence, or after the configurable progress-stall timeout. It preserves finished results and never marks missing tests as passed. See [live test runs](server.md#live-test-runs-and-heartbeats).
package/docs/server.md CHANGED
@@ -9,22 +9,24 @@ development and in production.
9
9
  CI job ──────┼── PUT /api/store/… POST /api/checks ─▶│
10
10
  device lab ──┘ atlas report publish | stream │ atlas report status (the gate: asks /api/status)
11
11
  │
12
- backends: ./atlas-data (default), S3, other folders, another Atlas server
12
+ one S3-compatible bucket: ALL reports and ALL user state (required)
13
13
  ```
14
14
 
15
15
  - **Clients upload directly to it.** `atlas report publish`, `atlas report stream`, `createReporter` and a plain
16
16
  `curl` all end up as layers on the server. Nothing else needs deploying: no bucket access for CI, no artifact
17
17
  merging, no static site to rebuild.
18
- - **Its default backend is its own filesystem** (`./atlas-data`). That folder is the whole state: back it up,
19
- mount it on a volume, or put the state in a bucket instead ([backends](#backends)). Several backends are one
20
- report, as ever.
18
+ - **Its only storage is an S3-compatible bucket.** All reports and all user state (the garbage collection policy,
19
+ saved settings) live there and nowhere else: not on its disk, in memory, in a database or in the browser. The
20
+ storage settings are required and the server refuses to start without them
21
+ ([required S3 storage](#required-s3-storage)).
21
22
  - **The report is live.** Every client holds one server-sent-events connection; when results arrive the server
22
23
  says so and each client reads only what changed ([streaming](#streaming)).
23
24
  - **Layered, never partitioned**, as everywhere in Atlas: the server only stores immutable, content-addressed
24
25
  layers and works out the report from them. Replace it, restore it, or run a second one on a copy of the data:
25
26
  the same layers give the same report ([the contract](report-contract.md)).
26
27
 
27
- - [Run it](#run-it) · [Configuration](#configuration) · [Backends](#backends) · [Tokens and roles](#tokens-and-roles)
28
+ - [Run it](#run-it) · [Required S3 storage](#required-s3-storage) · [User state and garbage collection](#user-state-and-garbage-collection)
29
+ - [Configuration](#configuration) · [Storage](#storage) · [Tokens and roles](#tokens-and-roles)
28
30
  - [Sending results](#sending-results) · [Streaming](#streaming) · [API reference](#api-reference)
29
31
  - [What the server refuses](#what-the-server-refuses) · [Performance](#performance) · [Operating it](#operating-it)
30
32
 
@@ -33,7 +35,8 @@ development and in production.
33
35
  ```sh
34
36
  bun add -d @crvouga/atlas
35
37
  export ATLAS_TOKEN=$(openssl rand -hex 24) # admin token; see "Tokens and roles"
36
- bunx atlas serve # http://127.0.0.1:8080, data in ./atlas-data
38
+ cp .env.example .env # then fill in the ATLAS_S3_* variables: storage is required
39
+ bunx atlas serve # http://127.0.0.1:8080, everything kept in your S3 bucket
37
40
  ```
38
41
 
39
42
  <!-- generated:runtime-requirements -->
@@ -49,21 +52,118 @@ server in front of it. Vite proxies `/api` (server-sent events included) to the
49
52
  uploaders and the client all use **one host**, exactly as in production, and a change to the client shows at once.
50
53
 
51
54
  ```sh
52
- bunx atlas dev --data .atlas-data # http://127.0.0.1:5180
55
+ bunx atlas dev # http://127.0.0.1:5180 (same ATLAS_S3_* variables)
53
56
  ATLAS_STORE=http://127.0.0.1:5180 bunx atlas report publish --producer unit --junit out.xml
54
57
  ```
55
58
 
56
59
  For Railway, deploy `ghcr.io/crvouga/atlas:latest` directly: the image contains the server and
57
60
  viewer, and every green `main` push updates it. See [running Atlas for a project](deploying.md#run-it) for
58
- Railway's volume, tokens, healthcheck and image auto updates, or for Docker, Kubernetes and CI.
61
+ Railway's variables, tokens, healthcheck and image auto updates, or for Docker, Kubernetes and CI.
59
62
  For anonymous reading, uploads and administration, use `ghcr.io/crvouga/atlas-insecure:latest` with no tokens
60
63
  configured; see [deploying without authentication](deploying.md#deploy-without-authentication).
61
64
 
65
+ ## Required S3 storage
66
+
67
+ The server keeps **all reports and all user state in S3-compatible object storage, and nowhere else**. There is no
68
+ local-disk, in-memory or database fallback and no flag to choose another place, so the storage settings are
69
+ **required environment variables with no defaults** (they hold secrets, so they are never read from a file or a flag):
70
+
71
+ | Variable | Required | Meaning |
72
+ | --- | --- | --- |
73
+ | `ATLAS_S3_ENDPOINT` | yes | S3 API endpoint, e.g. `https://s3.eu-west-1.amazonaws.com`, `https://<account>.r2.cloudflarestorage.com`, `http://minio:9000` |
74
+ | `ATLAS_S3_BUCKET` | yes | Bucket for reports and user state. It must already exist |
75
+ | `ATLAS_S3_REGION` | yes | Bucket region (`auto` for Cloudflare R2, `us-east-1` for most self-hosted servers) |
76
+ | `ATLAS_S3_ACCESS_KEY_ID` | yes | Access key of an account that may list, read, write and delete in the bucket and prefix |
77
+ | `ATLAS_S3_SECRET_ACCESS_KEY` | yes | Its secret key |
78
+ | `ATLAS_S3_PREFIX` | no | A folder inside the bucket for everything Atlas writes. Default: the bucket root |
79
+ | `ATLAS_S3_PATH_STYLE` | no | `1` (default) for `endpoint/bucket/key`, `0` for virtual-hosted `bucket.endpoint/key` |
80
+ | `ATLAS_S3_SESSION_TOKEN` | no | Only for temporary credentials |
81
+
82
+ [`.env.example`](https://github.com/crvouga/atlas/blob/main/.env.example) lists them with explanations. The server does not look for
83
+ ambient AWS credentials (`AWS_*`, profiles, instance roles) and the old `ATLAS_STORE`, `ATLAS_WRITE`, `ATLAS_DATA`,
84
+ `--store`, `--write`, `--data` and the `data`, `backends` and `write` keys of `atlas.server.json` are gone; a
85
+ configuration file that still has one of the keys is refused with a message that says so.
86
+
87
+ **It fails fast and loud.** Before anything listens, the server checks every variable, then proves the bucket is
88
+ usable (lists it, writes and deletes a scratch object) and reads the user state. If any step fails it prints a banner
89
+ that lists **every** variable with `[MISSING]`, `[INVALID]`, `[ok]` or `[not set]`, says why storage is required (and
90
+ what S3 answered, when S3 was reached), never prints a secret, and exits with status **2**:
91
+
92
+ ```text
93
+ ==============================================================================
94
+ ATLAS CANNOT START: S3 STORAGE IS NOT USABLE
95
+ ==============================================================================
96
+ Why: the S3 configuration is missing or invalid.
97
+ Reason: Atlas stores ALL reports and ALL user state (the garbage collection policy and saved
98
+ settings) in S3-compatible object storage, and nowhere else. There is no local-disk,
99
+ in-memory or database fallback, so the server refuses to start without working S3.
100
+
101
+ Required environment variables:
102
+ [MISSING] ATLAS_S3_ENDPOINT S3 API endpoint of the object storage service
103
+ [ok] ATLAS_S3_BUCKET Bucket that holds reports and user state; it must already exist
104
+ [INVALID] ATLAS_S3_REGION must be lowercase letters, digits and hyphens, like eu-west-1
105
+ …
106
+ Fix these 2: ATLAS_S3_ENDPOINT, ATLAS_S3_REGION
107
+ ```
108
+
109
+ Credentials that S3 rejects, a bucket that does not exist and a user state object that cannot be read stop the
110
+ server in the same way, with the answer from S3 in the banner. `atlas dev` behaves the same.
111
+
112
+ ## User state and garbage collection
113
+
114
+ Everything a person using Atlas has chosen is **user state**: one JSON document, `server/state.json` under
115
+ `ATLAS_S3_PREFIX`, in the same bucket as the reports. It is never kept on the server's disk or in the browser.
116
+
117
+ ```json
118
+ {
119
+ "schemaVersion": 1,
120
+ "gc": { "enabled": true, "maxAgeDays": 90, "keepLatest": 20, "intervalHours": 24 },
121
+ "settings": { "refreshSeconds": 30 }
122
+ }
123
+ ```
124
+
125
+ (published as [`user-state.schema.json`](https://github.com/crvouga/atlas/blob/main/packages/schema/schemas/user-state.schema.json))
126
+
127
+ - **`gc` is the garbage collection policy**, defined in the Atlas UI (**Settings**, `/admin`, *Garbage collection*) and
128
+ read by the server **from S3 every time it collects**, so a saved policy applies at the next pass without a restart.
129
+ - **`settings`** are the report and live-run settings saved from the same page. They apply at the next start, below
130
+ `atlas.server.json`, the environment and the flags in precedence.
131
+ - A document that does not exist is an empty state; one that exists but cannot be parsed stops the server and is
132
+ never overwritten.
133
+
134
+ **No policy saved yet.** Atlas is deletion-free until someone says otherwise: with no `gc` in the document,
135
+ garbage collection is **off and nothing is ever deleted**. The server says so at start, the API answers
136
+ `saved: false, policy: null`, the page shows a "No policy has been saved yet" notice with suggested values that take
137
+ effect only after you turn collection on and save, and `POST /api/settings/gc/run` answers 409. There is no
138
+ fallback to a file, an environment variable or built-in numbers.
139
+
140
+ **What a policy deletes.** With `enabled: true`, every `intervalHours` (and shortly after start) the server applies the
141
+ policy to every project. It deletes a report layer only when all of these hold:
142
+
143
+ 1. the server has read it and every check it carries has been replaced by a newer layer, so it no longer decides any
144
+ result (deleting it cannot change the report);
145
+ 2. it was recorded more than `maxAgeDays` days ago;
146
+ 3. it is not one of the newest `keepLatest` layers of its project.
147
+
148
+ The layer's catalog entry goes first (so nothing visible points at a missing manifest), then its manifest, then the
149
+ captured files (screenshots, logs, …) that no remaining layer refers to; if any remaining layer cannot be read, no
150
+ file is deleted. Specs, runs and commit inventories are never touched, and a layer that failed to load, that
151
+ `include` hides, or that still decides a result is never a candidate. Because a deleted layer is also removed from the
152
+ test history, history older than the policy allows is gone after the next start. One narrow race remains: a publisher
153
+ that reuses a stored file at the very moment the last layer that used it is deleted can end up with a layer that
154
+ points at a missing file; keep `maxAgeDays` well above the time uploads take.
155
+
156
+ **Editing it.** `GET /api/settings/gc` and `PUT /api/settings/gc` (admin, with `If-Match` the revision that was read,
157
+ same-origin in browsers) read and save the policy; `POST /api/settings/gc/run` applies it now, and
158
+ `POST /api/settings/gc/run?dryRun=1` only counts what would go. A save that races another is refused with 409. The
159
+ revision is the SHA-256 of the stored document, so saving the policy and saving settings invalidate each other's
160
+ revision: reload, then save. An object store has no conditional writes, so run one server per bucket.
161
+
62
162
  ## Configuration
63
163
 
64
- Everything has a default, so no configuration is a working server for this machine. Settings come from
164
+ Everything but the [S3 storage](#required-s3-storage) has a default. Settings come from the settings saved in S3 (the lowest layer), then
65
165
  `atlas.server.json` (or `--config <file>` / `ATLAS_CONFIG`), then environment variables, then flags; a later one
66
- wins. A mistake (an unknown key, a short token, two write targets, listening beyond this machine without tokens)
166
+ wins. A mistake (an unknown key, a short token, listening beyond this machine without tokens)
67
167
  is said in words and the server does not start. The file has a published
68
168
  [JSON Schema](https://github.com/crvouga/atlas/blob/main/packages/schema/schemas/server-config.schema.json)
69
169
  (`@crvouga/atlas/schemas/server-config.schema.json`).
@@ -73,9 +173,6 @@ is said in words and the server does not start. The file has a published
73
173
  "$schema": "./node_modules/@crvouga/atlas/packages/schema/schemas/server-config.schema.json",
74
174
  "host": "0.0.0.0",
75
175
  "port": 8080,
76
- "data": "./atlas-data",
77
- "backends": [{ "id": "archive", "url": "s3://my-bucket/atlas/my-project?region=us-east-1", "label": "Archive" }],
78
- "write": "data",
79
176
  "auth": {
80
177
  "tokens": [
81
178
  { "name": "ci", "role": "publish", "tokenEnv": "ATLAS_CI_TOKEN" },
@@ -91,11 +188,16 @@ is said in words and the server does not start. The file has a published
91
188
  <!-- generated:server-settings -->
92
189
  | Setting | File key | Flag | Environment | Default |
93
190
  | --- | --- | --- | --- | --- |
191
+ | S3 endpoint URL (**required**) | `storage.s3.endpoint` | | `ATLAS_S3_ENDPOINT` | **none: the server refuses to start** |
192
+ | Bucket for all reports and user state (**required**) | `storage.s3.bucket` | | `ATLAS_S3_BUCKET` | **none: the server refuses to start** |
193
+ | Bucket region (**required**) | `storage.s3.region` | | `ATLAS_S3_REGION` | **none: the server refuses to start** |
194
+ | Access key id (**required**) | `storage.s3.accessKeyId` | | `ATLAS_S3_ACCESS_KEY_ID` | **none: the server refuses to start** |
195
+ | Secret access key (**required**) | `storage.s3.secretAccessKey` | | `ATLAS_S3_SECRET_ACCESS_KEY` | **none: the server refuses to start** |
196
+ | Folder in the bucket for everything Atlas writes (optional) | `storage.s3.prefix` | | `ATLAS_S3_PREFIX` | the bucket root |
197
+ | Path-style bucket addressing (`0` for virtual-hosted) (optional) | `storage.s3.pathStyle` | | `ATLAS_S3_PATH_STYLE` | yes |
198
+ | Session token for temporary credentials (optional) | `storage.s3.sessionToken` | | `ATLAS_S3_SESSION_TOKEN` | none |
94
199
  | Address to listen on | `host` | `--host <address>` | `ATLAS_HOST`, `HOST` | `127.0.0.1` |
95
200
  | Port (`0` picks a free one) | `port` | `--port <n>` | `ATLAS_PORT`, `PORT` | `8080` |
96
- | The server's own folder: the default backend and write target | `data` | `--data <dir>` | `ATLAS_DATA` | `./atlas-data` |
97
- | More backends, read together with the data folder | `backends` | `--store <[id=]url>` (repeatable) | `ATLAS_STORE` | none |
98
- | The backend uploads go to | `write` | `--write <id>` | `ATLAS_WRITE` | `data` |
99
201
  | The built client | `site` | `--site <dir>` | `ATLAS_SITE` | `./atlas-site`, else built once |
100
202
  | Whose specs the report is laid over | `outlineBranch` | `--outline-branch <branch>` | `ATLAS_OUTLINE_BRANCH` | `main`, then `master`, then the newest |
101
203
  | Only layers recorded on these branches take part | `include.branches` | `--branch <name>` (repeatable) | `ATLAS_ONLY_BRANCH` | all |
@@ -117,22 +219,19 @@ is said in words and the server does not start. The file has a published
117
219
  `ATLAS_HOST` and `ATLAS_PORT` win over `HOST` and `PORT`, which other tools on a machine often set for their own
118
220
  purposes.
119
221
 
120
- ## Backends
121
-
122
- A backend is anything that implements `ObjectStore`: a folder, an S3-compatible bucket (`s3://`, `r2://`,
123
- `gs://`), or **another Atlas server** (`https://…`, with `ATLAS_TOKEN` for it). The server reads every backend
124
- and shows one report; one that is down is named in the client and skipped, and the server recovers by itself.
222
+ ## Storage
125
223
 
126
- - **Uploads go to one backend**: the `data` folder, or the one named by `write` / `ATLAS_WRITE`. Name a bucket
127
- to make it the system of record, so the server holds nothing a replacement would lose:
224
+ The server has one backend, the S3 bucket named by the [required variables](#required-s3-storage); it reads every
225
+ layer in it and shows one report. If the bucket becomes unreachable the server keeps serving what it has, names the
226
+ backend in the client and `/readyz`, and recovers by itself.
128
227
 
129
- ```sh
130
- ATLAS_STORE=project=s3://my-bucket/atlas/my-project ATLAS_WRITE=project atlas serve
131
- ```
132
-
133
- - **Layers written to a backend by something else** (CI with bucket access, `atlas report merge`) are found at
134
- the next refresh. Layers uploaded through the server are in the report at once.
135
- - **A server can be a backend of another**: a local server reading the project's shared one shows both.
228
+ - **Uploads go to the bucket** and are in the report at once. Layers written to the bucket by something else (CI
229
+ with bucket access, `atlas report merge`) are found at the next refresh.
230
+ - **Projects** are folders in the bucket (`projects/<slug>/`), as [below](#multiple-projects-on-one-node).
231
+ - **Clients are different.** `atlas report publish --store …` on a laptop or CI job may name any store (a folder,
232
+ a bucket, this server's URL); this page is about the server.
233
+ - **Library use.** `createAtlasServer({ backends, write })` accepts any `ObjectStore` so an application embedding the
234
+ server, or a test, can bring its own; `atlas serve` and `atlas dev` always use S3.
136
235
 
137
236
  ## Tokens and roles
138
237
 
@@ -321,6 +420,9 @@ curl "$ATLAS_SERVER_URL/api/projects/$ATLAS_PROJECT/reports/tests?format=markdow
321
420
  | `GET /admin` | read | Server settings page. |
322
421
  | `GET /api/settings` | admin | Read server settings. |
323
422
  | `PUT /api/settings` | admin | Save settings for the next restart. |
423
+ | `GET /api/settings/gc` | admin | Read the garbage collection policy. |
424
+ | `PUT /api/settings/gc` | admin | Save the garbage collection policy. |
425
+ | `POST /api/settings/gc/run` | admin | Collect garbage now, or preview it. |
324
426
  | `GET /healthz` | none | Is the process up. |
325
427
  | `GET /readyz` | none | Has the server read its backends. |
326
428
  | `GET /api/openapi.json` | none | This document. |
@@ -449,11 +551,11 @@ What makes it so, none of it at the cost of the contract:
449
551
  - **One node for multiple projects.** Local environments and CI select their project slug and talk to the same node, which tells
450
552
  every client at once. Two servers on one backend both work and show the same report, but a client hears about
451
553
  an upload only from the server that took it and about the other's at the next refresh, so give a team one.
452
- - **State is the write backend.** Back up `./atlas-data` (or the bucket). Restoring it, or starting a new server
453
- on it, shows the same report.
554
+ - **State is the bucket.** Reports and user state are all in it. Back it up, replicate or version it as any data;
555
+ starting a new server on a restored copy shows the same report and applies the same garbage collection policy.
454
556
  - **Stopping.** `SIGTERM` stops accepting, lets requests in flight finish, ends the event streams and exits 0.
455
557
  - **Logs.** One JSON line per request (method, path, status, ms) to stdout, none for the probes. The first lines
456
- say what it reads and which backend takes uploads, and warn when there are no tokens.
558
+ say which bucket it uses, what the garbage collection policy in S3 is (or that none is saved), and warn when there are no tokens.
457
559
  - **A backend goes down.** The server keeps serving what it has, names the backend in the client and in
458
560
  `/readyz`, and recovers by itself. The gate fails closed: unknown is not a pass.
459
561
  - **Upgrades.** The layer format is a stable, versioned contract; upgrade the server before the clients that
@@ -463,7 +565,10 @@ What makes it so, none of it at the cost of the contract:
463
565
 
464
566
  Open **Settings** (`/admin`) and enter an admin token. The token stays in page memory. Read and publish tokens cannot edit settings; the browser sign-in box only grants reading. The API uses `Authorization: Bearer` and refuses cross-origin browser writes.
465
567
 
466
- Use the Storage, Live updates, and Report rules forms to edit saved settings. Advanced includes request limits, logging, a JSON editor, and a read-only view of the active configuration. Save changes and restart Atlas to apply them. Atlas validates the configuration and atomically replaces `atlas.server.json`, preserving authentication and deployment settings. Changes apply at the next restart; environment variables and flags retain precedence. Concurrent edits require reloading the current revision. Data is never moved or removed by changing settings. Keep an old backend as a read source when moving future uploads to a new one. Embedded servers can provide their own settings persistence or manage configuration outside this API.
568
+ The page has a Storage notice (the bucket is set by the `ATLAS_S3_*` variables and cannot be changed here), the **Garbage collection** form, and the Live updates and Report rules forms. Advanced includes request limits, logging, a JSON editor, and a read-only view of the active configuration.
569
+
570
+ - **Garbage collection** edits the policy: enabled, delete layers older than N days, always keep the newest N layers, run every N hours. *Save garbage collection policy* writes it to S3 at once (it applies at the next pass, no restart). *Preview deletions* counts what a pass would delete, per project, and deletes nothing. See [user state and garbage collection](#user-state-and-garbage-collection).
571
+ - **Settings** forms save to the same JSON document in S3 and apply after a restart. Atlas validates them against the whole deployment first, and the configuration file, environment variables and flags retain precedence. Concurrent edits require reloading the current revision. Data is never moved or removed by changing settings. Embedded servers can provide their own persistence or manage settings outside this API.
467
572
 
468
573
  Native test reporters for CodeceptJS, Mocha, Jest and Node upload directly to this server; see [runner setup](runners.md). Every publisher uses the same server address and only a publish token. Storage credentials belong in the server environment.
469
574
 
@@ -475,13 +580,13 @@ Start with `POST /api/live/<uuid>`, send ordered check deltas with `PATCH /api/l
475
580
 
476
581
  The server uses its own receipt clock. By default it interrupts a run after 30 seconds without a heartbeat (`ATLAS_RUN_TIMEOUT_SECONDS`). Heartbeats do not reset progress: after 15 minutes with no changed tests/steps, a separate stall timeout interrupts it (`ATLAS_RUN_STALL_SECONDS`, 0 disables). Both settings are editable in the admin UI. Healthy long tests continue renewing heartbeats; increase or disable the progress timeout for tests that legitimately make no observable progress for longer.
477
582
 
478
- Interrupted runs retain completed results and show why the remaining work stopped. Missing outcomes never become passes. The server persists bounded summaries under `server/live/` in its write backend (an HTTP relay keeps operational leases in its own data folder; the remote server only accepts immutable report objects) and restores outstanding leases as interrupted after a server restart. S3 server credentials need GetObject/PutObject for that prefix; generate the updated `atlas report policy --role both` policy. Producers still receive no backend credentials. Live `run` events carry bounded snapshots directly into test rows, including step logs and interruption reasons. The legacy `atlas:change` announcement remains available for older clients; its optional `liveRunId` identifies lease updates. Reconnecting viewers re-fetch progress, reports, and specifications. With SSE connected, live progress uses a 60-second safety refresh; without SSE it polls every two seconds. Returning to the tab or coming back online also revalidates reports and progress.
583
+ Interrupted runs retain completed results and show why the remaining work stopped. Missing outcomes never become passes. The server persists bounded summaries under `server/live/` in its bucket and restores outstanding leases as interrupted after a server restart. S3 server credentials need GetObject/PutObject for that prefix; generate the updated `atlas report policy --role both` policy. Producers still receive no backend credentials. Live `run` events carry bounded snapshots directly into test rows, including step logs and interruption reasons. The legacy `atlas:change` announcement remains available for older clients; its optional `liveRunId` identifies lease updates. Reconnecting viewers re-fetch progress, reports, and specifications. With SSE connected, live progress uses a 60-second safety refresh; without SSE it polls every two seconds. Returning to the tab or coming back online also revalidates reports and progress.
479
584
 
480
585
  In this repository, use `bun atlas` and `bun example:run` in two terminals. Every example Gherkin scenario executes, including sharing and renaming; `@unrecorded` no longer excludes tests.
481
586
 
482
587
  ## Multiple projects on one node
483
588
 
484
- Start the node with `atlas server` (or `bun atlas` in this repository). The server builds or finds the Atlas client and serves it with the API from one process. It never imports a consumer or an example. In this repository the node uses port 5180 and `./atlas-data`; `bun example:run` publishes the Todo app to project `todo-app`.
589
+ Start the node with `atlas server` (or `bun atlas` in this repository). The server builds or finds the Atlas client and serves it with the API from one process. It never imports a consumer or an example. In this repository the node uses port 5180 and the bucket named by your `.env`; `bun example:run` publishes the Todo app to project `todo-app`.
485
590
 
486
591
  Set `ATLAS_PROJECT=todo-app` alongside `ATLAS_SERVER_URL` for native reporters and statechart runs. Report CLI commands accept `--project todo-app` (or `ATLAS_PROJECT`) with an HTTP store. Programmatic HTTP stores, live reporters and native reporter options accept `project: 'todo-app'`. Keep the same slug for the invocation's live progress, final results, spec sets and recordings. Slugs use 1–64 lowercase letters, digits or hyphens, starting and ending with a letter or digit.
487
592
 
@@ -80,7 +80,7 @@ In another terminal run `bun run example:run`: statecharts, Gherkin scenarios an
80
80
  to project `todo-app` on that server while testing. Producers renew a heartbeat every 5 seconds; 30 seconds of silence
81
81
  marks an invocation interrupted. Final specs and recordings upload when their privacy scan completes.
82
82
  Tests show inline running indicators and logs without waiting for the whole suite to finish. Report history
83
- is retained in `atlas-data/projects/todo-app`; local run artifacts remain in `atlas-runs` and
83
+ is retained in the server's bucket under `projects/todo-app/`; local run artifacts remain in `atlas-runs` and
84
84
  `atlas-reports`. To upload a completed run again without rerunning tests,
85
85
  use `bun run example:publish`. Set `ATLAS_SERVER_URL` for a different upload destination (and
86
86
  `ATLAS_PUBLISH_TOKEN` when it requires one). Restart `bun atlas` when switching from the older
package/docs/ui.md CHANGED
@@ -125,7 +125,7 @@ The narrowly scoped exceptions are:
125
125
  | Evidence | Steps, retries, annotations, facts, unknown extensions, safe downloads and previews | List, Badge, Accordion, Code, Image, native media |
126
126
  | Recordings | Pictures/video/timeline, actor selection, moments, failure jumps, timing, model-state differences | Tabs, Slider, Button, ScrollArea, Table, Progress |
127
127
  | Search/help | Ranked and semantic search, shortcut help, recording shortcuts | Spotlight, Modal, Kbd |
128
- | Administration | Token access, storage backends/write destination, live timeouts, report rules, limits, raw JSON, conflicts | Same provider/theme, Fieldset, Mantine inputs, Accordion, Alert |
128
+ | Administration | Token access, S3 storage notice, garbage collection policy (saved in S3), live timeouts, report rules, limits, raw JSON, conflicts | Same provider/theme, Fieldset, Mantine inputs, Accordion, Alert |
129
129
 
130
130
  The hash routes (`/`, `/chart/$chartId`, `/specs/$kind`), query contracts, project scoping, streaming,
131
131
  report APIs and data adapters remain the migration boundary. `/admin` still serves an independent document:
@@ -59,8 +59,11 @@ bun run workspace:status
59
59
  ```
60
60
 
61
61
  Everything binds to `127.0.0.1`. For remote workspaces, use the IDE's port forwarding. Reports and server
62
- settings stay in this checkout's `.atlas-data/`; live runtime leases stay in `.atlas-workspace/`.
63
- Keep both directories private to each worktree. Do not share `node_modules`, `.atlas-data/`,
62
+ settings are kept in S3 only, never in the checkout: `workspace:run` needs the required `ATLAS_S3_*` variables
63
+ (copy `.env.example` to `.env`, which Bun loads, or export them) and exits at once, listing every missing one,
64
+ without them. Give each checkout its own bucket or `ATLAS_S3_PREFIX` if their reports must not mix
65
+ (`workspace:setup` and `workspace:preview` need no credentials). Live runtime leases stay in `.atlas-workspace/`.
66
+ Keep that directory private to each worktree. Do not share `node_modules`,
64
67
  `.atlas-workspace/` or caches across checkouts with different dependencies. Frozen installs use Bun's
65
68
  download cache without linking one worktree's dependency tree to another.
66
69