Doc Review Workflows: Submit / Approve / Reject with Immutable Versioning
Wikis solved the problem of writing documents together. They did not solve the problem of governing them. The moment your documentation has to satisfy an auditor, or has to be relied on during an incident, the wiki model breaks down. Here is what a review-first knowledge platform looks like underneath: ETag locks, mandatory reject-with-reason, semantic diffs, and an append-only history nobody can quietly rewrite.

TL;DR: Wiki-style platforms optimized for collaborative editing made one assumption that does not hold in regulated or incident-response contexts: that the latest version is the authoritative one. When a document carries operational weight (runbook, policy, threat-model, change procedure), "anyone can edit, latest wins" stops being a workflow and becomes a liability. This post walks through the mechanics that turn a knowledge base into an audit-grade artifact: explicit submit / approve / reject states, ETag-based optimistic locking, mandatory rejection reasons, semantic diffs, and an immutable version history.
The default model for collaborative documentation is the wiki. Everyone can edit, the most recent edit wins, and the history is a linear chain of revisions you can inspect if you bother to look. For its original purpose (getting a team to write things down without a Word-document-via-email bottleneck), the model is excellent.
The model breaks the moment the documentation starts carrying operational weight. A runbook the on-call engineer follows at 3 AM. A response procedure the SOC executes during an incident. A security policy the auditor reviews next month. A threat model the architecture team relies on for the next release. In those contexts, the question is not "what does the document say now," but "what did the document say when this decision was made, who approved the change, and on what basis."
Wiki tools were not designed to answer those questions, and most attempts to retrofit them produce ceremony without substance.
This post walks through the model used in Krasper Thot, a knowledge hub built review-first rather than edit-first. The mechanics are unglamorous: ETag locks, explicit states, mandatory rejection reasons, semantic diffs, an append-only history. The result is a knowledge base that holds up under audit, behaves predictably under concurrent editing, and makes "what was approved when" a one-query answer rather than a forensic investigation.
Contents
- Why wikis are the wrong substrate for governed documentation
- The three-state document model
- ETag optimistic locking: concurrent editing without the last-write-wins trap
- Mandatory reject-with-reason: the form is the policy
- Diff viewer: semantic comparison over textual noise
- Immutable versioning: append-only history, period
- The audit record that falls out of the workflow
- What to ask a knowledge platform before betting on it
1. Why wikis are the wrong substrate for governed documentation
The wiki model rests on three assumptions, and governed documentation breaks all of them.
The first is that the latest version is the authoritative one. In a wiki, the most recent edit becomes the current truth, which is fine for a team writing meeting notes and exactly wrong for a policy document that has to be reviewed before changes take effect. Editing something should not, on its own, change what the organization treats as authoritative.
A second holds that history is a curiosity rather than a contract. Wikis do keep revision history, but they treat it as a debugging aid: nothing stops you deleting it, there is no signed audit trail, and no guarantee that what you see in the history is what was historically displayed.
The third is that edit conflicts are rare and last-write-wins is good enough. On a low-traffic page, sure. In a runbook that two engineers are editing at once during an incident, silent overwrites are exactly how critical fixes get lost.
A few tools in the category, including Confluence, have added approval features on top of the wiki substrate. They work for lightweight gating, but the underlying assumptions remain. The review step is layered on, not baked in; the history is mutable; the edit model is still last-write-wins.
A review-first platform inverts every one of those assumptions. The latest edit is not authoritative; the latest approved version is. History is append-only and cryptographically anchored. Concurrent edits are detected and surfaced as conflicts rather than silently resolved. The rest of this post is about what that looks like in practice.
2. The three-state document model
Every document in a review-first system lives in one of three states at any moment:
- Draft: being edited by one or more authors, not visible to general readers, not part of the canonical record.
- Submitted: frozen for review, visible to designated reviewers, awaiting an approve or reject decision.
- Approved: the current canonical version, visible to all readers, immutable until a new version is approved.
┌───────────────────────────────┐
│ │
author edits │ │
──────────▶ [Draft] │
│ │
│ submit │
▼ │
[Submitted] │
│ │
┌─────────┴─────────┐ │
│ │ │
approve reject │
│ │ │
▼ ▼ │
[Approved] back to Draft │
│ with reason ────────────────┘
│
new draft for next version starts from here
The transitions are gated. Only authors can move a document into Submitted. Only designated reviewers (not the author) can move it out of Submitted. The system enforces this at the API layer, not in the UI, so no client-side bypass can change a state without the backend logging the actor and the decision.
Approval is not the end of the document's life. It is the end of that version. A new draft for the next version begins immediately, branched from the approved state, and the cycle repeats. The version history grows linearly through approved versions; drafts and rejected submissions are recorded but never become part of the canonical chain.
3. ETag optimistic locking: concurrent editing without the last-write-wins trap
In operational contexts, two engineers editing the same document at once is common enough that you have to design for it directly. A review-first platform handles it with optimistic locking based on ETags.
Every read of a document returns an ETag, a strong, version-keyed fingerprint of the document state at the moment of the read. Every write must include the ETag the editor was working against. If the document changed in the meantime, the ETag no longer matches the current state, and the write is rejected with a conflict.
┌─────────────────────────────────────────────────┐ │ Editor A Server Editor B │ │ │ │ │ │ │ │ GET doc │ │ │ │ │◀───────── etag:v7 ─────────────│ │ │ │ │ │ GET doc │ │ │ │◀────────── etag:v7 ──────│ │ │ │ │ │ │ │ PUT etag:v7 │ │ │ │ │──────▶ (accepted, now v8) │ │ │ │ │ PUT etag:v7 │ │ │ │ │◀──── 409 Conflict ───────│ │ │ │ │ │ │ │ │ B re-reads, sees v8, │ │ │ │ resolves, retries │ └─────────────────────────────────────────────────┘
The conflict response does not just reject the write: it returns the current document state plus a structured diff between the editor's stale version and the current one. Editor B sees what Editor A changed, can resolve the conflict explicitly, and retries with the updated ETag.
This is more friction than last-write-wins, and deliberately so: a little resistance at the right moment is what prevents data loss. In practice it costs one extra round-trip when a conflict actually happens, and in exchange no edit ever silently disappears.
ETag locking applies to drafts. Submitted documents are frozen, no edits during review. Approved documents are immutable, no edits at all; the next version is a new draft branched from the approved state.
4. Mandatory reject-with-reason: the form is the policy
A review workflow is only as useful as its rejection mechanics. If "reject" is a single button with no required input, reviewers either stop rejecting (because it leaves the author with no signal) or reject without explanation (which trains authors to ignore rejections). Neither produces better documents.
The fix is structural: rejection requires a reason. The API refuses to record a reject without a non-empty rationale. The UI surfaces the reason field as the primary action on the reject affordance, not as an afterthought.
┌──────────────────────────────────────────────┐ │ Reject submission │ │ │ │ Document: incident-response-runbook v8 │ │ │ │ Category: ▼ [Required] │ │ ○ Incorrect technical detail │ │ ○ Missing required section │ │ ○ Conflicts with existing policy │ │ ○ Not aligned with current release │ │ ○ Other (requires explanation) │ │ │ │ Specific feedback: [Required] │ │ ┌─────────────────────────────────────────┐ │ │ │ Step 4 references the old isolation API. │ │ │ │ Update to use the unified action API per │ │ │ │ ARCH-2026-014. │ │ │ └─────────────────────────────────────────┘ │ │ │ │ References (optional): [link to spec/ticket] │ │ │ │ [Cancel] [Reject submission]│ └──────────────────────────────────────────────┘
Two design choices in that form matter beyond the obvious.
First, categories are structured but extensible. Reporting on "why are documents being rejected in this team this quarter" is a single aggregation query. Patterns become visible: if 60% of rejections in a quarter are "Missing required section," the template needs work, not the authors.
Second, the rejection itself becomes part of the document's history. Future authors editing the next draft see prior rejections and their reasons inline. The institutional memory accumulates with the document instead of disappearing into a chat channel.
5. Diff viewer: semantic comparison over textual noise
A diff between two document versions is what the reviewer actually spends their attention budget on. Textual diffs (line-by-line, the default of every revision-control tool) are noisy for prose documents. Reformatting a paragraph produces hundreds of changed lines that contain no semantic change.
A diff viewer for governed documents needs to operate at the block level: was this heading added? was this paragraph rewritten? was this list reordered? was this table modified? The reviewer can expand into textual diffs where they want to inspect carefully, but the default view shows them the changes at the granularity that matters for approval decisions.
┌────────────────────────────────────────────────────────┐ │ Diff: runbook v7 → v8 (submitted) │ │ │ │ ▼ Section 3.2 "Initial Triage" - modified │ │ Paragraph 2 rewritten [expand] │ │ │ │ ▼ Section 4 "Containment" - modified │ │ Step 4: old "call /isolate endpoint" │ │ new "submit isolate action" │ │ │ │ ▶ Section 5 "Notification" - unchanged │ │ ▶ Section 6 "Audit" - unchanged │ │ │ │ + Section 7 "Post-incident review" - added │ │ │ │ [Approve] [Reject with reason] │ └────────────────────────────────────────────────────────┘
The view also surfaces edits to metadata: owner change, tag change, classification change. Those are often more consequential than content changes (a "Public" document silently re-classified to "Internal" can break downstream automation), and they should never be invisible to the reviewer.
6. Immutable versioning: append-only history, period
Every approved version is stored as an immutable record. The storage layer is append-only at the API; there is no "edit a previous version" operation, no admin override, no "replace this version with a corrected one" affordance.
If a previously-approved version turns out to be wrong, the fix is to author a new version, take it through review, and approve it. The wrong version remains in the history as a record of what was approved at that time. This is uncomfortable for some organizations and load-bearing for the rest of the value of the system: if previous versions can be silently rewritten, the history is not evidence.
The immutability is enforced two ways. At the data layer, each approved version is content-addressed by a hash of its content plus metadata. At the storage layer, the records are write-once. A modification attempt produces a new record with a new hash; the prior record is unchanged.
The version chain itself is hashed forward: each version's record includes the hash of the previous approved version. The chain can be verified end-to-end at any time. Tampering with any individual version breaks the chain, and the break is detectable.
7. The audit record that falls out of the workflow
The combination of explicit states, mandatory rejection reasons, ETag-locked edits, and immutable versioning produces an audit record as a side effect rather than as a separate concern.
For any document, at any past point in time, the system can answer:
- What was the approved version of this document on date X?
- Who submitted version N and at what time?
- Who reviewed it, when, and with what decision?
- If rejected, what was the categorized reason and the specific feedback?
- What metadata fields changed in this version compared to the prior one?
- Does the version chain verify cleanly?
These are the questions an auditor asks. They are also the questions an incident responder asks ("what did the runbook say when this incident started?") and the questions a release manager asks ("what policy was in force when this change was approved?").
The same record satisfies all three. There is no separate "compliance export" that has to be reconciled against the live system; the workflow is the audit trail.
8. What to ask a knowledge platform before betting on it
If you are evaluating a knowledge-management platform for governed documentation, the questions worth asking are these.
Is the approved version a distinct state from the latest edit, or are they the same thing? If they are the same thing, the platform is a wiki with a comment field, not a review platform.
If two reviewers approve different submitted versions of the same document at the same time, what happens? The honest answer involves ETag locking on submissions too, not just on edits.
Can a rejection be recorded without a reason? If yes, the rejection record is decorative.
Can a previously approved version be modified or deleted? If yes, the history is not audit-grade.
Show me the chain verification. If there is no chain verification, there is no cryptographic anchor; tampering is undetectable.
How a platform handles these questions tells you whether it has done the governance work or just done the wiki work and called it governance.
Closing
Governed documentation has to be designed for from the start, not bolted onto a wiki after the fact. That means explicit states, locked edits, mandatory rejection rationales, semantic diffs, immutable versions, and a verifiable chain.
The mechanics above are not exotic. They are unfussy applications of well-understood patterns: optimistic concurrency, append-only storage, hash-chained verification, structured forms as policy enforcement. The investment is in insisting on them across the platform, not in inventing anything new.
The next post in this series steps into Thot's code-analysis side: SAST results that arrive on every commit and have to be reviewed, suppressed, or fixed under the same governance discipline as the documents above.
Further reading
- RFC 7232, HTTP Conditional Requests (the canonical ETag spec)
- ISO/IEC 27001:2022, Annex A.5 (Documented information)
- Martin Kleppmann, Designing Data-Intensive Applications, chapter on concurrency control
enterprise infrastructure?
Schedule a technical briefing. No sales pitch, just architects and your team.