TIER docs

TIER — Quickstart

Current access and retention: Real servers always require an issued key. Root reads everyone, node keys read their dated branch and personal keys read their holder. Named cards audit before construction and need no declaration or positive verified spend. Shared/static credentials and server reporting modes are retired. Team, division and company reports expand to their people; name-free monthly comparisons retain the k floor. Writable servers run raw retention at startup and daily (90 days by default, 60–365 days); pruning is permanent. Read-only servers and the synthetic demo do not initiate pruning. See identity access for current setup and trust limits.

Audience: anyone who wants to run TIER against a repository for the first time.

Version scope: installation examples target v0.7.0, pending publication. Developer access, coaching and retention below describe this release; the synthetic demo remains isolated from real data.

Who this release is for (v0.7.0). Run one TIER server for one organization. Administrators and host/database owners remain trusted with retained developer data. Give each developer a bound upload credential and use scoped organization keys for administration and monthly named branch reports. Named coaching uses the holder-key boundary described below; shared/admin uploads remain visible within that boundary without supplying census eligibility. Each person can use tierd score for their own local cost table or tierd score --outcomes for their own GitHub-token-bound personal TIER, with no persisted report or team band.

Access and trust (v0.7.0). Root keys read everyone, node keys (including read-only keys) read their dated branch and personal keys read their holder. Each developer ships with their own bound credential. Shared read and administrator upload credentials are retired. Administrators and host/database owners remain trusted operators, and host access can read the whole database. Team/division reports expand to their people; name-free comparisons retain the privacy floor. The coaching card (#1372) ships with raw-data retention (#1371) in v0.7.0; the separate per-issue report remains future work. Card reads require neither viewer declarations nor positive verified spend and log each view before construction on a writable server. The synthetic tierd demo keeps its startup exemption and invented data; tierd score remains a local cost report, with personal TIER available through --outcomes.

The k floor depends on counted identity evidence. Shared ingest no longer supplies that evidence; bound uploads and other qualifying sources do. Administrators can still pad a roster and provide qualifying evidence for invented people, so k does not protect against a malicious administrator. See the identity and census rules.


Table of contents

  1. Install
  2. See it work in one command
  3. Point it at your own repository
  4. The free cost view
  5. The full TIER score
  6. Capturing OpenAI Codex spend
  7. Viewing the dashboard from another machine
  8. Running on a different port
  9. What each command actually needs

1. Install

Building from source at this revision or later requires Go 1.27.3 or newer (v0.7.0; raised for GO-2026-6809); v0.6.1 shipped with Go 1.27.2. Download a release archive below if you do not have a Go toolchain.

go install github.com/tiermetric/tier/cmd/tierd@latest

@latest resolves to the newest tagged release — it does not track main, so you always get a published, reproducible version. Pin an exact tag instead (@v0.7.0, the release used as the example below) when you need a build that never moves, e.g. in CI.

go install puts the binary in $(go env GOPATH)/bin — usually ~/go/bin — or in GOBIN if you have set it. If the next command says command not found, add that folder to your PATH (e.g. export PATH="$HOME/go/bin:$PATH" in your shell profile) and open a new shell.

Or download a signed release archive from github.com/tiermetric/tier/releases. Each release carries one archive per platform, named tierd-<version>-<os>-<arch>.tar.gz: darwin-arm64 (Apple Silicon Mac), darwin-amd64 (Intel Mac), linux-amd64, linux-arm64, and windows-amd64, each with a .sha256 checksum beside it. The archive unpacks to a folder of the same name holding a single file, tierd (tierd.exe on Windows); move it somewhere on your PATH. <version> is the newest tag on the Releases page — v0.7.0 is the target release used below; these archives become available when it is published:

tar -xzf tierd-v0.7.0-darwin-arm64.tar.gz
sudo mv tierd-v0.7.0-darwin-arm64/tierd /usr/local/bin/
# optional: gh attestation verify tierd-v0.7.0-darwin-arm64.tar.gz --repo tiermetric/tier

Homebrew (macOS): brew install tiermetric/tap/tierd builds tierd from source.

macOS: if you downloaded the archive with a web browser, macOS marks the binary as downloaded, and because it is not notarized the system stops it from running. Remove that mark once (if it says No such xattr, there was no mark and nothing to do):

sudo xattr -d com.apple.quarantine /usr/local/bin/tierd

Windows: a windows-amd64 archive is published. A native command-line smoke test on Windows 11 Enterprise x64 (2026-10-22, synthetic data only) passed: download and checksum, tierd version, tierd demo, tierd serve, and tierd score on empty history, all on v0.6.2. Not yet tested there: scoring populated data, the dashboard against a real server, a browser download of the unsigned binary (SmartScreen), and this page's bash/zsh commands in PowerShell or cmd; there is no Windows CI. The 0600 guarantee does not apply (security.md §4): under your user profile, SYSTEM, Administrators and your account had full control; another folder gives its own permissions, and other accounts' access was not tested. Treat Windows as experimental.

Confirm the build:

tierd version        # also: tierd -version / --version / -v

2. See it work in one command

No configuration, no database, no repository — synthetic data:

tierd demo           # then open http://127.0.0.1:8080

Everything on that dashboard is invented (developers named demo-*, issues DEMO-*). It is the fastest way to see what TIER produces before pointing it at anything real.

3. Point it at your own repository

Before measuring anything, check that capture works on your machine:

cd ~/src/your-repo
tierd doctor --repo .

doctor reports what it can and cannot capture. A common first result is an attribution FAIL — TIER telling you that a large share of your AI spend cannot be tied to any issue. That is the tool working, not breaking: name branches feature/<issue-number>-slug so cost can be linked to an outcome, or lower --min-attribution if partial coverage is acceptable for this install.

4. The free cost view

tierd score --repo . --since 2026-05-01

From v0.7.0, this reads your own local Claude Code, Codex CLI, Opencode (Z.ai coding plan only) and Muse Code logs and prints cost per issue, per-source subtotals and the total — no server, no token, no GitHub access. Defaults are ~/.claude/projects/, ~/.codex/sessions/, ~/.local/share/opencode/opencode.db (opened read-only) and ~/.local/share/muse/sessions/. Override them with --claude-dir <dir>, --codex-sessions-dir <sessions-dir>, --opencode-db <file> and --muse-home <muse-home> (reads its sessions/ subdirectory). Missing sources are skipped with a stderr note. Opencode is opened read-only; SQLite may leave its -shm/-wal files beside it; nothing else is written. The “issue-attributed share of local AI spend (no outcome join)” is issue-linked spend divided by all captured spend, including every unattributed bucket in the denominator. With no spend the share is unavailable. The caveat names the server's real ranking requirements (at least 3 outcomes, at least $5 spend, zero flagged outcomes); there is no attributed-spend percentage ranking threshold.

For your own local TIER, add --outcomes:

# Set TIER_GITHUB_TOKEN privately, or add --token @/path/to/token.
tierd score --repo . --since 2026-05-01 --outcomes

This joins only the token owner’s merged PRs (GET /user) to your local costs, using the server’s scoring window and data-quality flags. Your OS username is aliased to that login for this run only. No team band is shown. Nothing persists: the private temporary SQLite directory is removed on exit, errors and Ctrl-C. HTTPS is required. Literal token flags, identity overrides, --db, --server and shipping flags are refused. On a fork, use --repo-slug upstream-owner/name. Costs from all four sources join only the token owner’s own merged PRs. Historical PRs start at quality 1.0; later CI failures and reverts are not reconstructed. Each outcomes run prints:

TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual.

5. The full TIER score

A TIER score divides merged work by AI spend, so it needs both loaded. Four steps: load your merged work, start the server, load the spend already on your disk, and join your two names if they differ.

Start this walkthrough in the current UTC month with a fresh database: the commands below load only that month, which closes 24 hours after its end. Import the roster and finish shipping and identity mapping before then. Writable serve closes due months at startup and every minute; later uploads cannot revise them. This sequence cannot bootstrap complete historical reports: use the offline score --outcomes walkthrough above for past work instead of loading partial history into this database. For branch reports, use the first full month after the roster takes effect.

# 1. Reconstruct outcomes from merged-PR history (do this BEFORE serve —
#    SQLite is single-writer). Needs a GitHub token that can read this repo's
#    pull requests: a fine-grained token with "Pull requests: Read-only" on
#    the one repo, saved to a file. Steps: README → "What you need before you
#    start" (https://github.com/tiermetric/tier#what-you-need-before-you-start).
tierd backfill --repo owner/name --token @$HOME/.tier/github-token \
  --since "$(date -u +%Y-%m-01)"

# 2. Run the server: it accepts shipped spend and serves the dashboard.
#    Leave it running. It does NOT record PRs merged from now on: it sets
#    no webhook secret (no --webhook-secret flag, TIER_WEBHOOK_SECRET or
#    config value), so the GitHub webhook is off, and GitHub cannot reach a
#    laptop's 127.0.0.1 anyway. To add later merges, stop serve, re-run the
#    backfill above (repeats are skipped), restart, then re-run step 3's
#    `tierd ship` below.
mkdir -p ~/.tier && chmod 700 ~/.tier
tierd serve --db ~/.tier/tier.db

Server reporting modes are retired. serve takes no aggregation setting; --aggregation, nonempty TIER_AGGREGATION, and the YAML aggregation key are rejected. Root keys read everyone, node keys read their dated branch, and personal keys read their holder. Organization, Team, Person and Models views remain available, including group-to-person expansion. Name-free monthly comparisons retain the k floor (default 5, minimum 3), complementary suppression. Archived raw-manifest verification retains its original pinned arithmetic; that pin cannot select a server access policy.

3. Load past spend. In a second terminal, import your team map with an issued root key (see the root bootstrap below), then issue your upload key on the host: tierd tokens issue --upload --subject YOUR_LOGIN --label laptop. Save the shown-once secret in a private file. While serve keeps running:

tierd ship --server http://127.0.0.1:8080 --repo ~/src/your-repo \
  --developer YOUR_LOGIN --bound-token @/secure/upload-key \
  --since "$(date -u +%Y-%m-01)"

This walkthrough starts no watcher: ship loads spend from the current UTC month and can be repeated for new activity before that month closes. Both backfill and ship above start at the current UTC month so startup cannot freeze older outcomes before their matching spend and roster arrive.

ship reads Claude Code sessions only, unless you add --codex-rollout for Codex (§6), --opencode for Opencode (Capturing Opencode; v0.5.2 or later, since v0.4.1 rejects the flag) or --muse for Meta's Muse Code (Capturing Meta Muse Code; also v0.5.2 or later). The step 2 serve records the shipped events whether or not it has that flag, so this step needs no restart. For serve to keep capturing that tool's new activity on this machine afterwards, configure its watcher separately; for this walkthrough, repeat ship with the same flags. Without the flag, a run on a repository where you used only Codex, Opencode or Muse finds nothing and stops with an error that blames the --repo path.

4. Join your two names. Spend from steps 2 and 3 is recorded under your computer's login name, which echo $USER prints. Merged work from step 1 is recorded under your GitHub login, which your GitHub profile page shows, or which gh api user -q .login prints if you have the GitHub CLI. If the two are the same, skip this step. If they differ, you appear twice in the scores: once with spend and no merged work, once with merged work and no spend.

Check before you join them. As the administrator, inspect your computer login with the authenticated export:

printf 'Authorization: Bearer %s\n' "$(cut -f4 ~/.tier/root-credential.tsv)" |
  curl --fail-with-body -H @- "http://127.0.0.1:8080/api/v1/developer/$USER/export"

This root-authenticated export works before the first monthly close. The sum of cost_micro in token_events must be above 0, and outcomes must be null or empty (no merged work). If the export fails, spend is absent, or any outcome is present, stop here. Merged work may belong to somebody whose GitHub login matches your computer login. The join moves both spend and merged work, so it would put that person's work into your score.

If the check passes, tell the server the two names are one person:

printf 'Authorization: Bearer %s\n' "$(cut -f4 ~/.tier/root-credential.tsv)" |
  curl --fail-with-body -H @- -X POST http://127.0.0.1:8080/api/v1/developer_alias \
  -H "Content-Type: application/json" \
  -d '{"alias":"'"$USER"'","canonical":"<github-login>"}'

The shell fills in your computer login from $USER; replace <github-login> with your GitHub login. The server answers 201 Created with an empty body, so the command prints nothing when it works. If the server refuses the join, for example because one of the names is already part of another join, it prints the reason. The join also applies to spend and work already loaded. To undo it, delete it by your computer login, or post it again with the right GitHub login:

printf 'Authorization: Bearer %s\n' "$(cut -f4 ~/.tier/root-credential.tsv)" |
  curl --fail-with-body -H @- -X DELETE "http://127.0.0.1:8080/api/v1/developer_alias/$USER"

Every protected curl command above reads the issued root key from the bootstrap file's last field and sends it through standard input. Root, node and personal keys open the named screens described in the identity access runbook.

Check that your new server is running:

curl -fsS http://127.0.0.1:8080/api/v1/livez

It returns JSON with "status":"alive". You can open http://127.0.0.1:8080, but team reports stay empty and /api/v1/scores returns 404 until a month closes automatically, 24 hours after its end. Use a full UTC calendar month of cost coverage with an effective roster for the first complete branch report. See Publishing the first month. Loading today's roster does not revise older closes.

For an immediate personal number from your own local spend and merged PRs:

tierd score --outcomes --repo ~/src/your-repo --token @$HOME/.tier/github-token

This uses your GitHub token owner's own PRs and needs no team server or seal.

The first administrator (root)

For a shared server, set up root before inviting anyone. Root is the top-level administrator for this server; its issued root key can sign in to the administration pages. Run these commands on the server, as the account that will run tierd. Stop any existing serve first and use its database path.

mkdir -m 700 -p ~/.tier
tierd serve --addr 127.0.0.1:8124 --db "$HOME/.tier/tier.db"

Wait for the server to start, then press Ctrl-C. This creates a new database if needed: root issuance refuses a missing database. Now issue the first key:

(umask 077; tierd tokens issue --db "$HOME/.tier/tier.db" --root \
  --label first-admin > ~/.tier/root-credential.tsv)

The private file contains one tab-separated row; its last field is the shown-once root key. The setup commands read it with cut -f4 ~/.tier/root-credential.tsv. Save that field in your password manager. Keep the file private; later lists cannot recover the key. Restart using the same serve command above. On the server itself, open http://127.0.0.1:8124/signin and paste only the key. From another machine, first set up HTTPS.

Build the organisation from the top down

While signed in as root, use these pages in order:

  1. Open Organization (/org). Under Name your levels, use Org, Division, Team for Levels 1–3 and click Save level names.

  2. Under Add group, create Acme with parent Whole company and level Org. Then create Engineering under Acme at level Division, and Platform under Engineering at level Team. Repeat for the other branches.

  3. Open People (/people). Add person places a person ID, such as maya, in Platform. Use their GitHub login when that is their outcome identity. Give Maya the shown-once claim code through a private channel. They open /claim on this server within 72 hours, enter the code and click Make my key. They save the personal sign-in key shown once in their password manager, then follow Sign in with your personal key to /signin.

  4. To delegate a branch, first add the manager as a person if they are not already present. Use Invite a manager with their person ID and child group. Give that manager the shown-once node key privately. They can manage that branch and invite managers below it; root can manage all branches. The manager key can also read named figures and coaching cards inside its dated branch.

  5. Maya signs in with their personal key, opens My devices (/devices) and issues a device key for their laptop. They save it in a private file, here /secure/maya-upload-token, and run this on the laptop:

    tierd ship --server https://tier.example.com --repo ~/src/your-repo \
      --developer maya --bound-token @/secure/maya-upload-token
    

Replace tier.example.com with the HTTPS address configured below. Add the capture flags from §6 for tools other than Claude Code. Creating people alone does not satisfy the privacy floor: qualifying uploads must arrive before they count. Root and managers can still invent people and upload for them, so the floor does not protect against a malicious administrator.

6. Capturing OpenAI Codex spend

tierd score reads Codex CLI rollouts by default from v0.7.0. Live capture by serve uses the same reader and is off by default:

tierd serve \
  --watch-repo ~/src/your-repo \
  --codex-rollout

Two things to know:

If the server runs centrally and Codex runs on a laptop, the same flag goes on the shipper — it is off by default there too:

https://<your-server> stands for your central server's address, and @/secure/maya-upload-token holds Maya's personal upload token, issued on the server. Shared ingest credentials are retired. Both are explained in the README under What you need before you start.

tierd ship --server https://<your-server> --repo ~/src/your-repo \
  --codex-rollout --developer maya --bound-token @/secure/maya-upload-token

Without that flag, a central deployment records the laptop's Codex outcomes (they arrive by webhook, whichever model did the work) while none of its per-developer cost ever lands. That does not read as "Codex is missing" — it reads as "Codex work was free", which inflates the score of exactly the developers moving onto the cheaper path (#492).

Capturing Opencode

Opencode gets the same treatment and the same reasoning, with one extra precondition. It is off by default:

tierd serve \
  --watch-repo ~/src/your-repo \
  --opencode

--opencode requires v0.5.2 or later; v0.4.1 rejects the flag.

Three things to know:

The shipper mirrors it, off by default there too:

tierd ship --server https://<your-server> --repo ~/src/your-repo \
  --opencode --bound-token @$HOME/.tier/upload-key

⚠️ Do not also route Opencode through TIER's reverse proxy. Its spend would be captured twice and the two rows cannot dedup — this collector keys on Opencode's local message.id, the proxy keys on the upstream response id, and neither side can compute the other's value, so the rows add rather than colliding. serve warns at startup when both are enabled.

⚠️ Pass --repo explicitly on ship. It defaults to ., and a cron or launchd job usually runs from $HOME. If $HOME is not a git checkout the run fails loudly — exit 1, "does not appear to be a git repository". And if $HOME is a checkout, such as a dotfiles repo, every session belonging to another repository falls outside the scope and nothing ships — which as of #549 is also exit 1, with a per-repo summary naming the zero rows:

Per-repo summary:
  "/Users/you": sessions_with_events=0 events_shipped=0
ship: every --repo target kept 0 sessions since 2026-05-05 — this is almost
always a wrong --repo path, not a legitimately idle repo. Pass --allow-empty if
zero is genuinely expected.

That case used to exit 0 silently, so "ship reported success" and "ship recovered nothing" were indistinguishable without diffing the store by hand. If a repo is legitimately idle for the whole window, pass --allow-empty (env TIER_SHIP_ALLOW_EMPTY) to get exit 0 back.

The guard measures the whole run, not just Claude Code: a machine that only produces Codex spend exits 0 normally, because the Codex events count even though they are scanned once across all repos rather than per repo.

Capturing Meta Muse Code

Muse Code is Meta's coding agent, run as the muse command. TIER reads the session log Muse writes for each session, and it is off by default:

tierd serve \
  --watch-repo ~/src/your-repo \
  --muse

--muse requires v0.5.2 or later; v0.4.1 rejects the flag.

Where the input comes from: Muse writes one log per session to ~/.local/share/muse/sessions/YYYY/MM/DD/<session-id>/session.jsonl, with no setup on your part. ls ~/.local/share/muse/sessions shows whether this machine has any. If your Muse data lives elsewhere, set home: in the collectors.muse block of the config file (config.example.yaml shows it) or pass --muse-home to ship; TIER reads the sessions folder inside it.

Five things to know:

To confirm it works, look for these lines in the serve output: Muse collector enabled with the sessions_dir it is reading, then muse scan complete with an events= count (subagent_events= is the part that came from subagents). That line is written only for a scan that had a session log to read; a scan with none writes nothing, so a quiet log on an idle machine is normal. If the sessions folder does not exist, it logs muse sessions root does not exist; nothing to scan instead.

tier_muse_events_total at http://127.0.0.1:8080/metrics counts events handed to the store, repeats included: a session log TIER reads again (a changed one, one still waiting on a run, or every one after a restart) re-sends its calls, and the store keeps one copy. A rising value shows capture is alive; it is not a count of stored rows. Send a credential as Authorization: Bearer <token> using an issued root or metrics key. Read it from its private file to keep it off the command line:

printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/api-token)" |
  curl -sf -H @- http://127.0.0.1:8080/metrics | grep tier_muse_events_total

