Overview#
Every committed write to a namespace is kept as an entry in the namespace's record, and the state of the namespace at any CSN is the result of applying entries 1 to that CSN in order. This page describes what an entry contains, how the record is protected against changes made outside the service, and the operations built on it: reading history, reading the namespace as it was at a moment, copying a namespace from a moment, and removing one. After reading it you can audit who changed what, recover from a bad write, and decide which record level your data needs.
Log entries#
An entry is a block of // header lines, a bare //, and then the declaration text exactly as the client sent it. The header lines that matter most when you read history:
| Line | Records |
|---|---|
csn= |
the entry's change sequence number |
ts= |
the commit time, from the service's clock, in UTC with milliseconds |
principal= |
the key id that made the write |
user= |
the portal user, when the write was made through the portal |
agent=, correlation= |
the client's Terse-Agent and Terse-Correlation-Id (or traceparent) headers, when sent |
idem= |
the idempotency key, scoped by key id, when sent |
authz= |
the version of the grants document the write was admitted under |
specVersion= |
the language version the declaration was written in |
alg=, prev=, chain= |
the hash chain (see below) |
Other lines describe how the entry is applied. Labels such as agent= and correlation= are recorded as given and are never used to decide anything. Here is an entry as the history route returns it to a key that can see the whole namespace:
// terse-log v1
// csn=42
// ts=2026-10-20T09:14:03.512Z
// principal=7Q3M9XBK2ADFHNPR
// requestId=0HN8A2KQ3T1R5:00000002
// agent=harbour-planner
// authz=3
// opClass=WriteSingle
// specVersion=0.6.0
// rules=2
// alg=hmac-sha256
// prev=1c9e4f0a7b3d52e86f14a0c9d7e2b6358a4f1d0e9c7b2a6f5e3d8c1b0a9f7e42
// chain=5e6b0f7c2d9a41e38b7f6c0a9d2e14b5c7a8f3e6d1b0c9a2e4f7d8c6b5a3e1f0
//
# Places
## Harbour(berths: 12)
fishing boat
The hash chain#
Each entry's chain= value is a keyed hash (HMAC-SHA256) of the entry, and its prev= line names the chain= of the entry before it. The service checks the chain when it serves history and point-in-time reads and when it copies a namespace. A change made to an entry or to a link outside the service is reported at the CSN where it occurs; the read continues and reports the finding. Every successful Issue answer on a chained namespace carries the newest entry's chain value in Terse-Chain.
The chain key is held by the service with the data. The chain therefore detects changes made without that key, such as an edit or restore made by hand, and it does not by itself detect a consistent rewrite by someone who holds the key, or the removal of the newest entries. Under the seal level, such a change to anything already sealed contradicts a statement the service has published; under worm, storage refuses it. A client that keeps the Terse-Chain value it received for a write can later check that the record still holds that value at that CSN.
Reading history#
GET /v1/ns/{ns}/history?from=&to=&limit=&path= returns log entries in ascending CSN order as one .terse document. from and to are each a CSN or an RFC 3339 time with an offset (2026-10-20T09:00:00Z). limit defaults to 100 and is capped at 1,000 entries, and the plan's per-read limit caps the size of one page. The key needs the history right; a key without it receives an empty 200 that is identical to an empty range.
| Response header | Meaning |
|---|---|
Terse-Csn |
the namespace's current CSN |
Terse-Chain |
the chain value at that CSN, on a chained namespace |
Terse-History-Next |
where the next page starts, when this page did not reach to |
Terse-History-Verified |
ok, unchained, violation, or violation csn=N |
A key whose history world is the whole namespace receives each entry byte for byte, with its chain lines, and can check each link and compare the last with Terse-Chain. A key with a partial world receives each entry pruned to the lines about nodes it may see, without alg=, prev=, chain=, resolvedAliases= and authz= lines. An entry with nothing visible to the key is left out and not counted. path= only narrows the key's world; a root-holder who adds path= receives a pruned page, so ask without it for the complete, checkable record. History on an address such as 7QK4M2C@4096 reads up to that CSN at most.
Reading a moment#
Any queries-only Issue can be answered as of an earlier point. Send Terse-As-Of-Csn: <n> for a CSN (a CSN past the current one is the current one) or Terse-As-Of: <RFC 3339 time with offset> for the newest entry committed at or before that time. The address form {ns}@{csn} or {ns}@{time} in the URL means the same, and works on the issue, history, explain, export, copy and playback routes.
POST /v1/ns/7QK4M2C/issue HTTP/1.1
Authorization: Bearer <key>
Content-Type: text/plain; charset=utf-8
Terse-As-Of: 2026-10-20T09:00:00Z
%%%SEAM%%%
? Places.Harbour
The answer's Terse-Csn and Terse-As-Of-Csn both give the CSN that was served, and Terse-Chain gives that entry's chain value. The rules:
- A request that carries declarations is refused with
400: the past is read-only. - Sending both headers, or a header and an address, is refused with
400. - A time before the namespace's first entry is refused with
422. - A moment too far from the nearest checkpoint (see Scrub and playback) is refused with
422, and the message names that checkpoint's CSN. - The answer is pruned to the key's
historyworld under its current grants. A key withreadand nohistoryreceives an empty200.
Copies#
An administrator of a namespace can create a new namespace that is an exact copy of it at a CSN or a time:
POST /v1/ns/7QK4M2C/fork?csn=40&name=before%20the%20agent%20ran HTTP/1.1
Authorization: Bearer <key>
csn or at (a time) chooses the point; name is the copy's display name, up to 200 characters, and is required. The answer is 202 with a Location for the copy's progress document, which moves through queued, copying, anchoring and active, or ends failed with a reason. GET /v1/ns/{ns}/fork lists every copy of a namespace. A point before the first entry is refused with 422, a copy that would exceed the plan's namespace limit with 409, and a full queue with 503 and a Retry-After.
The copy's entries 1 to n are the source's entries, byte for byte, with their original times and principals; its own record starts at n+1. Its record settings name the source, the CSN and the chain value it was copied at. The service checks the source's chain while copying and does not copy a record that does not verify. The copy receives the source's # Access grants as they stand (grants=current, the default) or as they were in force at n (grants=asof; if that version cannot be determined, the current grants are used and the progress document says why) and the source's description. It is created read-only. The copy is not addressable until it is active, it counts against the plan's namespace limit from the moment it is requested, and it takes its record level from its own plan. A copy holds every byte it needs, so it is unaffected when its source is deleted.
Record levels#
Every namespace's record is at one of three levels, set by the account's plan. Each level includes the ones before it.
| Level | Adds |
|---|---|
chain |
the keyed hash chain on every entry; every namespace has at least this |
seal |
a statement signed by the service at intervals, covering the chain value and a digest of the state at a CSN, also published into the account's event log |
worm |
storage-level immutability on every log entry and every seal for the plan's retention window; no credential the service holds can delete or rewrite them |
A raised level applies from the next time the service opens the namespace, so it may not take effect at once. Lowering from seal to chain first writes a system entry, # System(recordLevel: "chain"; was: "seal"; at: "…"), which appears in history, so the gap in seals that follows reads as a plan change. A namespace under worm cannot be lowered: it keeps writing immutable entries and seals whatever the plan later says. Under worm, no log entry or seal can be deleted or rewritten until its window has passed.
Seals#
A seal states, under the service's signature, that the namespace's record at a CSN ended at a given chain value and that the state at that CSN has a given digest. A seal document carries ns, csn, chain, export (the state digest), manifestHash and ts, signed with ES256 (alg, kid, sig). The public key is published at GET /.well-known/terse-seal-keys as a JWK set; the service uses one seal key for all accounts.
On a sealing namespace, every 200 that carries Terse-Chain also carries Terse-Seal-Csn, the newest sealed CSN at or below the CSN of the answer. GET /v1/ns/{ns}/seal returns the newest seal and ?csn=<n> the seal at n; it needs history over the whole namespace. Because each seal is also written to the account's event log, a record rewound or rewritten after it was sealed no longer matches a statement already published.
Scrub and playback#
The service keeps checkpoints of a namespace's state, saved states at past CSNs from which it starts point-in-time reads. The scrub is an administrator's check of them. For each of the newest checkpoints, the scrub rebuilds the state and compares it with the digest recorded for it and with its seal. A mismatch is reported in the scrub document and in the account's event log; nothing is repaired, and the namespace keeps serving.
A playback session lets an administrator step through the record one entry at a time, forward or back, checking each entry's chain link and the digest and seal of each checkpoint it passes. A session closes on its own after a period without use. The routes for both are described under Scrub and Playback in the API section.
What the record does not contain#
Only committed writes are entries. Queries, history reads, point-in-time reads and exports are not entries and do not change the CSN, and neither is a refused write. They appear instead in the account's event log, which the Logs page shows. Changes to grants are entries in the record of the companion document {ns}_meta, which /explain uses to report which grants governed a CSN.
Removal#
When you delete a namespace on the portal's Namespaces page, it stops answering within a few seconds: every route returns the empty 404, and it no longer counts against the plan's namespace limit. Its data is kept for a grace period, during which you can restore it; a restored namespace returns disabled, and you enable it to use it again. After the grace period, or at once if you choose to delete it immediately, the service deletes the namespace and its companion document, and writes the events namespace.removing and namespace.removed to the account's event log.
Under worm, the namespace's log entries and seals stay in storage until their retention window has passed; they are not readable through the API, and the deletion completes when the window ends. A deleted namespace's name is never issued again.