Skip to content
quarry Artifact [0af44d572e]
Login

Artifact [0af44d572e]

Artifact 0af44d572e04ba6b98056ba36f8475e425cfbfbdcb26ac2d973836d3a5f754d1:


# Running quarry

Two ways: the compose stack, or by hand. The hand-rolled path is what
this was developed against and is what the compose file encodes, so it
is written down first — a run book that stays true even if the images
are never built.

## What the stack is

| piece | role | state |
|---|---|---|
| **zit** | the store. quarry shells out to it for every read and write (0001 §2) | the `.zit` files themselves |
| **quarry** | the forge: indexes, UI, API, sync fronting | derived indexes, embedded NATS, OLTP database |
| **notify** | notifications: workflows, subscribers, topics, the inbox quarry embeds | SQLite |
| **dagnats** | the workflow engine notify runs on: embedded NATS + JetStream + an HTTP bridge | JetStream data |

`zit` is not a runtime dependency in the ordinary sense — it is quarry's
only store interface, so **its version is part of quarry's behaviour**.
On 2026-08-16 three zit builds changed what quarry does: `frames` got
31% faster, a card-parsing fix changed which artifacts parse at all (and
until it landed, sqlite's 794 tickets were invisible), and a new
`stat -R` let quarry serve a bare store. Pin the pair; do not let a
deploy resolve zit from whatever is on PATH.

## By hand

### dagnats

Runs under launchd on this machine as `com.dmestas.dagnats`, binding
NATS on 4222 and its bridge on 8080, with data in
`~/Library/Application Support/dagnats`.

```sh
launchctl kickstart -k gui/$(id -u)/com.dmestas.dagnats
```

A freshly-built Go binary replacing it must be **ad-hoc signed** or
launchd kills it with `OS_REASON_CODESIGNING` and the service silently
fails to come back:

```sh
go build -o ~/go/bin/dagnats ./cmd/dagnats
codesign -s - -f ~/go/bin/dagnats
launchctl kickstart -k gui/$(id -u)/com.dmestas.dagnats
```

In standalone mode dagnats binds **loopback**, which is why anything
talking to it shares its network namespace under compose rather than
reaching it by service name.

### notify

```sh
notify bootstrap -dev -db notify.db -org quarry -env dev   # prints apiKey once
notify serve -dev -db notify.db -addr 127.0.0.1:3000 \
  -nats-url nats://127.0.0.1:4222 -bridge-url http://127.0.0.1:8080
curl -s localhost:3000/readyz     # {"status":"ok","bridge":true,"db":true}
```

`-dev` derives a fixed, publicly-known master key. Outside development
set `NOTIFY_MASTER_KEY_HEX`.

Quarry triggers two workflows by name, so they must exist, each with an
`in_app` step or nothing reaches an inbox:

```sh
K=<apiKey>
for wf in quarry-activity quarry-mention; do
  curl -s -X POST localhost:3000/v1/workflows -H "Authorization: ApiKey $K" \
    -H 'Content-Type: application/json' -d "{
      \"name\":\"$wf\",\"triggerIdentifier\":\"$wf\",
      \"steps\":[{\"stepId\":\"S1\",\"type\":\"in_app\",
        \"controlValues\":{\"subject\":\"{{payload.summary}}\",
                           \"body\":\"{{payload.repo}} — by {{payload.actor}}\"}}]}"
done
```

**Activity is addressed to a topic, mentions to a person.** A repository's
activity goes to the topic `repo-<name>` (quarry sanitizes its own
`repo:<name>` convention, because notify accepts only lowercase letters,
digits, hyphens and underscores in a key and rejects the whole trigger
otherwise). A topic with no members notifies nobody, so subscribing is a
required step, not a nicety:

```sh
curl -s -X POST localhost:3000/v1/subscribers -H "Authorization: ApiKey $K" \
  -H 'Content-Type: application/json' -d '{"subscriberId":"you"}'
curl -s -X POST localhost:3000/v1/topics -H "Authorization: ApiKey $K" \
  -H 'Content-Type: application/json' -d '{"key":"repo-synthetic","name":"synthetic activity"}'
curl -s -X POST localhost:3000/v1/topics/repo-synthetic/subscribers \
  -H "Authorization: ApiKey $K" -H 'Content-Type: application/json' \
  -d '{"subscriberId":"you"}'
```

### quarry

```sh
quarry serve --addr 127.0.0.1:8950 --data ./data \
  --notify http://127.0.0.1:3000 --notify-key "$K" \
  --repo quarry=$HOME/quarry
```

`--data` is not optional in practice: it defaults to `.quarry` beside the
**first** `--repo`, which writes derived state into whichever repository
happens to be listed first. Always pass it.

Gates are open until they are closed, and closing them is two separate
credentials on purpose — accounts are the forge identity (argon2id), the
sync secret is the RFC §6.3 value the protocol fixes:

```sh
quarry account add --user you --admin --data ./data   # closes web/API writes
quarry user add --repo name=$HOME/quarry --user you --data ./data   # closes push
```

The account gate is **global**: the first account closes writes on every
repository in the fleet, not just one.

## Fleet admin

`/fleet` is the operator page — every store's ingest health, and the
upstreams it mirrors. It is **site admin** only: admin on one repository
is not enough, because the page reports operational detail across the
whole fleet and can force writes into any of it.

Health is per-process. `Total` and `Fallbacks` count what THIS server
has ingested since it started, so a restart with a warm index reports
zero of both however much the store holds. The parse-fallback number
worth looking at is the one a `quarry reindex` produces.

### Mirroring

A mirrored store tracks an upstream: quarry pulls, and never pushes.

```sh
curl -u you:pw -X POST http://localhost:8080/api/v1/mirrors \
  -H 'Content-Type: application/json' \
  -d '{"repo":"fossil","url":"https://fossil-scm.org/home","every":3600}'
```

`every` is seconds and is floored at 60 — these upstreams are other
people's servers, and the floor is a courtesy the configuration surface
cannot opt out of. `--mirror-tick` (default 1m) is how often due-ness is
evaluated, not how often anything is pulled.

Pulls take the store's single-writer lease, so mirroring is safe with
more than one node serving the same store. A pull that brings nothing
new does not trigger a re-index and is not logged; almost every one is
quiet, and a line per store per interval buries the ones that matter.

Two limits worth knowing before you rely on it:

- **The store needs a working checkout.** `zit pull` refuses `-R` and
  operates on a working directory, so a genuinely bare store cannot be
  mirrored. Stores created by `zit clone` or `zit import-fossil` carry
  one, so this bites only hand-built bare stores.
- **The upstream must allow anonymous clone.** Some Fossil servers serve
  a browsable timeline while refusing sync — `sqlite.org/lemon` is one.
  Note that `fossil clone` exits 0 on that refusal, so check for the
  repository, not the exit status.

## With compose

```sh
export NOTIFY_SRC=../notify           # a notify checkout; it builds dagnats too
export ZIT_REF=<commit>               # pin the store implementation
docker compose up --build
```

Then bootstrap notify once (`docker compose run --rm notify bootstrap …`),
put the key in `QUARRY_NOTIFY_KEY`, and create the workflows and topics
as above.

`docker run <image> cat /etc/quarry-build` reports which quarry and which
zit an image carries, without starting it.

> **Unverified.** `Dockerfile` and `compose.yml` have not been built —
> no container runtime is installed on the development machine. The two
> build recipes inside them *are* verified natively: zit compiles from a
> clean clone of the GitHub mirror with `zig build --release=fast`, and
> quarry builds fully static with `CGO_ENABLED=0` (its SQLite is
> WASM-based and Pikchr runs on wazero, so nothing needs a C toolchain).
> What remains untested is the image assembly and the compose wiring.

## Ports

| port | what |
|---|---|
| 4222 | dagnats NATS |
| 8080 | dagnats HTTP bridge (and its console — unauthenticated, keep it on loopback) |
| 3000 | notify API |
| 8950 | quarry |

quarry's own embedded NATS binds nothing (`DontListen`), so it never
collides.