Use your metrics key's file for scraping. Anonymous protected requests return 401. tierd doctor does not check Muse.

The shipper mirrors it, off by default there too. https://<your-server> and @$HOME/.tier/upload-key mean what the note in §6 says:

tierd ship --server https://<your-server> --repo ~/src/your-repo \
  --muse --bound-token @$HOME/.tier/upload-key

Its summary prints a muse (all repos): events_shipped=<n> line. A run still in progress is not shipped; the next ship picks it up once it finishes.

7. Viewing the dashboard from another machine

The demo is safe to expose directly — it is read-only and every row is synthetic:

tierd demo --addr 0.0.0.0:8124      # then http://<this-host>:8124

HTTPS with Caddy

TIER serves plain HTTP only. Signing in from another machine requires HTTPS. Put a reverse proxy in front: it accepts the browser's encrypted HTTPS connection, presents a certificate proving the server's name, and forwards the request to TIER over HTTP on the same machine. Caddy, nginx or your load balancer can do this. The example below uses Caddy and a public DNS name you control.

  1. Set up the first administrator above. Point your DNS name (here tier.example.com; replace it throughout) at this host. Allow inbound TCP ports 80 and 443 to Caddy; leave TIER's port 8124 bound to loopback. Caddy needs those public ports to obtain its certificate in this example.

  2. Install Caddy on that same host. On Ubuntu 24.04+ or Debian 12+:

    sudo apt update && sudo apt install -y caddy
    

    For other releases and systems, use the Caddy installation guide. Stop serve and restart it trusting only this local proxy:

    tierd serve --addr 127.0.0.1:8124 --db "$HOME/.tier/tier.db" \
      --trusted-proxy-cidr 127.0.0.1/32
    

    Keep any capture flags from your existing server command. The trusted proxy setting lets TIER accept Caddy's HTTPS scheme and client address headers. Do not trust arbitrary client networks.

  3. Save this as /etc/caddy/Caddyfile, replacing the hostname:

    tier.example.com {
        reverse_proxy 127.0.0.1:8124
    }
    

    With Caddy's standard Linux systemd installation, load it with:

    sudo systemctl reload caddy
    
  4. Open https://tier.example.com/signin from the other machine and use the root key. Caddy obtains and renews the certificate and redirects HTTP to HTTPS. See its reverse proxy guide.

