TIER docs

TIER — Quickstart

v0.7.0 identity, coaching and retention: Real-data developer mode uses permitted viewer credentials, a nonempty admin token and a named audited operator. Named coaching-card reads use verified spend and log each view before construction. Shared reads remain name-free; raw-data retention runs at startup and daily. See developer access and upload identity for current commands and restrictions.

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 sealed subtree reports. Named coaching uses the declared-viewer boundary described below; shared/admin uploads remain team-only. 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). Real-data developer mode requires subject-bound viewer credentials, a nonempty admin token and a named audited operator. Each developer ships with their own bound credential. Shared read credentials expose only name-free responses; shared/admin uploads remain team-only. Administrators and host/database owners remain trusted operators, and host access can read the whole database. Team/division modes remain name-free. The coaching card (#1372) ships with raw-data retention (#1371) in v0.7.0; the separate per-issue report remains future work. Card reads use verified spend and log each view before construction. 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.

# 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

# 2. Run the server: it captures new 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.
tierd serve --db ~/.tier/tier.db --aggregation team --watch-repo ~/src/your-repo

--aggregation is required and has no default — serve refuses to start without it, so an existing deployment's privacy posture never changes silently:

3. Load past spend. In a second terminal, while serve keeps running:

tierd ship --server http://127.0.0.1:8080 --repo ~/src/your-repo

This step exists because --watch-repo only picks up new activity: it reads session files written to after serve starts, not the ones already on your disk. backfill loaded up to 90 days of merged work, so without this step the score divides that work by only the spend recorded since step 2, and reads too high: the same work looks cheaper than it was. ship sends the last 90 days of session files still on your disk.

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, stop it with Ctrl-C and start it again with the same flag added. 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 curl --fail-with-body "http://127.0.0.1:8080/api/v1/developer/$USER/export", using the admin token as described below if the server has one. This export works in team/division mode, including before sealing. 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:

curl --fail-with-body -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:

curl --fail-with-body -X DELETE "http://127.0.0.1:8080/api/v1/developer_alias/$USER"

None of these commands needs a token header if the server has no token. If it has one (step 3), send it the way README § The API token shows, reading the token from its file (~/.tier/api-token there) so it never appears on the command line: start the command with printf 'Authorization: Bearer %s\n' "$(cat ~/.tier/api-token)" | and add -H @- to the curl part. The team-server form is in README § Identity mapping.

Then open http://127.0.0.1:8080, or read the same scores as JSON with curl -f http://127.0.0.1:8080/api/v1/scores.

The first administrator (root)

For a shared server, set up root before inviting anyone. Root is the top-level administrator for this server; the API token used by scripts cannot 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
[ -s ~/.tier/api-token ] || (umask 077; openssl rand -hex 32 > ~/.tier/api-token)
[ -s ~/.tier/read-token ] || (umask 077; openssl rand -hex 32 > ~/.tier/read-token)
tierd serve --addr 127.0.0.1:8124 --db "$HOME/.tier/tier.db" \
  --aggregation team --api-token @$HOME/.tier/api-token \
  --read-token @$HOME/.tier/read-token

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. 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. Keep the API token configured on every restart, including loopback: creating the root key permanently arms organisation authentication for this database.

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 to make their own personal sign-in key.

  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. Managing a branch does not grant access to anyone's coaching card.

  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 --aggregation team \
  --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 tokens are legacy credentials for existing shippers. 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 --aggregation team \
  --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 --api-token @$HOME/.tier/ingest-token

⚠️ 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 --aggregation team \
  --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. If the server has a token, send it as Authorization: Bearer <token>; in developer mode the read-only token (--read-token) is enough for /metrics, and in team or division mode use the metrics token (--metrics-token) instead. Read it from its file so it stays off the command line, as in step 4 of §5:

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 token's file instead if you have one (in developer mode, the read token's file also works). With no token, drop the printf … | line and -H @-. tierd doctor does not check Muse.

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

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

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. Stop serve and restart it trusting only this local proxy:

    tierd serve --addr 127.0.0.1:8124 --db "$HOME/.tier/tier.db" \
      --aggregation team --api-token @$HOME/.tier/api-token \
      --read-token @$HOME/.tier/read-token --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

