Concepts
qnop has a small vocabulary, and almost everything it does follows from how these few objects relate. This page is the map; the rest of the documentation assumes it.
A review, end to end
Section titled “A review, end to end”A document is uploaded and rendered. Reviewers mark passages, and each mark becomes an annotation carrying its own discussion thread. Every annotation ends in an explicit decision — accepted or rejected. Content changes create a new version, and existing annotations are carried onto it rather than orphaned. The review can only be finalized once no annotation is left open.
That last sentence is the product. Everything else exists to make it true.
Documents and versions
Section titled “Documents and versions”Uploading a document creates the first version. Every later content change creates a new, immutable version — old versions are never rewritten and stay fully readable. A document is therefore a chain of versions, not a mutable file.
Binaries never live in the database. The uploaded file goes to S3-compatible object storage, and the database holds the relational data that points at it.
qnop reviews PDF today. DOCX and Markdown are decided and pending on the same ingest pipeline, but do not ship yet.
Anchors
Section titled “Anchors”An annotation is not attached to a page number. It stores three layers at once:
| Layer | What it stores |
|---|---|
| Text quote | the exact wording, plus the text surrounding it |
| Position | character offsets within the extracted text |
| Layout | the region on the rendered page |
Three layers rather than one is what lets an annotation survive a new version: when the content shifts, qnop re-anchors existing annotations onto the new version by fuzzy matching instead of dropping them. An inter-version diff shows what actually moved.
This is the mechanism behind the claim that a comment “keeps pointing at what it means” — a coordinate alone would not survive an edit.
Annotations, threads and decisions
Section titled “Annotations, threads and decisions”Each annotation carries a Markdown thread: replies, reactions, and the context needed to decide. It also carries a status, and that status means something — it is a state, not a label someone remembers to set.
An annotation is OPEN until it is closed one of two ways:
| Status | Meaning |
|---|---|
OPEN | raised, still outstanding |
RESOLVED | the point was addressed — only the author may resolve it |
DISMISSED | the point will not be actioned — the review owner or an admin may |
Both closed states can be reopened. Every one of these transitions is recorded in the audit trail. Silence never counts as agreement.
The review state machine
Section titled “The review state machine”The review itself is a separate, explicit state machine — not a convention people are asked to follow:
The middle pair is the part worth understanding: IN_REVIEW and
CHANGES_REQUESTED are derived from the open-annotation count, not set by
hand. Raise an annotation and the review moves to CHANGES_REQUESTED on its
own; close the last one and it moves back. Nobody has to remember to update a
status field.
FINALIZED is gated. The transition is refused while any of three things is
true: an annotation is still open, a placement is still pending confirmation, or
no document version has finished extraction. That is what turns a review status
into something you can plan around — when a review says it is finished, nothing
quietly fell off the list.
Instances that need an escape hatch can set
review.finalize_with_open_annotations, which resolves the remaining
annotations as part of finalizing instead of refusing. It is off by default,
and turning it on gives up exactly the guarantee above.
People
Section titled “People”Access is modelled globally and per team:
ADMIN— administers the instance.MEMBER— the ordinary reviewing role.AUDITOR— a dedicated read role that exists to inspect the audit trail.
Teams sit on top of that, each with its own leads. A review can additionally run with anonymized reviewer identities when the process demands it — the contributions remain, the names do not.
What runs underneath
Section titled “What runs underneath”One container serves the REST API and the embedded web UI. It needs PostgreSQL for relational data and any S3-compatible object storage for the documents themselves. Long-running work — text extraction, re-anchoring — runs as durable background jobs on PostgreSQL, so a crash never loses work.
See Deployment for what that means in practice.
Editions
Section titled “Editions”qnop is open core. The main repository is the AGPL-3.0 community edition and is complete on its own: no user limits, no document limits, no feature gates.
Commercial add-ons are built in a separate repository against the published
qnop-spi contract and load at runtime — the community edition is never
degraded into a demo to make room for them. See
Extending qnop.
Next steps
Section titled “Next steps”- Quickstart — get an instance running.
- Configuration — every setting it reads.