Give administrators their scoped node keys and developers their personal keys, through a password manager or another private channel. A shared read-only token still works in the existing dashboard's token box for name-free reports; it cannot administer the organisation. Keep the API token for host operations and scripts, which send it as Authorization: Bearer. The API and read-token files must contain different values. See What you need before you start.

Private certificate authorities: SSL_CERT_FILE

A certificate authority (CA) signs certificates that clients trust. Public CAs normally work without extra setup. If your proxy uses a private company CA, install its CA certificate in each browser machine's trust store. A TIER process making HTTPS requests must trust it too: shippers on laptops, the child sending to a central server, and the company server pulling an import from a source.

Ask your administrator for a PEM CA bundle, then set this in the environment before starting the process that connects:

export SSL_CERT_FILE=/absolute/path/company-ca-bundle.pem
tierd ship --server https://tier.example.com --repo ~/src/your-repo \
  --developer maya --bound-token @/secure/maya-upload-token

For federation or import, set it in the service environment and restart serve. This is Go's certificate trust setting, not a tierd flag; it changes outbound trust and does not enable HTTPS serving. It overrides the default certificate-file location, so use a bundle containing the public roots you still need as well as the private CA. It does not install trust in your browser. Keep certificate verification enabled.

Publishing the first month

A writable tierd serve closes due UTC months at startup and checks again every minute. A month becomes due 24 hours after its end. Reports, CSV, trends, comparisons and month pickers read frozen month_person rows; Team and group views expand to the people allowed by the reader's key. Live branch views remain available before close. Late raw input does not rewrite a closed month. Read-only serving starts no close worker. Source coverage and known capture loss are frozen as evidence and disclosures; they do not require arming or skips. The seal command, preview, status, --seal-from, --report-grace, seal_from and report_grace settings have been removed. Nonempty TIER_SEAL_FROM and TIER_REPORT_GRACE settings fail startup explicitly.