Team and division modes show closed, sealed calendar months only. They do not show live spend. This preserves the privacy floor: overlapping date windows could be subtracted to isolate one person's figures even when each group clears k-anonymity. Team/division mode withholds numbers when fewer than k contributing developers are present (default k = 5, minimum 3). Each person can use tierd score --outcomes for their own local TIER, tierd score for the Claude Code, Codex CLI, Opencode (Z.ai) and Muse Code cost table and issue-attributed spend share, and tierd demo for the dashboard on invented data. Sealing does not remove that floor. From v0.6.1 through v0.6.3, real-data developer mode is refused; from v0.7.0, it requires subject-bound credentials and a named audited operator.

Before the first seal, GET /api/v1/scores returns 404 with this error:

sealing not armed: no month is sealed until the operator arms sealing with seal_from or `tierd seal --arm` (#913)

The dashboard shows YYYY-MM is not published: followed by that reason, with no scores. Opening or refreshing it never creates a seal.

On the server, finish backfilling cost and outcomes and loading the team and alias maps before arming: a sealed month never changes, and the first seal permanently pins the earliest published month. Leave serve running so it has registered its sources. In another terminal, under the same account and with the same environment, preview and then confirm the first seal:

tierd seal requires v0.5.2 or newer, so these commands are an exception to the released-binary check at the top of this page.

tierd seal --arm earliest --dry-run --aggregation team --db "$HOME/.tier/tier.db"
tierd seal --arm earliest --aggregation team --db "$HOME/.tier/tier.db"
tierd seal --status --aggregation team --db "$HOME/.tier/tier.db" \
  --server http://127.0.0.1:8124 --api-token @$HOME/.tier/read-token

The second command prompts for confirmation, seals the first eligible month, and arms future sealing. earliest selects the first full UTC month of cost coverage; use --arm YYYY-MM to start later. Arming is possible only once that full month has closed plus --report-grace (default 336h, 14 days), with configured sources settled through its end. For example, cost data starting 2026-09-03 makes October the first full UTC month: it closes at 2026-11-01 00:00Z and is armable from 2026-11-15 00:00Z with the default grace. With no eligible history yet, the command refuses: backfill the missing history or wait until a full covered month and its grace have elapsed. It also refuses a month whose start predates the retention horizon. If you customized serve, also pass its same --config, --prices, --k-anonymity, --report-grace and database settings to seal.

After success, a read with no month (GET /api/v1/scores) returns the newest sealed month, so the armed month shows immediately. To select the month printed by seal explicitly, use GET /api/v1/scores?period=YYYY-MM. The writable server seals subsequent eligible months at startup and on the next hourly pass (within the hour, once sources are settled); refresh the dashboard to see them. See the sealed-read contract for unavailable months and source holds.

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 lets a child server keep its people and data locally while sending name-free group totals to a central server. Set up root and HTTPS on both first.

  1. On the central server, sign in as root and open Federation (/federation). Under Allow a federated server, enter the child's name and hostname, choose its parent group and report schedule, then click Allow server and issue token. Save the shown-once federation token privately. To get its issued server_id, run on the central host:

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

    Find the child's entry by label and host in the returned JSON. Copy its server_id, not the group ID or credential ID.

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

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

    Stop its serve, then restart with the three host settings below. Replace CENTRAL_ISSUED_SERVER_ID with that ID and the URL with your central origin:

    tierd serve --addr 127.0.0.1:8124 --db "$HOME/.tier/tier.db" \
      --aggregation team --api-token @$HOME/.tier/api-token \
      --read-token @$HOME/.tier/read-token --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
    
  3. Sign in as the child's root, open Child server settings (/upstream), paste the federation token, choose the schedule/hour in UTC and click Save. This writes the token to the host file with mode 0600; it is never shown again. Test connection checks authentication only and sends no totals. Saving the schedule does not send a report either.

  4. Finish loading history and arm sealing on the child using the steps above. The scheduled sender sends eligible closed, sealed months, including withheld months with explicit withholding and zero wire figures. Those zeros represent suppressed figures, not measured zero activity. Unavailable node months, including unsealed months, are omitted. Check Last successful report on the child and Last authenticated report on the central Federation page.

Central totals are reported by the child, not re-checked centrally. Revoking its federation token stops future reports and keeps already 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 --aggregation team --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.