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
- Install
- See it work in one command
- Point it at your own repository
- The free cost view
- The full TIER score
- Capturing OpenAI Codex spend
- Viewing the dashboard from another machine
- Running on a different port
- 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.
--serveris the address the step 2 server listens on:http://127.0.0.1:8080, unless you changed it with--addr(§8).--repois the folder of your local checkout.- Real servers require issued credentials, including on loopback. Anonymous
protected reads and uploads return 401;
/livezstays public. Use each person's own upload or personal key for ingestion. Root keys administer the server and cannot upload. See the identity access runbook. - Running it again is safe: every record carries a key, and the server keeps one copy.
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:
-
Open Organization (
/org). Under Name your levels, useOrg,Division,Teamfor Levels 1–3 and click Save level names. -
Under Add group, create
Acmewith parent Whole company and levelOrg. Then createEngineeringunderAcmeat levelDivision, andPlatformunderEngineeringat levelTeam. Repeat for the other branches. -
Open People (
/people). Add person places a person ID, such asmaya, inPlatform. Use their GitHub login when that is their outcome identity. Give Maya the shown-once claim code through a private channel. They open/claimon 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. -
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.
-
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:
--codex-rolloutrequires--watch-repo. Codex spend is attributed to the watched repositories; with no watched repo there is nothing to attribute it to, andservenow refuses to start rather than silently capturing nothing.tierd scorereads Codex by default, alongside Claude Code, Opencode (Z.ai) and Muse Code.serveandship(below) read Codex's own local rollout logs when enabled. The reverse proxy can parse the OpenAI Responses API that Codex speaks (#459), but only for traffic you deliberately point at it with API-key auth, and that path has not been verified against live traffic — the rollout logs are the supported way to capture Codex.
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-tokenholds 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:
--opencoderequires--watch-repo, for the same reason--codex-rolloutdoes: with no watched repo there is nothing to attribute the spend to, soserverefuses to start rather than capturing nothing quietly.- GLM-5.3 needs no price-table override. The embedded table prices
glm-5.3andglm-5.3-flashper token at Z.ai's list price (#786). A GLM model with no embedded row, such asglm-5.2, prices at the guessedself-hosted-mediumfallback ($0.50/M), andservelogs a WARN naming it the first time it prices one. - Only routes with an audited rate are captured, and the rest are named. Today that means the Z.ai coding plan and nothing else. Ollama's cloud tier is deliberately excluded because it publishes no per-token rate; capturing it would price the spend at that same guessed fallback and inject invented dollars into a cost-per-outcome metric. The exclusion, its reason, and its per-scan row count are logged, so what is left out is visible rather than inferred.
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:
--muserequires--watch-repo, as--codex-rolloutdoes. A Muse session run in a folder outside every watched repository is skipped, not attributed.- TIER decodes counts, never text. From each session log it decodes token counts, model names, the session id (which it stores), each run's recorded branch, the folder Muse ran in (used to pick the repository, then dropped) and the ids linking a subagent to the run that started it. It never decodes your prompts, Muse's replies or tool output (privacy.md).
- A run is recorded after it finishes. Muse writes a run's git branch only
when the run ends, so TIER waits for that before recording the run's calls:
expect a run's cost to appear after the run completes, not while it works. If
a run's session log goes 6 hours untouched with no branch recorded, TIER
records that run as
unattributed:detached-head. That includes a run still waiting on you at a tool-approval prompt after 6 hours, and TIER cannot fix its issue afterwards. - Subagent spend counts toward the run that started it. Calls made by a Muse subagent are recorded under the repository and branch of the run that started it, once that run has finished. Three kinds are still left out, each with a warning in the log: a subagent that Muse's log never links to one run (TIER waits until neither log has changed for 6 hours, then leaves it out rather than guess), a subagent started by another subagent, and a subagent whose parent session recorded no folder. For those, TIER's Muse figure is lower than what Muse actually spent.
- Muse is priced at Meta's published per-token API rates, even if you pay
for Muse by subscription (see
reference-price-table.md).
tierd scorereads Muse by default;serveandshipread it when enabled.
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.
-
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. -
Install Caddy on that same host. On Ubuntu 24.04+ or Debian 12+:
sudo apt update && sudo apt install -y caddyFor other releases and systems, use the Caddy installation guide. Stop
serveand 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/32Keep 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.
-
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 -
Open
https://tier.example.com/signinfrom 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.
Link a second server and send totals
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.
-
Sign in as root on each server and open Federation (
/federation). Copy each server's ownorigin_idfrom 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 issuedserver_idshown in the list. Root can also read these IDs fromGET /api/v1/org/federation. -
On the child host, create a private directory owned by the
tierdaccount:mkdir -m 700 -p ~/.tier/federationStop its
serve, then restart with all four host settings. Use the central server's installation ID forCENTRAL_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 -
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 mode0600and the value is never shown again. Test connection checks the saved authentication and installation binding; it sends no report. -
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.
- 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. - 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.
- 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. - 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.