Select a month with GET /api/v1/scores?period=YYYY-MM. Without a month the reader selects the latest close; a missing close returns 404. Named branch reports use the dated roster; name-free comparisons retain k and complement suppression. Root can inspect monthly close status at GET /api/v1/org/report/status.

If you would rather not open a port at all, tunnel over SSH from the machine you are browsing on — nothing is exposed to the network:

ssh -N -L 8080:127.0.0.1:8080 you@the-host   # then http://127.0.0.1:8080 locally

Use 8080:127.0.0.1:8124 instead for the server above. The browser then connects to local loopback, where HTTP sign-in is allowed. This tunnel does not give federation or company import an HTTPS endpoint.

Federation v2 sends closed monthly people, model associations, group paths, conserved amounts and frozen census to the central server. The organization owns both servers; raw captures, coaching evidence, credentials and audit logs remain on the owning server. Set up root on both first. The Caddy recipe above works for federation when its address is configured with --trusted-proxy-cidr; only a trusted proxy can assert HTTPS.

In Settings, the organization chooses Require HTTPS for federation (on by default) or Allow plain HTTP between servers. For HTTP, turn it off on both servers: names, figures and the federation key travel unencrypted. The choice is logged and shown on Settings and Federation. Host pins use --federation-require-https, TIER_FEDERATION_REQUIRE_HTTPS or YAML federation_require_https; pinned choices are read-only on the page.

  1. Sign in as root on each server and open Federation (/federation). Copy each server's own origin_id from its diagram. On the central server, enter the child's installation ID, name and hostname, then choose its parent group and schedule. Click Allow server and issue token. Save the shown-once token privately and copy the issued server_id shown in the list. Root can also read these IDs from GET /api/v1/org/federation.

  2. On the child host, create a private directory owned by the tierd account:

    mkdir -m 700 -p ~/.tier/federation
    

    Stop its serve, then restart with all four host settings. Use the central server's installation ID for CENTRAL_INSTALLATION_ID:

    tierd serve --addr 127.0.0.1:8124 --db "$HOME/.tier/tier.db" \
      --trusted-proxy-cidr 127.0.0.1/32 \
      --federation-central-url https://central.example.com \
      --federation-token-file "$HOME/.tier/federation/token" \
      --federation-server-id CENTRAL_ISSUED_SERVER_ID \
      --federation-receiver-origin-id CENTRAL_INSTALLATION_ID
    
  3. Sign in as the child's root and open Child server settings (/upstream). Paste the token, choose the UTC schedule/hour and click Save. The token file has mode 0600 and the value is never shown again. Test connection checks the saved authentication and installation binding; it sends no report.

  4. The child closes elapsed UTC months after a 24-hour grace, then sends each origin's complete month through paged, resumable snapshots. Central readers can drill into the committed people and models within their authorized branch, subject to the reading server's name visibility setting and k floor. Check Last successful report on the child and Reported through on the central Federation page. Missing months stay missing; older data is not reused.

