Privacy
The production v2 upstream view (#1500) exposes only pinned destination identities, schedule and delivery state, a nameless pending snapshot/page summary and a token-stored indicator. It returns no token, token-file path or pending body. Its status probe changes no delivery state. Central erasure requires JSON media and an exact empty object through the same normalized administration boundary; malformed requests create no restore block. Revocation removes incomplete page bodies and internal staging metadata in the same transaction as its audit. Retained nameless receipts consume the aborted sequence and let a reissued route rebuild without claiming partial delivery. The complete descendant route stops at a revoked middle admission; accepted history and origin-qualified restore blocks survive reissue.
Frozen monthly rows (#1497)
The verifier checks frozen monthly contributions and their authorized
projection. Changing live loss settings or scoring policy after close does
not revise that proof. Retired tiersealedmanifest1 receipts return
COULD NOT CHECK; no old seal body is treated as evidence for named monthly rows.
Archived live raw-input auditing keeps its separate digest contract. Scores,
comparison and manifest HTTP routes no longer read sealed report bodies.
Synthetic period selectors use frozen monthly rows and their shared k floor;
legacy sealer state cannot change archived raw-window reads.
Prefold person-key sets and seal-body assembly are removed. Monthly cache levers
use frozen inputs and the same full-tree decision as comparisons; counted people
with spend and merged work must independently meet k on both cache-input sides.
Archived raw verification retains its separate per-measure census.
A writable server freezes each elapsed UTC month after 24 hours of grace, in one transaction. The monthly person/model table stores dated group paths, spend, points, fractional outcomes and additive lever inputs. A separate monthly partition preserves signed paid amounts without assigning bills to models; erasure pools those amounts without identifying allocation evidence. A companion stores captured event/outcome inputs and price provenance for reproducibility; it contains no prompts, code or credential-derived material. Late capture remains in raw storage and cannot revise a closed report. Close metadata discloses source coverage/loss and retention; a freeze does not certify complete capture. New closes also freeze the dated import/restart loss flag. Earlier metadata that did not record it is identified as such, without inferring an all-clear from current settings.
Monthly closes also protect the raw attribution inputs: the closed UTC month and its 14-day half-open lookback block insert/merge-time fills, read-time recovery, token promotion and historical price repair/repricing. Imported spend uses the same population writer and guard. CI provenance repair leaves closed outcomes and quality unchanged. Existing raw-verification receipt bands remain protected until that legacy adapter is removed. These guards do not prevent late raw capture; closed reports keep their frozen evidence.
Root report status reads monthly close metadata without people or digests, checks root authority in the snapshot and requires audit admission before responding. Seven-day reporting summaries remain explicitly labelled live receipt/watch diagnostics; monthly rows do not establish today's freshness. They use the shared k/complement decision without tree dominance.
Close checks each identifier's dated placement before joining aliases to one person. Captured spend without a named subject uses its producer identifier and does not become census evidence. Group totals sum the frozen dated rows, including Unplaced. Named last-upload status reads receipt history independently of the figures window, including when the figures are frozen.
Monthly retention runs in every mode using the configured window (default 90 days, 60–365). It pools whole UTC months ending before that horizon; the partial month at the horizon remains named until its whole month expires. Settings preview counts named monthly rows and rolls the simulated pooling back.
Erasure and retention remove named monthly rows and their captured evidence, adding their amounts to one nameless remainder per origin/month/group path/model. A later erasure merges into that same remainder. People plus remainders still sum to group totals. Anonymous group census counts frozen at close preserve the historical k decision after names disappear; they contain no erased-person keys. Mapped aliases contribute no additional census or peer seats; their captured spend and points remain in the named figures. Nameless before/after digests and conserved group totals record each erasure or retention revision; old named receipts are not rewritten or reported as reproduced. These are consistency checks, not an attestation against an owner who can rewrite the database. Frozen rows and evidence are included in subject exports. Ordinary database compaction/backup limits still apply. Whole-month bounds apply only to remainders visible in the selected projection.
Name-free monthly comparison decisions depend on group totals and the frozen anonymous census. Person names and row order cannot change their sums, census assignment or complementary suppression. The comparison endpoint projects the same full-tree decision for every authorized key, refuses narrowing filters, and omits hidden row identities, raw digests, row counts and erasure revisions. The exact row-space test oracle checks that no linear combination of published rows recovers a sub-k group. This is a k/complementary-suppression guarantee; it does not promise differential privacy across months or conceal changes in published group totals. Coaching retains its separate 1–50% contribution guard; its frozen outcome pool is ordered by value before bootstrap so renaming peers cannot change a typical range.
All the data TIER collects stays within your organization.
Partial closed-month reads exclude remainders outside the authorized person or dated branch before checking bounds. A visible remainder requires whole UTC month bounds and otherwise returns HTTP 400. Personal keys cannot read either monthly cross-branch comparison form; root and manager node keys retain those reports.
Admin sign-in sessions (#1417)
Admin-web sessions live only in process memory. Sign-in creates no database
row, file or durable audit entry. A random 256-bit session id is sent only in an
HttpOnly, SameSite=Strict cookie; it is never logged or written to disk.
Cookies use Secure and the __Host- prefix whenever the effective origin is
HTTPS; on permitted plain-HTTP loopback they use neither Secure nor __Host-.
Memory holds a hashed session id, the credential verifier, a separate CSRF token,
the serving origin and creation/activity times. It never retains the bearer
secret. Session data disappears on sign-out, expiry or restart; expired entries
are reclaimed at sign-in. Limits are 60 minutes idle, 12 hours absolute,
16 sessions per credential (its oldest is evicted) and 4096 simultaneous
sessions per process. Each authenticated request checks the
current credential and holder, so revocation or a recorded departure ends the
session on its next request. Read-only servers support sessions without database
writes.
GET /api/v1/me exposes only the credential id, kind, capability, holder,
node id/name/path/level name and available page names. It exposes no key secret,
verifier, prefix or length. Every admin-web response is no-store. The full
admin token cannot sign in; shared read-token dashboard behavior stays unchanged.
See identity access
for Origin, CSRF, cookie and HTTPS requirements.
The separate dashboard shell (/signin, #1437, plan P20) sends a sign-in key
only in the POST's Authorization header and clears the password field as soon
as the request starts. Its JavaScript never reads or writes cookies, localStorage
or IndexedDB, and never places keys in URLs or history. Admin keys and CSRF tokens
stay out of browser storage in this shell; the separate legacy dashboard keeps
its pasted API token in sessionStorage under tier_token (code:
internal/dashboard/assets/dashboard.js, load, fetchJSON).
On a 401 identity response, the shell saves the
requested path and query (without a fragment) in the tab's sessionStorage under
tier_admin_return_to. The client accepts at most 2048 characters, starting with
one /, with no backslashes or control characters. Sign-in sends the path for
server validation; a successful identity reload clears it before returning.
Sign-out clears it even if the request fails. This return path is the shell's
only use of sessionStorage; import state stays out of it. See
Identity and access for retry and fallback behavior. It learns
identity and navigation only through /api/v1/me; the acting banner displays
the returned holder, key kind and node path using text nodes. Identity and
navigation clear when leaving the page. Pages awaiting their APIs show only
their title, without sample people, keys or reports. Every page, including an
auxiliary page, is gated by the identity response's page allowlist. The shell
pages and /admin.js are absent (404) in synthetic demo and unsealed recompute
modes. A failed identity read opens sign-in only on 401; other failures keep
the form hidden and offer a session-check retry without sending a bearer key.
Sign-out success or 401/403 clears local identity and navigation. Other
sign-out failures retain the shell and its in-memory CSRF token for retry and
show that sign-out did not complete.
The existing server-held session CSRF token is returned in an X-CSRF-Token
header only on a successful cookie-authenticated /api/v1/me response. The
shell holds it in page memory for sign-out; it is absent from bearer identity
responses and refusals. All dashboard HTML and scripts, including the legacy
dashboard, serve CSP, nosniff and no-store. The legacy read-token form and its
storage behavior are unchanged.
The legacy DSAR export (GET /api/v1/developer/{id}/export) carries the opaque
node_id on its org_hierarchy and hierarchy_membership rows. It also
discloses the subject's org, division and team names in org_hierarchy, which
the tree keeps current on every rename or move, and the node the person was
first placed in as org_people.created_node_id.
Who counts toward k (D4)
A person counts once in a reporting window only with roster membership, no bot
classification, positive eligible captured cost and a qualifying merged outcome on the dated path, and frozen named evidence from this server: bound_upload from
their own personal/device credential, server_watch (--watch-as), or
vendor_attributed carrying a valid root-authored mapping ID. Roster-login
matches and legacy aliases alone never count. Census and coaching use the
same evidence predicate, including the own-credential check. Many devices still
count as one named person. Later alias edits cannot
lend somebody else's evidence to a person.
Vendor evidence counts only when the mapping names the event's frozen subject, the event timestamp is at or after its effective date, the mapping is not voided, and the event precedes any End. Root creates mappings prospectively: the first eligible date is the next UTC midnight. Earlier events and old alias-stamped vendor rows never gain counting eligibility, even on replay. End keeps past evidence, including lagged events received after End with timestamps inside the mapping interval. Invalid vendor identities leave spend stored unbound. Void withdraws past counting and cannot be undone. Existing monthly closes retain their frozen census after either action. Census and coaching use this same predicate. Both pollers now pass transient identity for root-mapped usage. Reconciliation keeps ended partitions inside their interval; voided partitions target zero on replay within the lookback and spend returns to the legacy path. Erased mapping partitions leave their day incomplete without deltas or settlement, while later days still ingest.
Vendor IDs live in vendor_mappings and nullable named org audit detail, never
on token events. Erasure hard-deletes the person's mappings and ended or voided
mappings with the same vendor and vendor ID, including rows assigned to other
people. Another person's active mapping survives.
It scrubs named audit detail matching both vendor and vendor ID to NULL.
Mappings follow roster retention: aged leavers lose their mappings, and mappings
never keep a person live by themselves. DSAR exports include every mapping with
the subject's vendor and vendor ID, including rows assigned to other people,
with vendor, vendor user ID, developer, effective/creation dates, creator, End
and Void dates. Raw vendor identity is transient during ingestion; only the
opaque mapping ID is stored on an event.
Exports omit a NULL vendor_mapping_id to preserve pre-mapping export shapes.
Created people who never upload, imported people without local bound evidence,
federated people, shared/legacy uploads, manual /costs rows, and outcomes alone
count zero. Their eligible amounts can remain in totals; they cannot fill k.
This is a breaking change for shared-token teams. Issue claim codes so people
can claim personal keys and create device keys. Startup and tierd doctor --db
explain when missing bound evidence withholds every group. Monthly closes store the counted-person census with their frozen evidence. Schema 30 drops legacy seal reports and their fold/census versions; they are not retained reports and cannot be read or verified.
Credential-free bound_upload evidence from dev builds between 2026-10-24
02:42Z and 22:16Z counts zero; no release writes that state.
For a withheld node reporting summary, root sees the cause; every other principal sees “Withheld — ask your admin”, with counts omitted. A managing node can invent people and upload through their keys; k does not protect against that node.
Upload freshness
Authorized people and key inventories show dates and Fresh / Stale / Silent /
Never chips. Fresh means less than 24 hours, Stale means 24 hours through exactly
7 days, Silent means more than 7 days, and Never means no bound receipt, including
legacy rows with no credential ID. Server receipt time determines freshness.
Personal keys see exact times on /api/v1/me and /api/v1/me/devices. Ancestors
see UTC dates only. People, device and self views preserve Silent and the last upload date or time
after retention using persisted upload-credential receipt metadata older than
the retention watermark. Shared upload and read tokens receive no freshness fields.
The subtree reporting summary counts distinct active people reporting within the
inclusive last 7 days; groups without k qualifying non-bot people in that
window omit counts. Upload evidence uses one precise server receipt-time
comparison for both eligibility and reporting, including fractional seconds.
The existing tree complementary suppression gates the full forest before any
subtree is selected, so parent and child counts cannot expose a sub-k sibling.
All reporting-summary keys retain the shared floor of at least 3. All reads use org authorization.
Coach the practice, not the person. Grade the work, not the worker.
TIER helps engineers choose the right model, give it the right context, and spend where it pays. On real servers, personal keys can read their own figures and cards. Root and node keys can read named figures in their dated branch when the organization's names setting permits it. Cards clip events and outcomes to that dated branch and log each real open before construction. Personal keys read only their holder. My card shows the actual card and the person's own view log. An empty card helps a new install get started. "Your organization chooses who can expand groups to names. You can always see your own figures." TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual. Anyone with host access can read the database; the API enforces these key scopes.
TIER is built so that adopting it does not mean handing over your source code or your prompts. This document states exactly what TIER reads, what it stores, and what it never touches — each claim grounded in the code so a security reviewer can audit it.
JSONL token events store a branch only as nullable branch_hash, a pseudonymised SHA-256 of
canonical repo + "\x00" + full branch name with refs/heads/ removed. Raw branch names are
never stored in token_events. Local watcher checkpoints keep the raw branch name in the
plaintext JSON fields GitBranch and LastRealBranch. Default/unknown branches and unknown
repositories produce NULL hashes. This is not
anonymisation: guessable branch names can be reversed by hashing candidates with the known
repo. Merged-PR outcomes store the same pseudonymised hash as head_branch_hash, never
raw head.ref, plus nullable PR created_at and merged_at. Webhooks and backfill use
head refs when available; API outcomes lack head refs and leave the hash NULL. Main,
master, HEAD and a payload-supplied repository default branch have NULL hashes.
Recognised revert exclusions retain the matching raw head ref or quoted title as
audit evidence in outcome_exclusions, under the PR author's access and erasure scope.
Branch credit fires only for still-unattributed events in the same repo and head-branch
hash inside [max(previous merge on that repo and head branch, PR opened − 7 days), merge].
It never changes existing attribution or closed monthly contributions; overlapping recorded claims are
skipped. Reusing a branch bounds the next PR's credit at the previous merge; unrecorded
merges cannot be considered. Missing head/open/merge evidence disables branch credit.
The hash does not conceal guessable names. Existing webhook audit payloads retain their
original body under the separate audit retention policy below.
The core guarantee
TIER never stores prompt or completion content, and never sends it anywhere you have not already sent it. Every capture path parses with an allowlist. The Claude Code JSONL parser deserializes only
type,timestamp,gitBranch,cwd,sessionId, andmessage.{id,model,role,usage.*}— token counts, not text; the reverse proxy passes your prompts on to the provider you chose and reads each response as it passes back to extractid/model/usage, which is all it keeps; the Codex rollout-log parser reads six fields, none of which is content; the Opencode reader touches one table (message) and six fields inside its JSON blob, never the tables holding prompts, tool output, or credentials; and the Muse Code reader decodes ids, timestamps, token counts, model names, the workspace root and branch records, never prompt, reply or tool text. One opt-in switch widens the Claude Code read: with worktree attribution on (--worktree-attribution, off by default), the parser also reads the file or folder paths in tool calls and uses the one that seven tools name, never the file's contents, the tool's other arguments, or a command line. See Worktree attribution.
Proxy routes are mounted by default on a writable server. They forward traffic only when a client points at them; --read-only or an empty provider target removes the corresponding route.
Why the guarantee holds
TIER parses inputs with allowlist structs. Only the fields named in the struct are deserialized; every other JSON field — including any prompt or completion text — is silently dropped by the JSON decoder and never enters memory as a named value, let alone the database.
-
Read-time recovery can attribute one person’s spend to the issue of a merged PR (theirs or a colleague’s) on the same branch, without storing anything.
-
JSONL capture (
internal/collector/jsonl.go, thejsonlEntrystruct): readstype,timestamp,gitBranch,cwd,sessionId, and undermessage:id,model,role, and theusagetoken counters (input_tokens,output_tokens,cache_creation_input_tokens,cache_read_input_tokens, and the 5m/1h cache-creation split). There is no content or text field in the struct. With worktree attribution on, a second, separate allowlist struct reads tool-call paths; it is described in Worktree attribution. -
Codex rollout-log capture (
internal/collector/codexrollout/parse.go, therolloutLinestruct, #464): reads exactly six fields — the linetypeandtimestamp, and underpayload:id(the session UUID),cwd,git.branch,model, plus thetoken_countevent'sinfo.total_token_usagecounters. Codex rollout logs contain a great deal more than that — the agent's full system prompt (base_instructions), your prompts, the model's messages and reasoning traces, tool output, and patch bodies — and none of it is deserialized: those fields have no counterpart in the struct, so the JSON decoder discards them. Thecwdis read to decide which repository a session belongs to (a session outside every watched repo is dropped) and is not stored by this path — and, since it is never stored, it is never forwarded either, including bytierd ship --codex-rollout(#492). What that flag sends is the same usage-event shape as every other collector, listed under "Where it goes" below; enabling Codex capture adds no new category of data to what leaves the machine.tierd scorereads this by default on the machine it runs on; nothing is stored or sent. -
Opencode capture (
internal/collector/opencode, opt-in with--opencode, #719): Opencode keeps its sessions in a SQLite database rather than JSONL files, and TIER opens it read-only (mode=ro; neverimmutable=1, which would ignore Opencode's live write-ahead log). It reads one table,message, and from each row's JSON blob only therole,modelID,providerID, thetokenscounts,time.completed, and thepath.cwdused to decide which repository the message belongs to. That database also contains OAuth access and refresh tokens (account), API credentials (credential), and prompt and tool-output text (part) — none of which TIER reads, stores, or forwards. Thecwdis used for the repo decision and is not stored, exactly as in the two paths above, sotierd ship --opencodeadds no new category of data to what leaves the machine either.tierd scorereads this by default on the machine it runs on; nothing is stored or sent. -
Muse Code capture (
internal/collector/muse/parse.go, opt-in with--muse, #895): Muse Code'ssession.jsonllogs hold your prompts, the model's replies, tool arguments and tool output. The reader decodes each line in two stages: first only the fields every record carries (the recordid,recorded_at, the session idstream.id, the run id, and the record and event kinds), and then, only for the five record kinds it uses, a second narrow struct for that kind: the model name and token counts of a billed call (model_completedorautomated_review_completed, plus the review'scompleted_at_ms), theworkspace_rootof ametadatarecord, the run id,workspace_root, VCS and branch reference of aworkspace_branchrecord, and the two ids of amemory_reminder_child_session_linkedrecord (parent_run_idandchild_session_id, which tie a subagent session to the run that started it, #901). No prompt, reply or tool text is ever decoded, andTestDecoderAllowlistfails if a field is added to any of those structs. It opens only files namedsession.jsonl, not the other files Muse keeps beside them. Theworkspace_rootdecides which repository a session belongs to and is not stored; the branch becomes an issue id, and the branch name itself is not stored. The Muse session id is stored (see "Session id" below). The collector keeps its scan position in memory and writes no checkpoint row, sotierd ship --museadds no new category of data to what leaves the machine.tierd scorereads this by default on the machine it runs on; nothing is stored or sent. -
Copilot billing (
collectors.copilot_billing, introduced in v0.6.2, supported): reads GitHub's organisation AI-credit billing reports and current billing seats. It stores the mapped developer identity (the user login when that is the canonical developer id), model, product, day and gross amount, plus an unattributed organisation remainder. Product is preserved in the reconciliation key; queried GitHub logins are not stored separately from the mapped developer. Root mappings use numeric seat IDs to follow renames; only mapped identity counts toward privacy k. Numeric vendor IDs and fetched logins are transient, never event columns, delta-key inputs, log values, error details or metric labels. Events retain only the opaque mapping ID and frozen named subject. It stores no token counts, repository or issue linkage from this API. -
Cursor Admin (
collectors.cursor_admin, introduced v0.6.2, supported for team costs): requires a team plan with Admin API access; reads team usage events and stores the mapped developer, model, day, cost and available token counts. Normalized email joins root mappings for counted identity and still joins aliases for spend. Email is transient insert input, never stored on events, in delta keys, logs, errors or metric labels; it persists only in the root-authored mapping and its existing audit/erasure/export surfaces. Events retain only the opaque mapping ID and frozen named subject. The literal"null"and service/cloud/automation accounts never carry mapping evidence. A colliding alias or otherwise unmapped user stays unattributed unless an effective root mapping supplies identity. Conversation id is neither decoded nor stored. BYO-key events labelledUser API Keyare skipped, including Cursor's fee on those requests. -
Reverse-proxy capture (
internal/proxy/proxy.go): the provider response parsers read only the messageid, themodel, and theusagetoken counts from the response body. The client gets the same response content the provider sent; TIER extracts usage and forwards the rest untouched. (Not always the same bytes: the proxy drops the client'sAccept-Encoding, so a response the client asked to have compressed arrives uncompressed.)
Because the parsers are allowlists, adding no new fields is the default — capturing content would require deliberately adding a content field, which does not exist.
Captured-dollar diagnostics
TIER denies GET /api/v1/diagnostics/captured-dollars on real servers. Doctor omits this check on HTTP 403 and instead reads frozen monthly close disclosures through the root-authorized diagnostic report manifest. The synthetic/offline diagnostic reads stored checkpoint cwd and session evidence plus read-only current git worktree metadata, without reopening transcripts. It emits counts and shares of captured dollars, never stored paths or session ids. Current worktrees cannot prove historical work locations. See the diagnostic limitations.
Worktree attribution (off by default)
Why it exists. Claude Code writes, on every line of a session file, the
folder the session started in (cwd) and that folder's git branch
(gitBranch). Many developers, and most coding agents, start Claude Code in the
main checkout of a repository and then do the work in a git worktree: a
second checkout of the same repository, on its own branch, in its own folder
(made with git worktree add). The session file still says "main", so TIER
books that spend to unattributed:main instead of to the issue the work was
for. Worktree attribution (#823) finds the worktree the session actually worked
in, from the paths its tool calls named. That needs a wider read of the session
file than the token parser makes, so it is off unless you turn it on. It is
an opt-in preview: its measured accuracy, and why a wrong attribution is
permanent, are in
how-it-works.md.
How to turn it on, and the rules it applies, are in
how-it-works.md. This
section states what it reads and what it keeps.
Parents no longer carry (#1016); only message groups labelled isSidechain:true can use carry. Worktree attribution stays opt-in and off by default. Activation requires recorded gates, the operator's security rulings and an explicit operator decision. No new accuracy figures have been measured.
What it reads from the session file. A second allowlist struct
(toolPathLine in internal/collector/toolpath.go), separate from the token
parser's, decodes from each assistant line only:
- each content block's
type, and its toolname; - from each block's tool input, whatever the tool (MCP tools included), the
string values of three keys,
file_path,notebook_pathandpath, spelled exactly so. The decoder copies those three values into memory and skips every other member of the input unread.
It then keeps one path per tool call, for seven tools only: file_path for
Read, Edit, MultiEdit and Write; notebook_path for NotebookEdit;
path for Glob and Grep. Every other decoded value, including a path
that an MCP tool or Bash carries, is discarded there, before any disk read,
and never leaves memory. What the decoder skips is never copied: the text
Edit removes and inserts, the content Write writes, the pattern Grep
searches for, and Bash's command. A relative path, a network path
(starting // or \\), a path over 4,096 bytes and a path containing a NUL byte
are dropped; at most 16 distinct paths are kept from one line
(maxToolPathsPerLine), from at most 64 content blocks (maxToolBlocksPerLine).
It also reads two markers each line already carries, isSidechain and
agentId, which tell a subagent's lines apart from its parent's
(messageLineHead in internal/collector/messagemerge.go). Only isSidechain:true permits carry; an absent, false or wrongly typed label does not.
Which session files. tierd ship and tierd score decode the tool paths
of every Claude Code session file whose session ended on or after --since,
and look up the git files below for every path they keep, before they drop
the sessions whose cwd is outside the repositories you named with --repo
(parseSessionFileAttributed in internal/collector/jsonl.go). So the .git
pointer files and worktree admin folders of other projects on the machine are
read too, to recognise them as another repository's; nothing from a dropped
session is stored. The live watcher also decodes the tool paths of every
session file it reads, but looks up git files only for sessions whose cwd is
in a watched repository.
Command lines are not read. Reading Bash commands (for cd <path> or
git -C <path>) was considered and is not built. The operator ruled
(via #826, recorded on #823) that it comes later, as its own change, behind its
own switch, off by default.
What it reads from disk. For each path it keeps, and for the session's own
cwd (rule 1 in how-it-works.md), it reads git's own bookkeeping files, never
the file the path names, and it never runs git to do it (worktreeIndex in
internal/collector/worktreeindex.go, branchAt in
internal/collector/worktreereflog.go). TIER still runs git log on the
repositories you configured, as it does with the switch off.
- the
.gitentry found by walking up from the path. In a linked worktree this is a small file pointing at the worktree's admin folder,<main checkout>/.git/worktrees/<name>/; - in that admin folder,
commondirandgitdir, which must point back at the same worktree (a moved, pruned or forged worktree fails this check and is not used), andHEAD; - the worktree's reflog,
logs/HEADin the same folder, at most 1 MiB. It lists every checkout with a time (checkout: moving from A to B), which is how TIER learns which branch the worktree had checked out when each message was written. The whole file is read into memory; each line also carries the committer's name and email and, for a commit, its subject line, and none of them is kept; - owners, permissions and modification times of those files. What is checked
differs by file. The
.gitpointer file and the admin folder must be owned by the OS user runningtierd.HEAD, thelogsfolder andlogs/HEADmust be owned by that user and not writable by everyone.commondirandgitdirget neither check of their own; they are trusted only because they sit in the checked admin folder and must point back at the same worktree. A file that fails its check is not trusted, and the path falls back to today's rule.
Paths under /tmp and ~/.claude are ignored.
What it stores.
-
For every Claude Code event it records, one fixed label naming the rule that chose the issue:
branch,worktree-cwd,worktree-toolpath,carryorpr-number-at-insert(#1074: server-assigned, never client-sent) (token_events.attribution_rule, see What IS stored). It is never path text: TIER refuses or drops any other value. -
Spend in a worktree of a repository you did not configure (not one of the command's
--repoor watched repositories) goes to the fixed issue labelunattributed:foreign-repo, with the repository stored asunqualified. No event stores that repository's name or path, andtierd shipnever sends them. The watcher's local checkpoint is the exception: see item (c) below and Watcher tail-state under What IS stored. Items (a) and (b) below are the known cases where spend tied to another repository is booked to your configured repository instead. -
Known gaps, with this switch on or off (tier#1018; operator ruling 2026-10-18: disclosed now, fixed after launch). Items (a) and (b) book spend to the wrong issue in a repository you configured. A booking is permanent: TIER keeps the first one it writes and does not rewrite it.
- (a) When an isolated-agent message inherits the most recent human-named
branch seen in its session file, that branch can come from another
repository. The message can then be booked to that branch's
issue-Nin your configured repository: with rulebranchwhen this switch is on, and with no rule (shown aslegacy) when it is off. - (b) A session that starts in a configured repository and later moves into
another repository's main checkout (not a linked worktree) records that
repository's branch on its later messages, and they can be booked to that
branch's
issue-Nin the configured repository. - (c) The watcher's checkpoint keeps the raw branch names and the absolute
working-directory path of each session file it tails, including session
files in repositories you did not configure. So another repository's path
and branch names can sit in that local row. They stay in the local database
and are never shipped. A data-subject export
(
GET /api/v1/developer/{id}/export) returns the rows it can link to that person through their events, so in case (a) it can return another repository's branch name; rows for sessions with no events stay local. For sessions in configured repositories this is by design, because a restart needs them to resume. The rows for other repositories are to be cut down to the session id after launch.
For (a) and (b), the event's
branch_hashis a hash of your configured repository plus the other repository's branch name. It is not the name, but for a common branch name the hash can be guessed.The #823 gate run (one machine, about seven days) recorded that "the latch passed only
main(0 of 229 cases)" and found no row with another repository's working directory. That is one machine, not a guarantee. - (a) When an isolated-agent message inherits the most recent human-named
branch seen in its session file, that branch can come from another
repository. The message can then be booked to that branch's
-
In live watch mode only, the watcher's checkpoint row for a session file also holds the absolute path of the worktree it is carrying from one message to the next, so a restart resumes correctly (see Watcher tail-state under What IS stored). That checkpoint stays in the local database and is never sent.
What it never stores or sends. No tool-call path is written to an event or
sent by tierd ship, and the code that reads and classifies the paths logs
nothing. The dry-run audit
(tierd score --worktree-attribution) prints separate tool-path and carry counts and USD costs, repository names, issue
labels, session ids and times, never a path or any message content, and stores
nothing. With the switch off, none of this is read and events carry no rule.
Messages held back until they finish (live watch mode only). One reply can be
written as several lines of the session file, spread over minutes. With worktree
attribution on, the live watcher (tierd serve --watch-repo) does not store a
message until it has all of its lines, so each message is judged once, from
every path it named. A message still open at the end of the file is held back:
no event is stored for it meanwhile, though the file's checkpoint row (its
path, and the session id) is saved. When the file has had no new line for
600 seconds (idleRelease in internal/collector/watcherattr.go), the next
read releases it and stores it. The longest pause between two lines of one
message was 417 seconds when this was measured (2026-09-30, over 156,834
multi-line messages in 8,532 session files on one machine), so the wait is
longer than any measured pause. Idleness is judged by the file's modification
time, and after a restart the watcher reads at once any file whose checkpoint
stops short of its end. A known gap: if a line of a message arrives after
that message was released, it is read as a fragment and stored again under the
same message key. The store keeps the first issue, rule and price version. A coherent larger observation can promote token counts and recompute token-derived cost atomically at the original rates. Caller-authoritative cost, unavailable original rates, frozen input bands and incompatible observations prevent that promotion. Historical count/cost mismatches need explicit tierd doctor cost-consistency repair. A message held back when an
erasure runs can be stored after it: see the erasure limits under
Data-subject rights.
tierd ship reads each file whole in one pass and holds nothing back.
Shipper credential and identity limits (#1144)
Use a per-developer upload credential with ship --developer maya --bound-token @/secure/maya-upload-token. The server stamps its subject and refuses a batch
that names another developer. Shared ingest credentials are retired. Historical
shared uploads remain in team totals but never count a person toward k or named
coaching evidence. Root cannot upload through /events; only personal and upload keys can. Root can post a named manual cost through /costs, which supplies no counted identity.
A later cross-developer collision is rejected and reported as
rejected_cross_developer in the response, with the original row unchanged.
The server logs a WARN and the shipper prints the rejected count, without
failing the ship or retrying. Same-developer replays still deduplicate, with a
zero rejection count. Global idempotency stays unchanged; the guard cannot
reverse a pre-claim that was inserted first.
What IS stored
TIER stores the following in a single local SQLite file. Configured network paths can transmit data; see Where it goes.
- Developer identifier — an OS username (JSONL) or GitHub login (PR outcomes), and any alias→canonical mapping you configure.
- Issue id — derived from the branch name or PR/commit body (e.g.
issue-42,TIER-99), or a fixedunattributed:…label naming why none was found. With worktree attribution on, spend in a worktree of a repository you did not configure gets the fixed labelunattributed:foreign-repoand the repositoryunqualified, so that repository is not named. - Repository (
token_events.repo,outcomes.repo, #231) — the canonicalowner/reposlug a cost was spent in or an outcome was earned in, taken from a per-reporepo:override, the repository'sremote.origin.url, a GitHub webhook'srepository.full_name, or the optionalrepofield ofPOST /api/v1/eventsorPOST /api/v1/outcomes. It is the reserved valueunqualifiedwhen the producer could not know the repository. A slug can identify a person: a personal repository such asalice/side-projectnames its owner. - Model names — e.g.
claude-opus-4-8. - Token counts — input, output, and the cache read/write counters.
- Cost — list-price cost in integer micro-dollars, computed from the token counts via a versioned reference price table.
- Pricing provenance (
token_events.price_version, #233;token_events.hostandtoken_events.billing_mode, #300) — the version of the price table that computed the row's cost (0for a row no version was stamped on); the serving host the cost was priced against (for example the reverse proxy's upstream host name or an Opencode provider id), orunknownwhen the producer does not know it; and how that host bills:per_token,subscriptionorself_hosted_amortized. A caller ofPOST /api/v1/eventsmay send its ownhost(up to 256 bytes), and TIER stores that string trimmed and lowercased but otherwise as sent, so it holds whatever the caller put in it — and a self-hosted host name such asalice-laptop.localcan name a person. - Session id (
token_events.session_id, #238) — the opaque agent session UUID an event belongs to (Claude Code'ssessionId, the Codex rollout log'ssession_meta.id, thesession_idcolumn of Opencode'smessagetable, or the Muse Code log'sstream.id), so context-bloat and rework-loop token-waste can be diagnosed at session grain. It is a random identifier only; it carries no prompt content, no completion text, and no file contents, and is NULL for rows a session-blind producer (the reverse proxy or an Admin-API poller) captured. - Idempotency key (
token_events.idempotency_key, #21) — the dedup key that makes a replayed event update one row instead of adding a second. TIER's own collectors and the reverse proxy store a SHA-256 hex digest. A caller ofPOST /api/v1/costsorPOST /api/v1/eventschooses its own key, and TIER stores that string as sent, so it holds whatever the caller put in it — which can include personal data such as an email address. It is NULL for rows that were stored without a key. - Billing declaration (
token_events.billed_to, #854) —otherwhen aPOST /api/v1/costscaller declared the row's spend is not billed to an org whose usage poller the server runs; NULL on every other row. - Attribution rule (
token_events.attribution_rule, #823) — which rule assigned the row's issue id: one of the fixed labelsbranch,worktree-cwd,worktree-toolpath,carry,branch-at-mergeorpr-number-at-insert(#1074: server-assigned, never client-sent), never path text. It is NULL on older rows and on rows no rule was recorded for, is written only when the row is first inserted, is included in the data-subject export, and is erased with the row. - Issue extraction rule (
outcomes.issue_rule,token_events.issue_rule, and the existingoutcome_exclusions.issue_rulesnapshot column, schema 12) — a fixed closed-set label, never branch, path or message text. Outcomes retain server-derived webhook/backfill evidence; events retain client assertions. Existing rows and unknown/absent event rules are NULL, rendered aslegacyin event/outcome exports; the data-subject access export preserves JSONnull. The field is included last and erased with the existing outcome/event row. The identifier-freeissue_rule_cutoversingleton records the UTC migration time and maximum outcome/event IDs once and survives erasure; these boundaries distinguish the pre-upgrade band from later NULL rows (cutover details). - PR metadata — PR number, author login, size-derived weight, quality, and the merge commit SHA.
- Watcher tail-state (live watch mode only —
--watch-repoor thewatch.reposconfig key) — when live ingestion is enabled, TIER persists one operational checkpoint row per JSONL file it tails (watcher_checkpoint), including session files whose repository you did not configure, so a restart resumes tailing from the last parsed byte instead of re-reading each file from the start. Each row'smetadatacolumn holds a small JSON blob describing the session being tailed: the absolute working-directory path (cwd), the git branch (and, when an isolated-agent session inherits one, the most recent human-named git branch seen so far in that file), and the session id (a UUID), plus the session start time, the last activity time (used to age out metadata), the first-observed model name, and an internal parse-sequence counter. For a session file in a repository you did not configure, that path and those branch names are that other repository's (tier#1018 (c)). With worktree attribution on (#823), it also holds the absolute path of the linked git worktree currently carried, if any. Nothing here is captured unless you run the watcher; a one-shottierd scorewrites no checkpoint. Each row is keyed by an install-secret HMAC of the session file's absolute path, not the path itself. The key does not store the OS username, working directory or session filename. The metadata'scwdremains a plaintext absolute path. The row also holds the file's inode, the byte offset read so far, and a CRC-32 of the file's first bytes. An erasure keeps the key; see Data-subject rights below. - Opencode scan watermark (only with the Opencode collector enabled —
--opencodeor thecollectors.opencodeconfig key) — the Opencode collector shares the samewatcher_checkpointtable, under a row whose key is an install-secret HMAC ofopencode-db:<path to opencode.db>:<scope digest>. The scope digest is SHA-256 of the sorted configured repository paths (absolute where possible) and slugs; changing their order does not change the key. Adding a repository changes the key and triggers an idempotent backfill, including its rows older than the previous watermark. Legacy keys without the digest are not resumed, so an upgrade also triggers one idempotent backfill. Itsmetadatablob holds two integers and a schema tag, and nothing else: the highest Opencodemessage.time_updatedalready processed (epoch milliseconds), and the number of rows in Opencode's ownmigrationtable (a schema-drift tripwire). It carries no cwd, no branch, no session id, no model name and no developer identifier — unlike the JSONL watcher's blob above, which describes a session; this one describes only how far a scan got. - Raw GitHub webhook payloads (only when the GitHub webhook path is
enabled — i.e. you set
webhook_secret/TIER_WEBHOOK_SECRETand point GitHub deliveries atPOST /webhook/github; that endpoint is fail-closed and stays disabled without a secret) — for each processed delivery (pull_request,push,workflow_run), TIER retains the raw GitHub webhook request body, gzipped, in thewebhook_payloadstable so a score input can be re-derived months later (#137). Unlike every token-capture path above, this raw body does contain personal data: commit author names and email addresses, PR titles and descriptions, and commit messages — whatever GitHub places in a standard webhook payload (up to 25 MiB forpush, 1 MiB for the other accepted event types). It is the same PR/commit metadata GitHub itself already stores, and TIER adds no prompt text, completion text or file contents to it. ⚠️ The titles, descriptions and commit messages are free text, stored as written: anything someone pasted into them (code, a prompt, a secret) is stored with them. A body is kept for every delivery of those event types, before any filter on action or branch, so pull requests that never merge and pushes to any branch are kept too; per GitHub's documentation, anediteddelivery also carries the previous title and description. Editing text out on GitHub does not remove TIER's copy, and the per-developer erasure does not reach it: it stays until the retention bound below prunes it, unless you delete the rows directly (see Known residual —webhook_payloads). Retention is bounded: rows are pruned at startup and then every 24 hours while the server runs, by two limits — a 90-day age cap and a 50,000-row cap (oldest evicted first), enforced byPruneWebhookPayloads(internal/store/audit.go). Both bounds are currently compile-time constants (webhookPayloadRetentionDays/webhookPayloadMaxRowsininternal/store/audit.go), not config keys — there is no setting to change the window or cap without a code change, and no flag to keep the webhook path enabled while suppressing retention. The one operator control is the path itself: to store none of this, run TIER without the webhook path (JSONL capture andtierd shipnever populatewebhook_payloads). - Monthly closes (
month_close,month_person,month_evidence,month_paid) — frozen person/model/group-path contributions and their captured evidence. Erasure and retention replace named contributions with a nameless remainder, remove their evidence and preserve group/model totals. Exports include surviving named rows. - Install secret (
install_secret) — one stable random key per database, used for checkpoint and erased-import HMACs. It names no one and is preserved across schema upgrades, erasure and retention.
Schema 30 drops the obsolete local seal report, rollup, person, identity, coaching, node/fold/audit, gap, floor and canonical seal-history tables. Opening an older database does not convert those aggregate-only records to named months.
This list is not every table: the other per-developer tables (actual_spend,
org_hierarchy, period_membership, quality_events, quality_history,
repo_repair_audit, push_outcome_commits and push_outcome_audit) are in the erasure table under Data-subject rights
below, which also covers the audit ledgers and the operator-identifying fields
they carry (actor, reason, forgotten_by).
None of this is prompt text, completion text, code, file contents, or diffs, with
these exceptions: free text in raw webhook payloads (for example PR titles,
descriptions and commit messages) and the matching raw head ref or quoted title
in outcome_exclusions are stored as written, with whatever was pasted into them. The session_id above is likewise an opaque UUID — it groups a
session's events without persisting anything the session said or produced.
Server logs are a separate store. They can contain commit subjects, author logins, developer ids in request paths and client IP addresses. Commit subjects are free text and may contain whatever someone put there. Log retention and copies depend on the operator's logging setup; database erasure does not erase those logs.
Two of the entries above are conditional and hold data an auditor
should note. An ordinary watcher tail-state row is derived operational state,
safe to delete — the watcher rebuilds it from the live file on the next change.
⚠️ An erasure tombstone is not (see Data-subject rights below): deleting
one while its session file still exists lets the watcher read that file from the
start and store the erased session again. The tail-state contains no prompt or
completion text and no file contents; its key is an install-secret HMAC of the
path. Its cwd and (with worktree attribution on) its worktree path are absolute paths (filesystem locations, not
the contents of any file), which can reveal an OS username or an internal project
name. Raw webhook payloads hold third-party personal data (contributor names and
emails) at rest, bounded to ~90 days. Revert exclusion evidence retains a
matching raw branch or title, which can also contain third-party identifiers;
it persists until the author's erasure or database deletion. Operators should
treat the local database file (mode 0600, single-tenant) accordingly — and may
decline the webhook path entirely to store none of the webhook PII.
Retention on writable servers (#826/#1371)
The retention engine and startup/daily scheduler run on every real writable server, including before the first watermark. There is no reporting-mode off switch. The view-log window is capped by the same named-history window, and the Settings preview simulates that same prune inside a rolled-back savepoint. Read-only serving and the private synthetic demo do not initiate age-prunes.
The default is 90 days, the minimum 60 days, and the maximum 365
days. The minimum is derived from store.RevertObservationWindow, the longest
finite quality-change window; a smaller setting is refused with that reason.
The ceiling supports a full annual coaching cycle, not performance reviews.
An explicit serve --developer-retention-days N, TIER_DEVELOPER_RETENTION_DAYS
or developer_retention_days config key pins the window, in that order. Otherwise
the root Settings page's saved value wins over the default. Bounds are validated
before use; there is no off value. A page save that lowers the window requires a
preview and explicit confirmation and takes effect at the next prune.
One transaction tombstones affected watcher sessions before deleting their cost
rows, preserves cost horizons, folds already-priced costs, paid invoices and
allocated outcome points into a permanent name-free roll-up, freezes connected
correction chains, scrubs old endpoints, removes expired named rows and advances
a permanent watermark. Any failure rolls all of these changes back. Costs age
by event time, outcomes by merge time, quality rows with their outcome, and paid
spend by the end of its billing period. Current hierarchy and open memberships
stay; closed memberships age by their end. A leaver's aliases, canonical history
and current hierarchy disappear after the last retained reference ages out.
Small team/division labels fold into other; no person key enters the roll-up.
Offline live-window totals can include that history without re-pricing.
Real-server /scores reads frozen monthly person rows and conserved nameless
remainders; it refuses arbitrary historical windows. Historical repo/individual
reconstruction is unavailable.
Widening the window works forward only and never restores pruned data. Before
the first prune, installation backfill remains available. Afterwards, every
historical named writer respects the maximum cutoff ever applied, in every
mode. /events returns rejected_before_watermark; ship prints that count and
treats those rows as final. The report manifest and startup log disclose the
name-free refusal count. Consented graft imports separately retain safe source name-free totals under their destination node.
Event 3 reviews have no age deadline on intact, unpruned data. After a prune
freezes a connected chain, later reviews are recorded and return
ErrCorrectionFrozen without moving any allocation. Old endpoints read
aged out. Closed months pool removed named contributions into nameless remainders, remove their evidence and retain the frozen census and conserved totals. Raw webhook bodies never outlive the developer window (and retain their
existing 90-day/50,000-row cap).
Checkpoint keys are HMACs of the path using the install secret; the migration
re-keys existing rows atomically without changing offsets or head fingerprints.
Pruned sessions retain a tombstone and the watcher skips appends, including
after restart. Historical checkpoint metadata copied to a new filename is
also refused and replaced by a tombstone, using last activity time (or the
legacy start time) and retained session events. Sessions with retained activity
keep capturing. After a successful startup filesystem walk, unresolved file and
subagent checkpoint keys are deleted; other collectors' keys are preserved.
The job checks the result of wal_checkpoint(TRUNCATE) and retries within a
bounded deadline. If a reader prevents truncation, the committed prune remains
successful and logs a warning to use tierd backup. Successful truncation
removes old WAL page images; pages freed before secure deletion was enabled
still require the tierd backup / VACUUM INTO procedure below.
Back up before the retention upgrade. For a deployment requiring a full annual coaching window, select 365 days. Its database must be backed up before upgrading. Use the installed pre-upgrade binary against the actual meter database, writing a new snapshot file, for example:
tierd backup --db /path/to/meter.db --out /path/to/backups/meter-before-retention.db
Keep that snapshot separately and set developer_retention_days: 365 (or
--developer-retention-days 365) before the upgraded server's first start. The
snapshot contains personal data and needs the same access controls as the live
database. A backup is a separate copy: erasure of the live database does not
erase it. Age-pruning is destructive; the permanent name-free roll-up cannot
reconstruct individual records. A rollback needs the pre-upgrade database
snapshot.
Data-subject rights: access and erasure (GDPR Art. 15 / Art. 17)
Because TIER stores personal data keyed to a named developer, it ships two bearer-gated (write/admin-scoped) admin endpoints so an operator can honour a data-subject access or erasure request without hand-editing the database (#184). Root data-subject exports remain available and audited under every organization names setting; erasure retains its existing root authority:
-
Erasure —
DELETE /api/v1/developer/{id}deletes every row stored under that person's identifiers, and erases their watcher tail-state (see Watcher tail-state below), in one transaction (all-or-nothing). It first resolves{id}through the alias map (single-hop) and then removes rows stored under the canonical id or any raw login that aliases to it, across all developer-PII tables:Table What it holds token_eventsper-developer token counts and list-price cost outcomesper-developer PR/issue outcomes, weight, quality outcome_exclusionsrecognised revert PR tombstones and whole moved outcomes, PR author, classification evidence (the matching raw head ref or quoted title), source actor and timestamp; included in access exports and hard-deleted on erasure actual_spendper-developer actual-paid amounts org_hierarchythe developer's tree-derived team/division/org and node ID org_peopleenrolment origin, creation node and server-clock departure vendor_mappingsroot-authored vendor IDs and their prospective effective dates, End and Void dates; exported and hard-deleted on erasure org_auditsubject and actor names, frozen node names and named detail; erasure redacts these in place, including subject credential IDs, rather than deleting the row period_membershipthe developer's org-membership windows hierarchy_membershipthe developer's dated team/division history, including the rows each alias carries from the day it was added (#886, #914) quality_eventsper-developer CI/revert quality signals quality_historyper-developer quality-transition log repo_repair_auditwhich repositories a tierd repair-reporun moved this developer's stored spend intopush_outcome_commitsthe SHA, repository, commit time and GitHub push time ( push_order, #938) of each direct commit push capture folded into an outcome (#849); erasure removes only this developer's entries: a push outcome that also holds another developer's commit is kept and re-owned to that developerpush_outcome_auditeach time a push outcome in this developer's name was superseded by a merged PR or re-owned (#849); erasure also blanks this developer's commit SHAs in other developers' rows developer_aliasthe developer's alias→canonical mappings Watcher tail-state (
watcher_checkpoint) is erased by a tombstone, not always deleted (#919). The table has nodevelopercolumn. The erasure finds the person's rows through the session ids of their storedtoken_eventsrows, read before those rows are deleted. For each row it finds, erasure keeps a tombstone under the HMAC key. The watcher can delete the row when it observes the original path's removal. Erasure itself cannot reverse that hash to stat an orphan file. Otherwise itsmetadata(thecwd, branch, session id, model, start time, counters and worktree path) is replaced by the marker{"erased":true}, and the row keeps its path key, inode, byte offset and head CRC. The watcher reads that row before every read of the file and skips the file unless a full read of its first bytes (as many as the row recorded) finds different content: appends included, across restarts, and when the file is replaced by a byte-identical copy with a new inode (a backup restore,rsync, an atomic save). A file at the same path whose first bytes differ is a new session and is captured normally. Seven limits, stated plainly:- The HMAC key is kept. It allows the watcher to recognise the same path without storing its OS username, directory or session filename. A current session's export includes the stored hash, not the plaintext path.
- A tombstone can outlive its session file. The watcher deletes it on an observed removal, but an orphan hash cannot be reversed at startup to stat the file. Retaining the name-free marker is safe; dropping it while a session still exists could permit capture again. A developer retention prune also preserves these markers.
- Unjoinable rows are not found. A row whose session left no stored
token_eventsrow cannot be linked to a person, so neither the erasure nor the export can find it. For example, the watcher can record tail-state for a session whose working directory is outside every watched repository, and such a session stores no events. These rows hold the samecwd, branch and session id. Nothing ages them out today. They go when their session file is deleted. ⚠️ Do not delete such a row while its session file exists: the watcher then reads that file again from the start and stores whatever it can attribute. To retire one while its file exists, tombstone it instead: set itsmetadatato{"erased":true}. Erasures made before this release did not touch this table at all, so each one left that person's rows here as ordinary rows that nothing now links to them; retire those the same way. - Lines being read during the erasure are stored after it. If the erasure
commits while the watcher is already reading new lines from that person's
file, those lines are stored after the erasure; calling the erasure
endpoint again removes them. Where the file already had a row, the
tombstone stops every later read. A file on its very first read has no row
yet, so it gets no tombstone: the watcher saves an ordinary row for it and
goes on capturing it. Calling the erasure endpoint again once any of that
session's lines are stored removes them and tombstones the file. There is
one gap: if the erasure commits after the watcher stored lines from a file
with no row but before it saved the row, the erasure removes those lines
and the watcher then saves an ordinary row after them. The erased lines do
not return, but no stored line now joins that row, so neither the erasure
nor the export can find it, and it keeps the session's
cwd, branch and session id. It stays that way until the session writes a new line: the watcher stores that line, and calling the erasure endpoint again removes it and tombstones the file. If the session never writes again, the row goes when its session file is deleted. The same holds for a new session file at a path whose tombstone the watcher has just released. - A message held back by worktree attribution can be stored after the
erasure. With worktree attribution on, the watcher holds back a message
still being written until its session file has had no new line for 600
seconds (see
Worktree attribution). If the
person's session has no stored event yet when the erasure commits (a
session that has just started, for example), its file's checkpoint row
names the session id, but no stored
token_eventsrow carries that id, so the erasure cannot link the row to the person and does not tombstone it. When the hold ends, the watcher stores the message under the person, as an ordinary row, and goes on capturing the file. A session that already has a stored event is found and tombstoned as usual. If worktree attribution is on for the server's watcher, call the erasure endpoint again after the person's session files have had no new line for more than 600 seconds, and check the person's export: that call removes what was stored in between and tombstones the file. - A moved or renamed session file is not covered. Once the old path is gone the watcher deletes its tombstone, and it reads the file at its new path from the start (a move within the watched projects directory is seen at once), storing the erased session again. Calling the erasure endpoint again after that removes those rows and tombstones the file at its new path.
tierd shipis not covered. The tombstone protects only the watcher on the server host.tierd shipre-posts every event in its--sincewindow (90 days by default) on each run, and the server de-duplicates by idempotency key. After an erasure there is nothing left to de-duplicate against, so the next ship run stores those events again for as long as the lines are still in the JSONL files on the shipping machine. Stop runningtierd shipfor that person before erasing.
Closed months preserve totals. Erasure pools the subject's frozen rows into a nameless remainder and removes contribution evidence in the same transaction as raw-record erasure. Captured alias evidence selects only the months belonging to that subject; a retired identifier now held by someone else does not extend raw erasure to that person's records. Monthly erasure reports its affected rows under
month_person. Old seal-key tombstones and canonical seal intervals are removed by schema 30.The companion
repo_repair_row_auditledger is not listed because it has nodevelopercolumn to erase by: only a repair id, a row id, and the before/after repository slug (old_repo,new_repo). It deliberately does not copy the resolvingsession_id, precisely so that an audit record designed to outlive the row it describes cannot carry a session id past an erasure. Erasure deletes the subject's before-images by row id, before their token events, so their repository slugs do not survive it. A slug can identify a person (a personal repository such asalice/side-projectnames its owner; see Repository above). Other developers' before-images stay: this ledger is the per-row before-image atierd repair-reporun is reversed from by hand (there is no--undo). Developer retention removes before-images with aged source events or audit timestamps.All five audit ledgers —
reprice_audit,reprice_row_audit,repo_repair_audit,repo_repair_row_audit,cost_correction_audit— are append-only by construction: no code path mutates them; the tables themselves are not protected against direct database access. One of them carries more than that, and it is narrow:cost_correction_audit(the money-rewrite ledger) refusesUPDATEat the schema level, so no code path can silently rewrite a recorded correction — a mutation would have to drop the trigger first.DELETEis deliberately not refused on any of them — erasure (below) and future retention/GC are lawful deletes, and no database constraint can tell a lawful delete from tampering. None of this is tamper-proofing against someone holding the database file.The
cost_correction_auditledger (#346, the sanctionedPOST /costscost-correction override) follows the SAME pattern and is also not listed: it holds nodevelopercolumn, and — after an earlier draft got this backwards — deliberately does not copy the correction'sidempotency_keyeither, precisely because that field is often client-chosen and could embed personal data (an email, a ticket reference), and this ledger is designed to outlive the row it describes. It carries only a server-generatedtoken_event_id(which simply stops resolving once the row it names is erased or pruned — correct, not a bug) plusold_cost_micro/new_cost_microand the operator-suppliedactor/reasonthat explain the correction.actor/reasonname the OPERATOR who made the correction, not the data subject whose spend was corrected — the same third-party-identifier class as thewebhook_payloadsresidual below — so they are not erased by this endpoint. Operators must not put a data subject's own name or email inreason; nothing technical enforces that today.The
price_table_registrytable (#714 — the record binding each price-tableversion:to the content identity of the table served under it) is also not listed, and holds no personal data: a version integer, two content hashes, an effective date, a model count, a source, atool_version, afirst_seentimestamp, and — once retired —forgotten_atplus aforgotten_bythat names the OPERATOR who retired it (self-asserted, and a third party to any data subject whose spend was measured — the same class ascost_correction_audit'sactor). The companionprice_forget_auditledger carries the same fields and is likewise not personal data about a data subject. It describes a price table, not a person, and it carries nodevelopercolumn to erase by. ⚠️ One honest caveat, stated rather than glossed: itssourceis a filesystem path, and on a workstation install that path can incidentally contain an OS username (/Users/alice/prices.yaml). That is operator-environment provenance — the same third-party/operator class astool_versionand thewebhook_payloadsresidual below, not personal data about the data subject whose spend was measured. It is never served over HTTP. ⚠️tierd prices forget-version --commitdoes NOT remove it. It retires the identity — the row is kept and stamped, and aprice_forget_auditentry is written — deliberately, so the record of what a price version meant survives its own retirement. Treat that as a retention decision: neither command erases this row, and the same override path is in any case written to stderr at startup and intotierd score-log, both far likelier to leave the machine than a database row. There is no erasure path for it today; if a deployment ever needs one, it belongs alongside the other audit-ledger retention questions, not as a side effect of the escape hatch.On real servers, only root may insert or override manual costs. The actor is the authenticated credential ID, and new rows carry that ID. Root chooses the corrected row's developer; upload and personal keys are refused. Closed monthly inputs cannot be corrected, and retention uses the stored timestamp. The correction's reach remains bounded:
POST /api/v1/costsforcessource="api"and the correction path compares it, so an override can only ever touch a manually imported row. Automatically captured spend cannot be corrected through this endpoint at all.The erasure endpoint returns per-table affected-row counts under the historical JSON names
deletedandtotal_deleted, not physical row deletions. The correction counts are rows scrubbed:correction_declarationsincludes both the subject's declarations subsequently deleted and shared declarations retained with text cleared;corrective_relationsandcorrection_auditcount retained rows with identifiers and shared text cleared.total_deletedsums these affected-row counts, including retained rows. Thewatcher_checkpointcount (and sototal_deleted) is rows tombstoned plus rows deleted. It is idempotent (a second call, or a call for an unknown developer, deletes nothing and returns404). -
Access (DSAR) —
GET /api/v1/developer/{id}/exportreturns every row stored under the same resolved identifier set, plus the person's watcher tail-state rows (path key andmetadataincluded), found by the same session-id join the erasure uses, as JSON — the portable artifact you hand to the data subject. It cannot return the unjoinable tail-state rows described under Erasure above, because nothing stored links them to the person. Surviving named monthly rows and their captured contribution evidence are included; pooled remainders no longer identify the person.
Known residual — webhook_payloads. The structured erasure above does not
rewrite the raw GitHub webhook bodies retained in webhook_payloads (only present
when the webhook path is enabled). As described in What IS stored above, those
raw bodies may embed a contributor's name or email address. Surgically editing a
gzipped raw audit body to redact one identifier is out of scope for the erasure
endpoint; the mitigation is the retention bound — every such row is pruned at
startup and then every 24 hours while the server runs, by a 90-day age cap and a
50,000-row cap (PruneWebhookPayloads), shortened by an applied developer
retention cutoff,
so any identifier embedded there ages out within ~90 days. An operator who must
guarantee immediate removal of that residual must delete the affected
webhook_payloads rows directly. Running TIER without the webhook path stops new
payloads from being stored, but rows already stored stay until the retention bound
removes them or someone deletes them. The bound still runs with the webhook path
off: each time tierd opens the database, and every 24 hours while tierd serve
runs (cmd/tierd/webhook_prune.go).
org_actual_spend is org-level (no developer column) and holds no personal data,
so it is correctly outside the erasure/export scope.
Deleted bytes can remain until the database is compacted. TIER uses
SQLite's secure_delete, but erasure and checkpoints do not guarantee removal
of every leftover byte in the database file. This does not make the erased
person visible to TIER's readers or reports. The database and any remaining
bytes stay within your organization.
An administrator who wants to compact it now can stop tierd serve and run:
sqlite3 "$HOME/.tier/tier.db" 'VACUUM;'
Use the database path passed to serve --db if it differs from this default,
then restart serve. Compaction keeps the database's current records, including
frozen monthly rows and nameless remainders. Copies and backups already made remain separate files.
Alternatively, create a compacted backup and replace the database:
- Stop
tierd serve. - As the user that runs TIER, run
tierd backup --db <path> --out <new-path>, where<path>is the--dbyou givetierd serve(default~/.tier/tier.db) and<new-path>does not exist yet.tierd backupwrites the copy withVACUUM INTO, which rebuilds the file from its live rows only, so the copy has no free pages and no deleted rows. - Move the old database file, and its
-waland-shmfiles if they exist, out of the directory. Rename the copy to the old database's name. - Start
tierd serveand confirm your data is there. Then delete the old files you moved aside: they still hold the deleted rows.
Where it goes
The tier-federation/v2 contract sends closed monthly people, group paths,
integer measures, conservation controls and frozen census to the authenticated,
host-pinned parent. Raw capture evidence, aliases, keys and child audit stay
local. Identities remain origin-qualified, including equal displayed logins.
Name-free readers apply k, equality and complementary suppression here.
Owning tombstones are authorized through the approved origin's pinned route. Central-root erasure and local retention pool names into conserved remainders, block restoration and scrub invalidated staging and outgoing bytes atomically. A relay's own privacy action produces a redacted projection. Content digests bind exact retries; there are no signing keys or signed controls. The v1 aggregate wire, bridge and pending storage are removed. An upgrade with accepted v1 history refuses before changing its schema, rows or version.
The database is a local file (SQLite). Outbound network traffic depends on the commands you run, the collectors you configure and the traffic you route:
- the child federation pusher, when configured, sending frozen monthly v2
rows and owning-route tombstones to the host-pinned central address as
POST /api/v1/federation/push. Status and abort reconcile durable page reservations. Named projections include the currently permitted people; capture evidence stays local; - the reverse proxies (
/anthropic/,/openai/,/gemini/) forwarding your API calls to their upstream: by defaultapi.anthropic.com,api.openai.comandgenerativelanguage.googleapis.com, or whatever--anthropic-target,--openai-target(any OpenAI-compatible host) and--gemini-targetname. This happens only for traffic you route through them; - the Anthropic Admin API and OpenAI Usage API pollers, which fetch your
organization's usage and cost reports from
api.anthropic.comandapi.openai.com— only when thecollectors.anthropic_adminorcollectors.openai_usageconfig block is present; - the Copilot billing poller, which calls
api.github.comby default for organisation AI-credit billing usage, the paginated Copilot billing seats list and, in GitHub App mode, the installation token exchange. It sends the organisation, requested day and, for per-user requests, a lowercased query identifier, with a bearer credential. Query identifiers come from mapped numeric IDs resolved to current seat logins, plus every rostered developer id and every alias key whose canonical target is rostered, if the text matches^[A-Za-z0-9][A-Za-z0-9-]{0,38}$with no trailing or doubled hyphen; the reserved unattributed developer is excluded. These identifiers can be local OS usernames or configured developer ids, not just GitHub logins. An alias key that is a local username but happens to be a different org member's GitHub login is queried as that login, so its spend is attributed to the alias's developer unless an effective root mapping for that seat overrides it for that day; keep alias keys that are not GitHub logins unambiguous. Only root mappings supply counting evidence. Seat lists stay in memory for the pass, and missing seats are reported only as a count. Missing organization GitHub Copilot Business: read permission disables mapping polling; accept the App's updated permissions after upgrading. App mode sends an App-signed JWT and installation id to obtain the bearer token; the private key stays local. It runs only withcollectors.copilot_billingconfigured (or a configured doctor probe). The root-only Unmapped vendor users panel (GET /api/v1/org/vendor-mappings/unmapped) also fetches/orgs/{org}/copilot/billing/seatsand can trigger the installation token exchange, including on a--read-onlyserver; - the Cursor Admin poller, which calls
api.cursor.comby default for team usage events. The root-only Unmapped vendor users panel (GET /api/v1/org/vendor-mappings/unmapped) also fetches/teams/members, including on a--read-onlyserver. It sends the team Admin API key as HTTP Basic authentication with an empty password; usage requests also include the day's start/end timestamps and pagination fields. It sends no email or conversation id in the request. It runs only withcollectors.cursor_adminconfigured (or a configured doctor probe). Both new pollers allow an explicitapi_base_urloverride; outbound requests then go to that configured origin; tierd backfill,tierd outcomes,tierd qualityandtierd score --outcomes, which read the GitHub REST API (api.github.com, or the--github-api-urlyou pass) for repository, PR, commit and workflow evidence.score --outcomesalso reads/user. They send the GitHub credential and the requested repository, PR, SHA or run id, only when you run them;tierd doctor --config, which probes each configured provider, including Anthropic and OpenAI, using its configured credential, only when you run it;- the company import pull, which sends a one-time export key to the source
server's
GET /api/v1/export/bundleover HTTPS and receives consenting people's named data and name-free totals, only when root requests a preview; tierd hierarchy import, which sends the rows of your team CSV (developer,team,division,org) to a centraltierdyou operate, asPOST /api/v1/org_hierarchyto the--serverURL — only when you run it without--dry-run;tierd doctor --server, which sendsGET /api/v1/report_manifest?audience=diagnosticto thetierdyou name using a root key, optionally with--month YYYY-MM. It checks the clock and frozen monthly identity counts, horizons, pricing and input completeness without requesting named people or live/raw diagnostics — only when you pass--server;tierd issues --server, which sends oneGET /api/v1/issuesto thetierdyou name, andtierd healthcheck, which probes the configured server health path — only when you run them; andtierd shipforwarding captured usage events (the fields listed above, never content) to a centraltierdyou operate — from Claude Code sessions by default, from Codex rollout logs when--codex-rolloutis passed, from the Opencode session store when--opencodeis passed, and from Muse Code session logs when--museis passed. All four produce the same event shape; none forwards acwd, a Museworkspace_rootor any content. With--worktree-attributionon, Claude Code events also carry theirattribution_rulelabel and may carry theunattributed:foreign-repoissue label; no tool-call path is forwarded.
TIER does not call home or send telemetry to the project maintainers. The configured connections above can send data to your servers, GitHub and AI vendors.
Legal deployment guidance
This document bounds what TIER stores as content. It does not resolve the labor-law question: per-developer cost attribution is personal data about a named individual, and in TIER's EU target markets (DE Betriebsrat co-determination, FR/NL works councils, GDPR Art. 22 profiling) measuring named individuals is co-determined or restricted. For deployment guidance — works-council prerequisites, DPIA pointers, and a team-only vs per-developer decision table by jurisdiction — see legal-and-privacy.md. (Positioning guidance, not legal advice.)
Corrective work evidence
correction_declarations, corrective_relations, and correction_audit are
personal data covered by access/export and erasure. Each endpoint includes
nullable declaration and outcome ids, developer login, canonical repository and
PR number (repo#PR), merge SHA, and occurrence timestamps. Bounded supporting text,
disposition and appeal reasons, audit reason text and reviewer rationale are
subject-bearing content, bounded to 4,096 bytes per text field. Audit actor and
role are self-asserted; they do not establish authenticated identity.
TIER denies GET /api/v1/corrections/{kind} on real servers, including root requests. The synthetic/offline raw contract describes paginated relations, assessments, allocations and audit evidence in JSON or CSV, including free text and actor/reviewer identities. Use authorized subject-access exports or the local correction CLI for retained evidence.
Data-subject access exports all stored columns for either endpoint, including
shared declaration text and the counterparty's identifiers on shared relations.
For a reviewer who is neither endpoint, access returns only audit rows where
they are the actor and assessments where they are the reviewer; it does not
return other developers' allocations, relations or declarations.
Canonical repo/PR matching includes excluded outcomes under either
their original developer or trusted author, even when no outcome row remains.
Erasure resolves aliases, scrubs the subject's copied identifiers and
indirect declaration links, and clears shared text and audit actors before deleting
the subject's declarations and outcomes, including their declaration stats and
recorded timestamps. Relations and audit rows remain without the erased endpoint;
surviving evidence is retained. Evidence capture alone does not enable scoring.
When the operator enables Event 3 scoring, resolved allocations affect live
points as described below. Public GitHub can still link the surviving endpoint
to the erased subject through its PR history; this residual linkage is accepted by design.
Erasing a reviewer who is neither endpoint clears only matching actor/reviewer
identity columns. It preserves roles, reasons, supporting/disposition text and
reviewed sizing, and never freezes or reallocates the parties' allocations.
Schema version 8 prevents older erasers leaving this data behind.
Canonical declaration replays preserve the first evidence. A replay with an edited
declaration state or parent set returns the existing declaration id and the distinct
unchanged-ignored status (ErrCorrectionDeclarationIgnored) to the store caller;
it leaves declarations, relations, and audit rows unchanged. An identical state
and parent set is a successful no-op, including reordered parents.
The identifier-free API diff-stat cutover marker remains: pre-upgrade all-zero
API evidence is conservatively unavailable, including genuine zeros recorded by
the pointer-aware version-7 API writer, because the marker cannot distinguish
those from older omitted stats stored as zeros.
Event 3 also stores correction_assessments and correction_allocations as
endpoint-linked personal evidence. Assessments include reviewed original/own and
before/after cumulative sizes, comparison commit identities, bounded scope and
rationale, reviewer identity and review time. Allocations retain raw size, own
quality, share, final allocated points and marginal growth as distinct exact
rational values, plus pending/provisional/disputed and ranking eligibility state.
Export includes only rows where the subject's outcome (including aliases) is an
endpoint or participant, the subject is the reviewer, or the relation is a direct
subject relation. Export does not traverse the connected chain. Erasure deletes
the subject's own assessment and allocation rows, scrubs comparison identities,
scope, rationale, reviewer and review time from direct subject evidence, and
freezes every connected surviving allocation after severing its relation and
parent links. Reviews of unrelated survivors remain intact. Only survivors'
numeric allocations remain unchanged; they cannot be upgraded or reallocated
after any party is erased. Counts for these tables include rows deleted,
scrubbed or frozen. These rows do not enable scoring: the stored policy defaults off. Closed months preserve frozen contributions and policy provenance; schema 30 removes the old seal bodies and digests.
See Enable Event 3 scoring for configuration, rollback and startup behavior.
Enabled evidence and disputed state
Enabling scoring uses the stored reviewed assessments and allocations: reviewed cumulative growth is separate from individual allocated share. A pure repair can receive points transferred from the original without adding growth. Eligible unconfirmed same-author pairs use a provisional churn/file-count approximation; unconfirmed cross-author declarations cannot debit the other author. These figures are evidence with stated limits, not independently verified scope.
Actor and role are self-asserted local CLI input, not authentication. A
single-operator installation confirms its own pairs. Requiring an actor label
different from the declarer is advisory; it does not prove a separate reviewer.
A dispute or withdrawal after confirmation keeps the previous treatment until
operator settlement. Disputed state remains visible in developer/operator
views, authorized exports, the corrective digest and monthly closes. Real servers deny raw correction diagnostic routes; synthetic/offline anonymised raw views return not_shown. An operator who can read the database can read retained rows. Enablement, disputes, settlement and late evidence do not rewrite closed contributions; erasure and retention remove names and evidence while preserving totals.
See Event 3 coverage for recorded declarations and limits.
Identity credentials and view records (#1370)
Subject identifiers, credential labels and kinds, issue/revocation times,
view timestamps and
routes, and remap old/new values are personal data. identity_credentials,
coaching_view_audit and identity_remap_audit join the developer PII registry.
Erasure removes held keys, view records and historical identity references. Exports include their identity
metadata; credential hashes and operator credential fingerprints are secret
material and are excluded.
org_people holds the developer id, creation and departure times,
created_node_id (the node where the person was created), and creator/departure
actor ids. Developer erasure removes these rows and developer export includes
them. On-roster people stay live; a person marked left ages out from the departure
click recorded at the server clock. Org write actors accept only credential UUIDs
or the host-cli and break-glass sentinels, keeping developer names out of
immutable node-edge history.
Token events also store server-written provenance and the immutable
named_subject binding. Both are exported, and erasure finds events by either
raw developer or frozen subject. Existing rows remain legacy_unverified.
The view log's --view-log-days retention (default 90, range 1–365) is capped at the developer retention window whenever developer retention/pruning is enabled. See
developer access and upload identity.
Coaching card boundary (#1372)
TIER authorizes the holder and records the view before constructing a card from one read snapshot. Root sees retained subject data; node readers see only the subject's dated contributions within their branch, and personal keys see self. View-history entries follow the reader's permissions, so different holders can receive different card contents. No persistent copy of the card is created.
The route requires an authorized holder key and a successful view-log write.
A person with no spend in the window receives the empty card state. Named score reads return
this same card. For an open month, its spend is every event recorded under the
person's named subject or under any identifier currently aliased to them (the
developer name where no subject is bound), inside the reader's dated branch; a
closed month shows the frozen monthly rows instead. This is wider than the census:
the census and peer eligibility count only bound_upload with the subject's own
local credential or server_watch under --watch-as, and vendor rows count toward
them only with root-authored vendor mapping provenance. For the census and peer
eligibility, remapping an alias cannot promote excluded rows; on the card, remapping
an alias adds that identifier's earlier events. The excluded-spend share uses
rows bound to the subject or unbound rows recorded under that exact identifier;
it does not infer ownership of old aliases. Excluded dollars never leave this
calculation. The current card stores no additional personal data.
The 50% attributed-spend floor is independent of capture fidelity. Below it, TIER, its CI and work-type/no-issue figures are withheld with neutral text and an information icon. Typical ranges pool outcome costs across at least k other eligible same-team/org contributors, each above the attribution floor; they reveal neither identities nor per-person point values. Bot and remapped duplicate identities cannot pad that floor. Evidence is PR and opaque session-id links only, without titles, prompts, code or session contents. Structured annotations are deferred (PR8); the notes section states that explicitly.
TIER runs raw-data retention at startup and daily on every real writable server: 90 days by default, configurable from 60 to 365 days. Read-only serving and the synthetic demo do not initiate pruning; widening the window cannot restore removed detail.
Org tree and append-only audit
org_levels, org_nodes and dated org_node_edges describe the organisation;
org_people records named enrolments. org_hierarchy, hierarchy_membership
and retention_rollup carry node IDs to retain placement. Tree nodes and edges
are shared structure and remain after a person's erasure. Their labels are
organisation metadata, so operators should avoid putting personal identifiers
in node names.
org_audit separates nullable named fields (developer, actor_developer,
subject_credential_id, node_name_chain, detail_named) from retained action,
time, opaque actor credential ID, subject node ID, node ID chain and name-free
detail. It stores no credential verifier, token hash, prefix or token length.
New hierarchy membership writers also use opaque IDs or the host-cli /
break-glass sentinels. Pre-existing fingerprint rows are not rewritten.
Audit rows cannot be deleted or rewritten. The only update is a named value becoming NULL. Erasure clears those fields in rows naming the subject, actor, or credential subject. Access exports include exactly those rows; a row not about the subject carries only the subject's own name. Age pruning uses each row's timestamp and the same watermark-clamped developer window, within the developer prune transaction; team/division servers also run an audit-only scrub when full history folding has never been armed. Names in recent rows survive until their own cutoff, even if their subject has no recent activity. Audit rows never feed the live person set used by retention. Widening a window cannot restore scrubbed names.
Who sees org people and keys (#1421–#1423)
Root credentials see the entire organisation tree, people and
public credential inventory. Managing node credentials see names, membership,
creation metadata, reporting dates and public credential metadata within their
own subtree. Read-only node credentials see the tree within that subtree, but
neither the people nor the key inventory. Ancestors see creation and departure
as dates only.
The live branch and card routes also use dated holder scope, so root and node
keys can read the individual figures inside that branch. Read-only node keys
use the live Team screen for people; their key inventory remains closed.
Targets outside the branch return 404, the same as missing targets. A move of a
visible node requires authority over both parents; missing that authority gives
403. Shared and legacy tokens cannot reach these routes. Developer identifiers
are global, so POST /api/v1/org/people answering 409 tells a manager that the
name exists somewhere in the organisation, even outside their branch.
Personal credentials see only their own devices and exact last-data timestamps;
ancestors receive dates and freshness chips. No shared view receives these
inventories. A secret appears only in its creation/reissue response, marked
no-store. Lists contain opaque credential IDs, labels and lifecycle metadata,
never secrets, hashes, prefixes or token lengths. Reissue closes the old key and
creates the replacement atomically. Revocation retains device history and does
not erase work or revoke sibling devices.
Every successful administrative write appends an audit row in the same
transaction with the actor's opaque credential ID. Names, labels and level
names use the nullable named fields; permanent detail contains only approved
name-free values. The existing developer retention window scrubs audit names
on real servers. A newly enrolled person is not_counted until
eligible named evidence arrives. The creation cap (default 25 per UTC day across
a credential's whole branch, --people-cap-per-day) limits padding, but a
manager can still invent people and upload their data: a node can pad its own
subtree. k does not protect that subtree against its own manager.
Personal-key custody and invitations (#1425)
Managers and root distribute one-time claim codes, not working personal keys.
A manager who keeps a code could redeem it before the person does; that is
detected, not prevented: the person then sees "already claimed", and root sees
the reuse refusal and the redemption (audited with the claim-code actor). A code works only on
POST /api/v1/claim, expires after 72 hours and can be consumed once. The person
receives their personal key once on redemption and can later rotate it from
/me/key/rotate. Root recovery keeps their existing key active until the new
code is redeemed, then revokes every personal key of theirs and every device
those keys minted. Managers can end personal access only by marking the person
left. A stolen unused code can be redeemed by whoever holds it; secure hand-off
is still required. An already-used-code refusal states its claim time and is
recorded for root. Failed codes share the IP authentication lockout.
org_claims holds developer, node, issue/expiry/consumption/cancellation times,
opaque claim id, creator credential id or host sentinel, kind and the SHA-256
code hash. Plain codes are never stored or listed. Subject-access exports
include lifecycle metadata and viewer_notices, excluding code hashes as
credential security material. Erasure deletes both tables' subject rows in the
same transaction as other personal data. Retention deletes whole claim and
notice rows by their creation timestamps under the developer window, including
when only audit retention is armed; they never keep a subject live.
Claim issue, use, observed expiry and cancellation are audited and notified to
the subject. D10 applies: developer, actor developer, subject credential id,
node names and named detail age out; action, time, actor credential id, node
ids and opaque claim id remain. No code or verifier enters audit or notices.
Devices added by a manager/root show that creator's opaque credential id and
developer id to the holder, who can revoke the device. Mark-as-left stores the
stated leave date on the person's org_people row; it is exported with that
row and erased with it.
Access notices and settings changes (#1440)
Declarations, viewer keys and declaration notices are removed. Named card reads use the person, root or a dated branch holder, including read-only node keys, commit their view log before construction, and clip figures to the dated branch. The organization can choose names on (default), top admin only, or names off in Settings. Names off hides named figures from every other reader, including root; top admin only permits root alone to expand groups. Monthly records still include people under every choice, and changes are root-only and audited. Hidden-name reads use the existing whole-company monthly k decision, with no person/model filters, hidden row hashes or extra branch totals. Live screens show an explicitly requested whole closed month or the latest closed month. Personal keys keep their own live figures and card. Root data-subject exports remain available and audited under every choice; finance read-back refuses when names are off. The choice controls readers; it does not constrain the database owner.
The card states: "Your organization chooses who can expand groups to names. You can always see your own figures."
Person and node moves notify subjects of effective holder-access changes. A
holder with several keys receives the union of those keys' access. The notice
and move commit together, and a non-root self-grant is refused. Claim/key notices
remain personal data, covered by retention, DSAR and erasure. /me/notices and
/me/audit return only the caller's records. /me/views is a read-only personal
view history, available without qualifying spend; root and node keys cannot use it.
Historical audit names age out under the same scrub policy. Erasure's permanent
audit retains action, time and an opaque actor ID without the erased name.
Root settings saves record old/new validated values and the actor credential ID, never credential-derived material. Explicit host flags, environment variables or config keys pin settings; defaults do not. The unsaved retention control defaults to 90 days. Saved changes apply without restart; lowering retention requires a token-bound preview and confirmation. Pruning remains scheduled, so confirmation reports a pending fold. The preview runs the scheduled prune inside a rolled-back savepoint and reports its current deletions and scrub counts without changing data or the watermark. Data arriving before the scheduled prune can change these counts. Existing view-log retention is capped at the effective developer window. Widening cannot restore previously removed data. Read-only serving mounts no save route.
Legacy seals recorded live settings values and sources before schema 30 removed their storage. Their writer and replay adapter are removed. Current monthly closes freeze scoring and source provenance; later Settings changes do not reinterpret frozen rows. Shared-ingest settings and the daily request tally are removed in schema 30.
Consented graft snapshots and temporary previews
Source HTTP refusals disclose only the numeric status at the destination, never the source response body or headers. Validated numeric size/limit fields remain available for oversized-bundle refusals.
The Source export and Company import dashboard pages use the existing cookie
session and CSRF checks. The source page exposes only the signed-in person's
consent; root can issue a shown-once export key with its expiry. Hide, same-tab
navigation and pagehide erase that key from the page. The company page clears
the pasted export key immediately when it starts the preview request, even if
the request fails. Neither page writes secrets, previews, personal mapping
choices or the returned apply summary to browser storage, URLs or history.
Late responses cannot restore those values after navigation. Every clash needs
an explicit saved map/new choice and target before apply is enabled; matching
IDs never preselect a merge. The summary shows the API's stored receipt and
keeps withheld totals withheld.
Choosing map sends only the preview's matched existing person; editing a new
identity cannot redirect that mapping. A preview conflict clears the staged
batch and clash controls before another pull. Handoff notices about imported
people appear only when the stored summary reports named people.
A graft bundle contains named events and dated outcomes only for people with active consent on the source and accepted by the pinned v1 import policy. Whoever holds the person's personal key can give or withdraw consent; this is only as strong as claim-code custody. A root or managing node that redeems the person's onboarding claim code can obtain that key and consent with it. Each consent change writes a subject notice as well as an audit row. Consent covers the fixed event subject (the stored developer when no fixed subject exists) and exact outcome developer ID; aliases are not expanded into additional named histories. Departed holders are excluded from named export. The bundle projects an explicit field allowlist: credentials, viewer declarations, audit records, and view logs are excluded. Stored micro-dollar costs, price versions, pricing bases, and provenance travel with named events; the source generates deterministic keys for unkeyed events. The v1 digest covers the entire bundle except the digest field itself, using the same canonical JSON helper as the federation contract.
Bundle v1 exports jsonl, codex-rollout, opencode, and muse events only at
realtime fidelity, with nonnegative tokens and list costs within the import
ceiling (10,000,000,000 micro-dollars). Other source classes, incompatible fidelity,
negative cost or tokens, over-ceiling events, and invalid transport figures stay
on the source. Non-push outcomes without a PR number stay too; push outcomes may
have a null PR. New capture sources require a contract bump to travel in bundles.
Negative or over-ceiling name-free cost totals are omitted as unsafe, without
adding non-consenters to any retained-row report. Finite negative outcome and
name-free points keep their existing semantics.
A required digest-covered left_behind report aggregates only active,
non-departed consenters' retained rows, without a per-person split. Its
basis: "source-claimed" label distinguishes it from verified destination usage.
Its fixed ordered classes are jsonl, codex-rollout, opencode, muse, proxy,
anthropic-admin, openai-usage, cursor-admin, copilot-billing, api, other
(unknown event sources), and outcomes (retained outcomes, zero cost). Each
source_class carries a count and signed net_cost_micro, including zeros.
Missing, null, malformed, or overflowing reports are refused, never read as zero.
Preview and the stored apply receipt show the report and left_behind_rows,
separate from destination retention skips. Count and cost sums are overflow-checked.
The dashboard labels the retained-row total and class table as what the source
reports in preview and completion, hides empty reports, and allows import.
The report does not add destination spend or census evidence. Preview refuses
unacceptable row claims before storage; apply also refuses forged or hand-built stages.
The bundle's v1 change uses existing JSON storage without a database migration.
Consent responses, consent notices, and bundle_pulled records state:
"Some rows stay on the source because bundle v1 exports only accepted events and outcomes."
Imports are exempt from --people-cap-per-day because apply is root-only and
audited; ordinary people creation still follows the cap.
Non-consenting people and departed consenters contribute only to safe name-free
closed-month totals; departed consenters remain excluded from named rows and
the retained-row report.
These totals use validated frozen monthly rows, the stricter frozen/requested k
(at least three), per-person points/cost gates and shared full-tree
k/complement checks across all frozen additive measures. Frozen counted
evidence supplies cover; financial cells never add contributors. Active named
consenters are excluded once through frozen canonical and alias evidence.
Open months have no total. Pooled, unrecorded or otherwise unproven residual
populations produce an explicit eligibility_not_established monthly omission;
unsupported transport figures produce transport_figures_not_supported. The
required digest-covered name_free_omissions array carries only whole-month
bounds and a reason, without identities, figures or counts. Preview, stored apply
receipts and import screens preserve it. These generated totals retain the
transport marker sealed:false; bundles claiming sealed:true are rejected.
Later raw edits and raw pruning cannot change a closed-month total. Two pulls can
still be differenced as consent changes; closed-month suppression does not
provide protection against that cross-pull inference. Consenting people's figures
are excluded from name-free totals, including rows retained by the import policy.
Those rows remain in the source ledger and are accounted for by left_behind
in the bundle; consenters' source event counts and net cost equal exported plus
retained events. Pull does not delete or subtract source ledger rows.
Named exports write a subject-keyed bundle_pulled audit row for every exported
person, visible in their /me/audit. The stored row keeps the destination label
in named detail.
A recognized export key sent over plain HTTP is consumed before refusal. HEAD
and other non-GET methods return 405 without consuming the key.
For an import preview, the company server fetches from whatever HTTPS host root names in the source URL and sends the export key to that host. Choosing that host is a root-only trust decision; internal, loopback and link-local addresses are allowed. The URL and fetch limits do not restrict which addresses it can reach.
On the company server the preview holds the validated bundle for at most 24 hours
of use. A background sweep removes expired bundle bytes and mappings within the
next minute while the server is running, and startup also cleans expired stages.
The export credential is never retained. A source response echoing that credential
is refused. DSAR, erasure, and retention select held staged people and batch-person
metadata by their explicitly mapped local identity, falling back to the source ID
only when unmapped. Erasing or ageing out that local subject invalidates the whole
staged bundle. A colliding source name neither receives the held mapping nor
invalidates the mapped person's stage. After staging is cleared, source-named
subjects can export source metadata without the mapping's target developer,
and erase that metadata. Erasing a source-only name in an id_mapped audit row
nulls only source_developer; the target retains their audit row.
Consent and mappings participate in DSAR and erasure, and
named consent/mapping rows do not keep an otherwise aged identity alive.
A copy already imported by another server remains that server's responsibility: erasure must be requested there too. Withdrawing consent or erasing the source after a pull does not retract a copy previously delivered to the company server. The admin-only private-address SSRF residual is described in Security.
Applying a graft is one transaction, including the company import_applied
audit. Mapping decisions have company id_mapped audits, and delivery already
has the source bundle_pulled audit. No callback credential is retained.
An identical bundle has one effect even if previewed again. Reused event keys
or UIDs are skipped only within the same mapped destination person. Equal
content from another person retains its own row and spend. When no same-person
duplicate exists, a transport key or UID owned by another person refuses the
transaction. No person field is added
to bundle events, and name-free totals stay name-free. Conflicting content is flagged in the apply receipt only
when a stored fingerprint exists and differs. Native rows without stored fingerprints cannot be compared;
existing events are never overwritten. Source pricing and provenance are kept,
including an unknown price version of zero, without applying local prices. Apply refuses negative or over-ceiling list costs and capture-source or fidelity claims outside the contract-pinned v1 local-collector policy. Rows before the permanent retention watermark are skipped as final, counted in the receipt, and never recreated; later rows still apply successfully. Identical content staged as a new preview records an apply audit referring to the original batch.
New previews record a replay identity that excludes bundle issuance time while
leaving the wire digest unchanged. A fresh export of that same content returns
the original batch identity and lock times after destination erasure and
recreates no named rows. A fresh batch rechecks name-free totals against the
current destination k and preserves sticky withholding in storage and its
receipt. Exact retries of an applied batch retain their frozen receipt.
Before returning a replay receipt, apply removes the erased person’s plaintext
batch-person and mapping rows. Mapping to an erased target also leaves the
source person behind with the nameless erasure report line, even after remapping.
Older applied batches with cleared staging have only their old issuance-dependent
digest, so this identity cannot be reconstructed from that digest. Retrying
their original batch ID still returns its receipt. Presenting the exact original
bundle also preserves the receipt and records a stable identity for later pulls;
older pending previews acquire that identity when applied. Changed content is a
new import, subject to the erased-person record below.
Imported event rows cannot establish local census evidence even when their source
provenance says bound_upload or server_watch. New imported people count zero
until an upload bound to their own credential on this server; local watcher
activity alone does not bind them. Apply issues no keys or claim codes. Viewer declarations are removed. A mapped identity with local history but no org-person row stays native when registered at the destination. Imported spend stays in the Unknown identity bucket until that subject has qualifying local evidence, then joins the subject in live totals and future monthly populations. Imported rows never supply census evidence; existing monthly census and apply receipts remain frozen. Imported event provenance is included in the person's DSAR;
erasure deletes their named events through the existing subject erasure path.
Company name-free totals remain separate from named histories and the local
census, in external_totals at the destination node. They appear as Other in the
apply summary. Each source cohort must independently meet the destination k,
floored at three; otherwise figures are null and no count is disclosed. Re-imports
persist the highest threshold and any withholding, so a lower k cannot release a previously withheld total. Small
cohorts are not pooled to claim k. A changed or overlapping total from the same
source is refused atomically rather than added twice. These are source-reported
aggregates: the company cannot verify distinct people or dominance from the
name-free data. Frozen financial, horizon, retention, capture-loss and pricing provenance accompany the residual total and its first stored evidence; source sealed and price-version metadata are retained; apply
does not turn a generated snapshot total into an authoritative local seal.
Source event identity aliases are retained for deduplication, including identities whose content matched an existing event. They are exported only for their mapped subject, erased with that subject, and pruned with the canonical event or an aged subject. They never supply census evidence or keep an identity live for retention.
Federation acceptance and refusal records
Federated figures are reported by them, not re-checked here against child
raw captures. Production v2 receives monthly people and model associations from
approved origins on authenticated, pinned routes. (origin_id,person_id) keeps
namesakes distinct. Readers see only their authorized dated branch or personal
scope, with name policy, k, lever eligibility and audit admission before bytes.
Staged snapshots remain invisible. Conserved cells equal retained named rows
plus nameless remainders in every additive integer unit.
HTTPS is required by default before credential lookup, including loopback. Direct TLS or HTTPS asserted by a configured trusted proxy satisfies it; untrusted forwarded headers never do. Root can allow plain HTTP between servers in Settings. Names, figures and the federation key then travel unencrypted. The same organization choice governs this server’s outbound HTTP federation requests. A federation key writes only its approved admission and origin routes, reads only its own status and grants no report access. Hosts pin the destination URL, admission and receiving installation; bearers remain in 0600 files. There are no signing keys or signatures. Owning tombstones are enforced through the authenticated route. Revocation scrubs incomplete staging and stops future delivery while preserving accepted history and restore blocks. Rotation preserves namespace and receipts.
Owner erasure issues tombstones and pools every retained local month for each
resolved owning person ID, including earlier closed alias lineage. A relay's own erasure sends only its redacted
projection. Central-root erasure pools names locally, installs permanent blocks,
scrubs pending bytes and records a nameless audit receipt. Increasing retention
cannot restore names. Incoming blocked identities are consumed and counted in
blocked_records; their names are removed before staging. Restart-safe digest
progress validates the original snapshot bytes before publication, and immutable
controls conserve their amounts in nameless remainders. A retention preview
rolls back its privacy changes; confirmed settings govern the scheduled prune.
Longer retention, restart and exact retries cannot undo the stored cutoff.
Refused pushes are recorded without the presented token or any value derived from it. Records contain UTC time, a source-address class (loopback, private, public or unknown), a fixed reason, and the authenticated server id when known. Raw addresses and body-claimed identities are not stored. Root alone sees source classes. A top-level node reader sees only identified servers in its branch, plus unassigned attempts and hourly reason counts. Other node keys cannot read this log. Detail is bounded to 90 days and 10,000 rows; overflow retains only hour, reason and count, for 90 days. Refusals buffer in bounded process memory and flush on the existing 60-second worker and shutdown. The worker prunes durable detail and hourly counts even with no new refusals. A crash can lose unflushed entries: normally the last minute, longer during flush failures. No refused caller triggers a database write, failed flushes retain a bounded retry batch, and read-only serving performs no refusal writes. These records never feed the local census or retention_live.
People-page handoff and card notices (#1449)
The People page displays a newly issued claim code or invited manager's node key only in its transient result. Hide, same-tab navigation, pagehide and session loss clear it; it is never placed in localStorage, sessionStorage, browser history or a URL. As with the Keys page, opening a link in a new tab leaves the original page's shown-once result visible until it is hidden or that page is left. Recovery controls are root-only and use the existing claim-code API; a root sees "code used by someone else" from existing claim-reuse audit rows. This is a reuse warning, not proof of the redeemer's identity.
The People page distinguishes the stated leave date from the server-click date when they differ. The input defaults to and permits dates through the UTC day. Membership and keys end at the click; the stated date changes no figures. Erasure requires root and an exact typed person ID, separately from Mark as left. My card renders the card and view history. Named cards use the reader's dated branch. Schema 30 deletes declaration notices; the retained notice API carries claim and access lifecycle notices without writing read markers.
Live vendor identity review
Only root can explicitly fetch the People page's unmapped vendor users. The server reads current Copilot billing seats and Cursor team members with the configured collector credentials, holds the lists only for that request, and joins them to existing mappings and exact roster/alias matches. Opening People does not fetch a list. No fetched unmapped login, email or vendor ID is written to a table, cache, log, audit detail, error text or metric label. The GET works without writes, including under read-only serving. Vendor failures return fixed messages and no stale list; CSV import remains available.
The browser keeps review data only in page memory, clears it on navigation or sign-out, and uses no persistent browser storage. Root acceptance or CSV import creates the existing developer-keyed vendor mappings and one audit row per pair; those records retain their existing erasure and access-export coverage. A suggestion alone creates no mapping and never makes earlier usage count.
Monthly score readers (#1499)
Real-server /scores, /scores/compare and non-month report-manifest requests
now use the same monthly name-free projection as org comparisons. Root, node,
Read-node and personal keys receive the full-tree k/complement decision
at the close's frozen k. Shared, legacy-admin and viewer credentials are refused.
The census counts qualified distinct people independently of model rows; live
arrivals cannot enlarge a closed population. The old identity-total calculation,
including declaration visibility and hidden-spend dominance, no longer serves
these routes. Named branch reports and self-only cards retain their own scope.
The default is the latest monthly close; the default comparison uses its preceding UTC calendar month as well. Missing closes return 404. These reads refuse arbitrary windows and narrowing filters, disclose the real close time and frozen capture-loss flag, and admit the bounded audit tally before releasing figures. They disclose no hidden row hashes, row counts or erasure revision. The dashboard clears prior figures before requests and displays each month's same gated decision. Closed months and frozen contribution rows are the only stored reports. Schema 30 drops legacy seal storage; archived raw manifests can still be checked offline, while retired seal receipts return COULD NOT CHECK.
Monthly branch reports
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. Named monthly
reports/manifests and people CSV include authorized frozen paid cells once,
including paid-only people and nameless remainders. Name-free comparison JSON
omits hidden identities, fold membership and census counts; published financial
amounts use the same full-tree decision as points and list cost. Monthly
disclosures preserve close-time horizons, partial-input status and pricing
provenance. Named diagnostics stay within the authorized population. Name-free diagnostic counts publish whenever the whole row is shown; the frozen counts survive pooling. Withheld whole rows publish no counts. 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.
Group reports and CSV use named dated-branch projections. Name-free comparison and peer views use frozen rows with k and complement suppression. Cost-only and pre-tree spend remain in the monthly totals; pre-tree spend stays unplaced. Federation delivers the same closed amounts through v2 records; name-free receiving readers retain cost-only and sub-k suppression. No legacy seal or federation bridge producer runs.
Central monthly readers sum authorized local and committed received person/model rows and nameless remainders. A root can drill through immutable admission mounts to remote divisions, teams and origin-qualified people. Node reads require the branch and selection on the same dated path, in that order; personal reads stay local regardless of remote namesakes. Named views do not apply k. Cross-branch name-free comparisons run one full-tree k/complement decision, count opaque origin/person identities once, retain frozen anonymous census and exclude remainders from current peer and lever eligibility.
An explicitly selected month uses only that month's committed rows. Missing
origins and their available history are explicit; incomplete figures say
reported subtotal. There is no common-month search or substitution of older
figures. Offline group/person views and exports say reported through YYYY-MM-DD
from the latest committed closed month, never from staged pages or delivery
activity. A server-approved normalized HTTPS hostname supplies full-card links;
rows cannot supply URLs, and no central credential is sent to a child.
tiermonthmanifest2 reproduces authorized received sums, model allocation and
ratios, distinguishes local refolding from received/deidentified rows and does
not claim to refold child raw captures or coaching/timestamp evidence. Local
tiermonthmanifest1 verification remains. The live endpoint stays local.
All monthly reads admit the in-memory audit tally before releasing bytes and
fail closed on audit failure, without request-time database writes. Production
v2 is active and the v1 wire, storage, pending paths and aggregate bridge are
retired. Monthly screens clear prior figures before refresh and reject stale
responses after a new month or privacy decision.
Reporting coverage is scoped to the root/node reader's subtree in every
aggregation mode. Its native evidence is a valid own-credential bound-upload
receipt, with the census's roster, alias and bot exclusions. Shared and
imported-unbound uploads cannot make a local group reporting. Native groups below k or complementary-withheld show status only, including
in CSV; even setup/stopped reasons are omitted. Shown groups may expose an
aggregate UTC report date, receipt-day end boundary, next daily receipt date
and setup/stopped reason after the full dated population passes the shared k/complement gate,
without tree dominance. Each distinct reporter contributes at most one census
seat across dated paths. Monthly details also require a shown month_person
group from the full-tree comparison decision, with k at least the frozen floor;
multiple model rows add no census seats. Months without receipts use that
monthly gate. Missing closes and withheld groups remain status-only; seals
cannot authorize detail. Live and monthly views share the same reason definition.
Receipt dates and setup/status remain live diagnostics over retained raw inputs,
labelled local_evidence: "live_bound_upload_receipts" in JSON and CSV and
explained on the Coverage page. Late uploads and retention can change that
diagnostic; closed monthly costs and points remain frozen.
No native group publishes contributor counts, even above k. The N-of-M summary counts group statuses,
never contributors. Server rows describe the configured reporter's schedule
and acceptance state, not individual freshness. Coverage is a read snapshot
with no request-time database writes. It reuses the report/totals D9 in-memory
subtree tally and fail-closed behavior and is absent under --read-only.
Unended monthly requests return 400. Live server Current uses the end boundary
of the latest month whose end plus the 24-hour monthly-close grace has passed; a complete
accepted horizon remains Current even when its last push is over seven days old.
The Organization page displays closed-month cells only. Withheld figures and counts stay “Withheld” for root, “Withheld — ask your admin” for other readers. Published children remain visible under a withheld parent; the server suppresses complements. Its renderer omits contributor columns and counts for read-only node principals, including comparison and fold rows. Where provided, the API’s “Reported” or “Not reported” status replaces withheld people counts. Unavailable and months without a close both display “not available”, clear the previous figures and CSV link, and never become zero. Comparison places stored months side by side without computing a multi-month total or changing suppression.
The native “Teams on this server” rollup uses published root cells once, including dated slices, and never adds descendants to their ancestors. A historical cell whose parent was redacted is not treated as an additional organization root. If any required cell is withheld, this subtotal is withheld. When the current subtree contains an opaque federated place, the native subtotal is structurally “not available”. The report API also provides separate native/federated rollup fields, as described above; this page uses its monthly-cell renderer and does not derive a native remainder by subtraction or invent federation totals. Federated cells show “Read only · managed on its own server.” without figures or counts. Unplaced appears only at the tree root, labelled “Unplaced” in a distinct flagged row without a team contributor count. Root readers default to the structural company root when there is one. Organization page state and responses are never written to browser storage, and stale requests cannot repaint after navigation or session clearing. The shared sign-in shell stores only a bounded return path in tab storage, as described in Identity and access; sign-out and denied pages clear it.
On real servers, /time-to-merge reads frozen monthly outcome timestamp
evidence for holder keys in every mode. It projects the same full-tree
k/complement decision as monthly scores, then requires k distinct
eligible counted PR authors per statistic. Multiple model allocations cannot
pad authors or the PR sample. Every sample, contributor and exclusion count is
null, so subtracting a measured count from unit-weight points cannot expose an
excluded person's work. No hidden group receives a metric row. Older closes
without timestamp evidence and erased evidence produce explicit null statistics;
raw backfills cannot revise a closed month's durations.
Monthly name-free cache levers use frozen token numerators and denominators, with k distinct eligible input contributors on both sides of the cache partition. They release no hidden group IDs or other practice ratios. Erased people cannot supply category seats. Named branch and personal reports retain their authorized practice ratios, people and CSV. Opening a front-page group goes through the holder-scoped Organization report at the same month; it grants no cross-branch named access. The front page uses that month for time-to-merge too. Manifest failures clear stale figures and start no fallback report request. Legacy seal headers, stall states and disclosures are no longer interpreted by the browser.
Child federation exports (P28 + P29)
The production worker exports closed local and active received origin/month projections. It preserves original identity, immutable controls and owning watermarks, relays owning tombstones first, and sends a conserved nameless remainder after local erasure. Missing closes claim no successful delivery. Reserved exact pages survive retries and restarts. A privacy epoch change scrubs invalidated bodies, reconciles/aborts the remote identity and rebuilds the permitted projection. A connection test reads only status. No central request fetches child data. Token replacement retains the existing audited 0600-file handling and discloses no credential-derived values.
Federation and coverage display (P31 + P33)
The Federation page labels outside totals “reported by them, not re-checked here.” A missing or unavailable closed month displays no monetary amount; withheld totals also show no amount. A Company totals 404 renders None for all three rollups and leaves the CSV unavailable. Refusal detail and hourly roll-ups are bounded separately; roll-ups retain their counts. These pages add no per-team PR or person counts for closed name-free months. Coverage groups below the privacy floor show only Reported or Not reported, with dates and other details withheld. Reporting-detail checks use distinct reporting people per dated cell as their figures, preserving complementary suppression without tree dominance. Safe siblings retain their detail; a small group's necessary complement stays protected. Monthly detail also requires a shown frozen month_person comparison group. CSV uses the existing API suppression rules. Live server status is separate from the historical monthly report, and revoked servers are excluded from the current server denominator while their accepted history is retained.
Federation allow/reissue responses reach only the transient secret result.
Browser persistence, URL parameters, and inventory rows never receive the secret.
The child token paste field uses autocomplete="one-time-code" outside the
schedule form. Pasted child tokens clear after submission and on navigation;
the existing host file service owns atomic 0600 replacement and re-reads the file for every push
and connection test. The page displays only the host-pinned central address.
v0.7.0 organizations: who can do what
The credential scope map
links the rules for creating people, issuing keys and reading reports. The
people and key inventory boundary
also states the limit that a node can pad its own subtree. Padded people with
qualifying bound evidence also lend k to ancestor rows, so the figures of a
neighbouring real sub-k group can publish in an ancestor or fold. Anyone who can read that
ancestor and knows the invented amounts could subtract them to learn the
remainder. Monthly comparisons retain k and complementary suppression
without person-share or tree dominance gates (code:
internal/scoring/treesuppress.go, SuppressTree). Read it with the
claim-code custody limit: k is
not proof that an administrator has enrolled distinct real people.
One server per developer. Choose one reporting server for each person's live data. This is a deployment rule, not a check across servers. Federation sends monthly people rows with origin-qualified identities, but central has no cross-origin identity binding. It cannot recognize the same person reported by two children, or by a child and its own local capture. Reporting that person's work through both paths can count it twice. See federation acceptance for why federated totals are reported by them, not re-checked here.
When moving history between installations, use the graft consent and explicit mapping rules. The graft snapshot boundary explains why withdrawing consent or erasing the source cannot retract a copy already delivered to another server.
The child upstream SSRF residual covers outbound requests to the host's configured central origin. For a separate deployment review, see Legal & Privacy. TIER is built for coaching, not for evaluating developers. Per-person figures are not accurate enough to judge an individual.
Erased people and later imports (#1491, ruling A⁗)
TIER keeps this record so a completed erasure stays effective. Your organisation decides the legal basis with its counsel.
On an erasure request, if any identifier has an import trace, TIER stores one unlinked keyed hash for each canonical ID, alias and mapped source name. A batch row mapped to someone else is not a trace. If erasure deletes a mapping row whose target is someone else, it also records that source name; a wrong exclusion is reported and reversible. Applied-batch plaintext source-name rows for the erased mapped person are deleted. Retention ageing makes no entry and never deletes entries. The record holds no figures, timestamp or link between identifiers and uses its own HMAC domain. Hashes never appear in API output, logs, audit rows, receipts, previews or data-subject exports. Those compliance artifacts state the record's existence in fixed wording, without checking membership or counting entries.
The record is not anonymous: anyone holding the database could test a guessed identifier. It matches on the identifier alone, so the same identifier from any source is excluded, including a different person. Preview and apply leave a match behind, rechecking inside the apply transaction, with this report line naming nobody: “One or more people were left behind so a completed erasure stays effective.” The bundle is not refused; lookup failure refuses the import. A wrongly excluded person has to come forward because the report names nobody; root can release them. A different (renamed) identifier evades the record. A person erased here with no import trace is not recorded, except this clash case, so a later first import can recreate them. A staged copy can remain for 24 hours.
Entries are kept until root clears them or the database is deleted, with no timer.
A clear removes only the named identifier; a returning person with several
identifiers needs one clear each. Root states either “new consent” or “a different
person with that identifier” (the API value is different person with that identifier).
The response is identical whether or not an entry existed. Each clear is audited
without the identifier, hash or entry count. An audited clear-all states
“our counsel refused this record”. There is no dashboard control. Nothing in the
release waits for counsel, and no Article is cited as the record's legal basis.
The secret and entries share the database file. Entries do not survive a restore
from an older backup that predates them: restoring it loses newer entries and
undoes those erasures. Re-run erasures after a restore.
See the root API commands for one-identifier clear and counsel-refusal clear-all.
Import recording warnings (EXTREME)
Each source pull on both servers, and each destination apply, records its own
attempt before taking the writer lock. Cleanup retries on a detached context for
up to five seconds without holding the import-holder mutex during database-lock
retries. The last holder releases waiting captures after cleanup completes. Abandoned attempts and failed live captures set a warning in
the existing settings table, with the earliest and latest UTC dates when events
may have been lost. Only overlapping report periods carry
data_quality.live_capture_may_be_lost; org reports, totals and compare retain
the warning stored with each monthly close. Later losses do not change old closes.
Read-only serving re-reads the warning on every report. Busy startup recovery
warns with a sanitized error class and serving continues. Pending warnings retry
and writable close flushes them. No clear command is needed for later periods.
The dashboard says: “TIER may have missed some AI costs in this period, because a server restart or an import interrupted recording.” At most 10,000 captures run after responses concurrently; above that cap the response waits for capture. The erase dialog and success receipt disclose the retained erased-import record. Consent alone on a source server is not an import trace for that record.
Uploads during an import can receive 503. tierd ship retries the unchanged
batch for up to three minutes after the first 503, with bounded exponential
backoff and Retry-After support, and reports when it waits or gives up.
Other upload clients must retry themselves; proxy capture uses the separate
import wait described above. Re-sending an upload deduplicates.
A fresh import replay applies normally after root releases an excluded identifier when the original receipt had an erasure notice and no current exclusion remains. Same-person identity deduplication still prevents duplicate spend. Outcome export follows canonical aliases, retaining work type and provenance; rejected aliased outcomes are counted as left behind. A changed overlapping name-free total keeps the first total, records that choice in the receipt, and permits named rows to import.
Personal/upload credentials refuse departed subjects, including aliases, both at issue and on subsequent use. Copilot leaves a day unchanged when alias spend could overlap a preserved missing-seat mapping partition. Corrections CLI list/show open read-only, without migrations or startup pruning. The browser asks before hiding a one-time key or code; anyone holding a claim code can redeem it.
Live branch and model association (#1495)
/api/v1/org/live returns only the named people and activity in the credential's
dated branch when the organization's names choice permits it. Root includes all people when permitted; a personal key includes only its holder. When names are hidden, the route returns the shared closed-month group decision instead.
The row's upload time comes from receipt history independently of the figures window. Spend and
points reconcile with the selected group's person rows. The live read adds no
audit write; Team-screen reads do not appear in the person’s viewed-by log.
The card states this limit. Card reads retain their mandatory committed view log.
Model points are a cost-weighted association with merged outcomes. Attributable
cost can precede the selected period; the spend column remains period-bound.
A missing model stays in a labelled remainder. The published Model Report task
figures are bundled, linked and shown in their own units. No report is fetched
at runtime. Protected anonymous API and proxy access is denied on real servers.
tierd demo alone serves anonymous synthetic data
from its private, disposable database; offline recomputation mounts no listener.
Production federation sends frozen monthly people/model rows and conserved cells. Central applies the reader's k/scope/name policy, with name-free group views below the threshold and audit admission before read bytes. Late raw events do not revise closed rows; nameless remainders preserve erased amounts.
Live figures: identity, placement and limits (#1496)
Live Team, Person and Models reads join aliases to one canonical person after checking each raw identifier's dated placement. Spend uses the stored named subject when present and otherwise the producer's developer identifier, so shared uploads and upgraded history remain visible without gaining census eligibility. Personal keys open only their own card and figures. Last upload uses receipt history independently of the figures window, with the existing credential receipt fallback after retention; its visibility follows the holder's branch at receipt time.
Root totals include activity before placement. The live response's groups
array contains visible node totals (node_id, parent_id, name, spend_usd,
merged_points), plus unplaced:true and direct:true rows where needed.
Pass a direct:true row's node_id as ?node=direct:<placement> to read only that placement's direct people, under the same dated branch authorization.
Unplaced belongs only under the root. With independent company roots and no
placement evidence, it stays in the forest remainder visible only to the global
root credential; company node keys cannot infer its owner. Children plus direct and unplaced figures
reconcile with each parent; the overlapping ancestor rows must not be added
together. Organization uses one live response and loads closed monthly reports
independently, so large trees do not spend one read per group.
Live figures and holder cards default to 30 days and refuse windows longer than 90 days with HTTP 400: choose a shorter period or use closed monthly reports for older months. This window cap is the simpler bound on live read cost. Recorded zero-cost models receive an equal model share of associated points; remaining points are divided among paid models by attributable cost. With only zero-cost models the split is equal. "No model recorded" has a null cost per point rather than implying free work. Card opens on read-only servers explain that the view log needs a writable server.
Retired evaluation_mode YAML and saved settings are ignored. They cannot
relax live access or k; historical report replay retains its recorded labels.
Federation comparisons clear unsuppressed local totals before returning the full-tree monthly k decision. The reading server applies the maximum of its configured floor, received close floors and the hard minimum. Pinned name visibility applies inside each report snapshot. Erasure derives opaque identities for stored logins that cannot be sent on the wire; those logins and erased people pool into nameless remainders at later closes, preserving additive amounts and frozen census. Retention cutoffs still refuse expired local closes.
Restoring a server from backup
Federation delivery after restoring a server from backup needs a new admission. A resynchronization contract comes after v0.7.0.
Monthly publication residuals (#1508)
The organization owns these servers and their data. Monthly group publication uses k and complementary folds of at least two members, with all 44 additive slots checked for partitioning. Equality of figures does not veto a fold. Paid, realtime and work figures do not decide which group rows are shown. Nodes come from month activity; zero-figure, zero-census nodes are dropped even when seated or paid-only. Whole company stays in the decision.
These exact residuals are accepted. A person with $30 of a group's $100 paid can subtract their own amount to learn $70. A fold may publish 12 points or $60 paid that all belong to one sub-k member. A manager can subtract a known $70 branch from a $100 company total to recover $30 elsewhere. A leaver's removal between $100 and $80 snapshots can reveal $20. A revised row reading $120, was $100, reveals a $20 billing change. Paid outside shown groups of $100 can be the sum of a $60 idle engineering allocation and a $40 idle sales allocation. A first-ever late tier-one bill of $60 on a dropped node enters that company line. This is also why name-free groups exclude these allocations at every ancestor. They receive one identical amount-free note whenever the company line is nonzero. Named readers retain a person where they sat; their group paid can therefore differ from the name-free group, with that note explaining why.
No paid-carrier count or matched ratio is published. Partial coverage is a boolean on group paid and Spend Leverage. Whole-company unjoined counts publish whenever whole is shown. Work categories use one frozen safe Other work union, counting distinct counted people across its categories.
Every close freezes the billing basis, including aliases, dated duration weights,
seat flags and labels, even before invoices arrive. It is part of month_close's
personal-data inventory, subject export, erasure and retention. Billing revisions
use a separate ledger-ID chain and frozen basis. Erased seats retain nameless allocations on their frozen paths. Off-basis late
payers, including re-posts under erased identifiers, enter the company line. An erasure, including retention pooling, first reconciles
pending ledger rows in its transaction, bypassing the daily limit; a failure
explicitly reports that the whole erasure rolled back. Readers distinguish
awaiting billing, recorded dates, revised dates and previous amounts. Recorded
is not final while a prior closed month's source is still expected. Closes with
no basis say billing not recorded at close and refuse late invoices.
Federation v2 carries the frozen paid partition, recording/allocation status, realtime numerator, billing reconciliation and its independent revision chain per origin and month. It retains quality, horizon, retention, capture-loss, price/rubric/build evidence, dated labels and selectors. Relay forwards each origin once; its own invoice never reallocates onto child contributions. Readers sum integer numerators and denominators before deriving ratios; unknown billing remains unknown. Local and received rows use one census-only publication decision, with no fold-equality check. Paid stays outside that decision, with nodeless paid on the name-free company line. Erasure, retention and replay preserve amounts without restoring names. Multi-origin JSON and CSV retain each origin's disclosures. The organization owns its data; unreleased databases have no compatibility adapter. Paid is schema step 31 and federation is step 32.
Name-free live reads require one exact closed UTC month and refuse explicit partial or multi-month bounds with HTTP 400. Whole-month dashboard selections use the committed report including received origins. Name-free origin disclosures retain frozen warnings while omitting origin identity and build inventory; partial-input status follows only selected origins.