Each descendant installation and its route must also be explicitly approved through POST /api/v1/org/federation/servers/{server_id}/origins before a middle server can relay it. An origin_not_allowed refusal backs off that origin while other admitted origins continue. Central numbers are reported by their owners; child raw evidence is not rechecked centrally. Token revocation stops future reports and keeps accepted history.

A one-time company import

Use Company import to bring an existing installation's history into the company server once. Federation is the ongoing totals link. Use HTTPS at the source and company, and check private-CA trust before consuming an export key.

  1. On the source, each person signs in with their own personal key, opens Source export (/import-source), chooses Move my named data, enters the destination label and saves if they consent. Without consent, only eligible name-free totals move. Root cannot choose consent for them.
  2. Source root opens the same page and clicks Issue one-time export key. Transfer it privately to company root. It expires after one hour and the preview pull consumes it; another pull needs a new key.
  3. Company root opens Company import (/import), enters the source's HTTPS address, selects the destination group, pastes the export key and clicks Preview import. Resolve every identity clash explicitly and save the mappings; matching IDs never merge automatically. Review the counts, omitted rows and live-upload warning before clicking Import.
  4. Open People on the company server. Issue claim codes for imported people; they claim personal keys and create device keys here as above. Source keys and viewer declarations do not move. Imported people do not count toward the privacy floor until qualifying uploads arrive here.

Arrange for each developer to upload to one server going forward. An import does not erase the source; a later erasure of named data must be requested at both servers. See the import contract for the preview and apply limits.

8. Running on a different port

Every server command takes --addr host:port:

tierd demo  --addr 127.0.0.1:8123
tierd serve --addr 127.0.0.1:9000 --watch-repo ~/src/your-repo

9. What each command actually needs

Command Needs a repo? Needs a token? Produces
tierd demo no no synthetic dashboard
tierd doctor yes (--repo) no a capture health check
tierd score yes (--repo) only with --outcomes cost per issue from Claude Code, Codex CLI, Opencode (Z.ai), Muse Code; opt-in personal TIER
tierd backfill yes (--repo owner/name) GitHub token (how) reconstructed outcomes
tierd serve for live capture (--watch-repo) for non-loopback binds the full TIER score + dashboard

Run any command with -h for its full flag list.