> ## Documentation Index
> Fetch the complete documentation index at: https://docs.suchi.page/llms.txt
> Use this file to discover all available pages before exploring further.

# Document lifecycle

> From upload or intake to a usable document: sources, splitting, title ownership, versions, links, notes, typed metadata, folder layouts, and archive serial numbers.

Part of the [architecture guides](https://docs.suchi.page/architecture.md). This page follows a document from capture to its stored, filed state. Confirm contracts against source and tests.

```mermaid theme={null}
flowchart LR
  I[Upload or intake] --> B[Original bytes in CAS]
  B --> T[Commit document, source, first job] --> J[Durable dispatcher]
  J --> P[Extract and normalize] --> F[File and review]
  P --> S[(SQLite text and metadata)]
  P --> D[Derived CAS objects]
  F --> S --> A[ACL-filtered retrieval]
  D --> A --> C[Web app and API clients]
```

## Capture and processing

`UploadDocument` requires write scope, enforces the body limit and sniffs content
rather than trusting supplied MIME. It streams original bytes to CAS, then opens
one write transaction to recheck authority, deduplicate by system/owner/hash or
insert the document, record provenance and enqueue system-owned `post-ingest` work.
After commit it nudges dispatch and emits audit. A lost nudge cannot lose the job;
re-uploading a live duplicate records its source without another extraction job.

Post-ingest selects the format path and stores extracted metadata/derived bytes.
The common post-content path performs local matching, automations, filing/thumbnail
refresh and optional `post-classify`. Document/version uploads share
`core/api/upload_prepare.go` for multipart validation, rewind-safe MIME detection,
source attribution, CAS and idempotency fingerprints; each owns its transaction
and authorization. Keyed response receipts commit with document/job in
`upload_idempotency`; subsequent keyed uploads prune receipts older than 30 days.

Device OCR is provisional PDF text subject to configured confidence. Native PDF
text remains authoritative; empty extraction cannot erase accepted device text.
`core/mimeutil` owns the normalized document-format registry: MIME aliases,
filename-refinement hints, output extensions, and post-ingest capabilities.
Mail, filesystem intake, post-ingest routing, downloads, and rendered filenames
derive from that registry instead of carrying independent format lists. Byte
sniffing still retains specific valid source types and repairs missing, generic,
or malformed MIME, so rescans can reach PDF encryption handling without rewriting
originals or changing the pipeline revision. [Supported formats](https://docs.suchi.page/formats.md) owns
dispatch, format limits and real-tool requirements.

## Splitting and archive serial number labels

PDF marker analysis and child extraction/CAS work stay outside the writer.
ASN planning first scans every page, then an immutable preflight rejects
malformed or mixed plans and values already retained anywhere in the selected
system. This runs before pre-consume and other mutable hooks. `assign` commits
the ASN on the existing document; `split` stages every derived PDF before one
writer transaction inserts all child rows, ACLs, sources and jobs and retires
the current live parent. No partial child set is visible. Each child inherits
the transaction-current parent system, owner, category, title text, source time
and sensitivity, but starts with automatic title ownership.

`split_origin_id` and its one-based index identify completed parts after parent
retirement. Retrying a completed origin returns those children; upload and
filesystem deduplication do not restore the retired parent over them. A trashed
child makes that result a conflict instead of silently manufacturing a
replacement set. Retry retirement preserves the original Trash deadline.
Legacy disposable separators share the planner but drop marker pages; ASN
marker pages are retained. See [barcode-assisted physical
filing](https://docs.suchi.page/formats.md#barcode-assisted-physical-filing-with-archive-serial-numbers).

## Filesystem and mail intake

Filesystem and IMAP producers use the same document/job transaction pattern.
Filesystem intake is a read-only, polling reconciler: `os.Root` confines opens,
recursive walking retains no per-directory watches, and checkpoints suppress
unchanged work. It skips symlinks, special files, hidden/legacy-error paths,
cross-filesystem directories and excessive depth; source files and sidecars are
never written, moved or deleted. Sidecar metadata, including a requested ASN,
is validated and committed with the document and first job; retained ASN
conflicts leave no partial row. Observation state commits with successful
document/job creation. An oversized observation remains current only while its
unchanged fingerprint exceeds the watcher's present byte cap, so a restart with
a raised or disabled cap retries it without clearing successful checkpoints.
Deterministic sidecar/schema errors become blocked for the unchanged source
fingerprint, while transient failures retry after a delay. A sidecar change
retries that file, a custom-field definition change through the API clears
blocked observations for that filing system, and any other blocked observation
is retried after an hour. All failure state remains in Suchi-owned storage and
audit. IMAP checkpoints ascending batches of at most 50 UIDs after
import/bookkeeping; failure stops the cycle while preserving earlier progress,
and connection tests do not clear polling failures. [Mail intake](https://docs.suchi.page/guides/mail-intake.md)
owns history/rules/health.
Portable import differs: `core/importer/bundle/` may restore supplied content,
archive bytes and metadata without scheduling extraction.

## Sources and title ownership

`document_sources` records acquisition, separately from identity, version chains
and relationships. One system/owner/hash document may have multiple sources, and
different documents/systems may share CAS bytes. Mailbox history preserves the
observed name/folder after account deletion. Split children/email attachments
inherit sources; each version records its own upload.

Document Detail derives its display from these rows without new storage
(`ui/src/lib/documentSources.js`). The **Original** row shows the oldest
acquisition's file name, with later distinct names folded beneath it. A
source's `filename` column names the file where `detail` does not: email
attachment children set it on the sources they inherit from the message, in the
same transaction that creates them. **Added from** shows one line per kind and
label, with older folders or watched-folder directories folded under the newest.
An upload with an API token issued by mobile pairing sets `client` to
`companion_app` from the token's server-recorded provenance, never from request
data; `label` stays the uploader, and the row renders as **Suchi Companion ·
name**. Other API tokens leave `client` empty and render as API uploads.

`documents.title_owner` is the narrow precedence state for naming: `auto`
allows model or rule naming, `rule` allows a later non-self-referential rule
refresh, and `human` blocks both. A rule template containing `{{title}}` may
consume an automatic title once; rule ownership then prevents replay from
feeding the result back into itself. Title placeholders expand in one pass, so
substituted values remain literal. API renames and approved title suggestions
pin the title; **Unpin** returns it to `auto` without changing the text and
makes it eligible for naming again. Filesystem sidecars
preserve an explicit owner and otherwise treat supplied titles as human;
Paperless import is human, new uploads and split children are automatic, and
replacement versions inherit both title and owner. Email subject parsing cannot
replace a non-automatic title. Title and owner changes share `title_revision`,
so in-flight suggestions lose to a later decision.

## Versions, references, and discovery currentness

Acquisition sources, replacement versions, and document links are separate
relations. `document_sources` records where one exact row's bytes came from.
`previous_version_id` and the internal `version_family_key` identify revisions
that supersede one another. A `documentlink` custom-field value connects two
exact rows that continue to coexist. None of these relations is inferred from
shared CAS bytes.

`core/api/versions.go` owns replacement upload and visible family history.
The upload rechecks predecessor change permission inside the single-writer
transaction, gives a previously standalone row and its replacement one random
`v:` family key, inserts the new source and `post-ingest` job with the row, and
copies continuity metadata: system, owner, filing category, sensitivity,
explicit ACL grants, manual tags, correspondents, title, and title ownership.
The ASN moves atomically to the new head and is cleared from the predecessor.
Only a selected folder layout whose template uses `{{ asn }}` follows it;
rendered moves for both affected revisions are enqueued in the same transaction.
Extraction state, accepted intelligence, classifier-owned tags, custom-field
values/document links, notes, non-ASN layouts, and other exact-revision state
start fresh. Every immutable blob remains unchanged. Idempotency receipts and
the predecessor/hash fallback recover a lost successful response without
creating a second version.
Before inserting a non-replay replacement, the same transaction requires the
predecessor to be the greatest remaining family ID and live. A newer or trashed
head returns the same ID-free `version_head_changed` conflict. This makes
concurrent replacement single-winner and prevents Trash/restore from creating
new sibling branches.

When an archive is upgraded, the v0.2.0 metadata migration assigns deterministic
internal `m:` keys to valid pre-existing chains and rejects a cycle or
disconnected predecessor graph before advancing the schema. Family keys are
not a public address. Discovery in `core/api/documents_list.go`, `search.go`,
`intelligence.go`, and `chat.go` uses `core/searchquery` plus
`documentVersionWhere`: by default a family contributes the greatest live row ID
visible to that caller. `version:all` and `version:older` change that selection.
Exact detail IDs, saved snapshots, and Trash remain exact-row operations.

`core/api/custom_field_values.go` owns reference mutation;
`document_references.go` owns typed detail projection and the paged computed
backlink view. An authenticated cross-system write requires change permission on
the source, view permission on the live target, and entry to both systems;
actorless writes stay same-system. Reads return a bounded target summary only
when the exact target is still live and visible. They never substitute a newer
revision or expose a raw stored target ID. Backlink counts/pages put source ACL
and liveness checks inside SQL before pagination. `has-field:` uses the same
exact-target rule inside its `EXISTS`, so hidden or trashed targets behave as
absent without becoming an existence oracle.

Trash suppresses a target from outgoing projections, backlinks, and field
presence without deleting the value, so restore makes it visible again.
Permanent target purge removes incoming `documentlink` value rows before deleting
the document. Replacing either endpoint neither copies nor retargets links.
The mutation helper and schema triggers reject self-links and links within one
version family. Reciprocal or cyclic links between distinct documents remain
valid named relationships, but no API recursively traverses them.
Verify these boundaries with `go test ./core/api ./core/customfield ./core/db
./core/searchquery ./core/trash ./core/authz` and the version/reference browser
workflow in `ui/tests/document-versions-references.spec.js`.

## Typed metadata and folder layouts

`core/api/document_notes.go` owns attributed, exact-revision notes. Document
Detail projects them with author/admin edit authority. Create, update, and delete
recheck the document ACL and current writer inside the single-write transaction;
replacement creates no note rows for the new revision.

`core/api/custom_fields.go` owns definitions and protects stored values from
schema drift: choice fields require normalized choices, a populated field cannot
change type, and an in-use choice cannot disappear. `custom_fields.managed_by`
records extraction-owned definitions. Upgrading adopts compatible existing
`invoice_*` rows, database triggers reject definition mutation/deletion, and
`core/pipeline/zugferd` creates or adopts the same typed fields transactionally.
The API reserves those names and reports the extraction owner on managed
definition and document-value rows. Per-document values remain ordinary editable
typed values. `core/customfield` remains the
single validation/write/render dispatch for API values, filesystem sidecars,
automations, and imported values. `document_references.go` projects typed raw
values, formatted display values, and choice metadata while retaining exact-link
ACL behavior. `core/searchquery` resolves `field:` against the system-local
definition before compiling the matching typed column; `has-field:` remains the
presence operator.

The UI presents filesystem-view templates as **Folder layouts** under Advanced.
The wire boundary remains `/api/rendered_layouts/`;
`core/api/taxonomy_crud.go` validates templates and serves sample previews.
Existing archives retain the internal `storage_paths` table,
`documents.storage_path_id`, and persisted automation action names. The public
bulk method and ACL kind are `set_rendered_layout` and `rendered_layout`. A
document with no selection uses the default filing-tree projection. Document
PATCH, path-affecting bulk edits, built-in automation metadata actions, template
edits, and layout deletion enqueue rendered moves in the same transaction as an
effective state change. After a successful bulk metadata transaction,
`core/api/documents_bulk.go` invokes `document_updated` rules once per unique
authorized document in request order. Like single-document PATCH, rule execution
is synchronous but fail-soft, cannot roll back the committed metadata, and uses
a context detached from request cancellation so a disconnect cannot skip it;
lifecycle bulk methods do not invoke it. Replaying an already-converged bulk or
automation assignment does not add another render job.
`core/jd/systems.Address` owns the derived filing-category projection: `SYS.AC`
after introduction, and empty while the original archive is unnamed.
`core/api` applies that value consistently to detail, list, search, receipt,
version, reference, and backlink rows. The value is intentionally non-unique;
numeric document IDs and URLs own exact identity. `/api/jd/resolve` authorizes an
existing category and returns its runtime ID for Documents filtering, without
performing or granting document ACL access.

Renaming or deleting a correspondent or tag, and renaming a filing system, also
enqueue rendered moves in the mutation's transaction for live documents whose
effective template reads the changed value (`correspondent`, `tag_list`, or
`jd.system.name`). Every filing-tree import moves the system's live documents.
Closing the classifier's `needs-review` marker during a human edit queues each
document that actually lost the tag. `view.EnqueueMove` skips trashed documents,
since the renderer projects only live ones, and every restore (single, bulk,
duplicate upload, or watched-folder duplicate) queues a move so edits made in
Trash reach the rendered path.

`archive_serial_number` is system-scoped physical-filing metadata. A document
PATCH accepts a positive JSON-safe integer or `null`; assignment is limited to
the live current version, while clearing is allowed on Trash and history to
resolve retained conflicts. Uniqueness spans live rows, Trash and historical
versions. Same-value writes are idempotent. Document Detail edits the value, and
list, search, Trash and version projections render the same **ASN *n*** chip.
`core/searchquery` supports numeric comparisons and null retrieval through
`asn:`, and source-bundle saved-view import maps exact, missing, greater-than,
and less-than rules to that query syntax. A layout using the exact `{{ asn }}`
variable is marked in previews/lists and cannot be assigned to a document
without a value. Combined ASN/layout writes validate their resulting state
before committing.

`core/api/setup.go` owns session-admin GET/POST for the live scan controls.
`core/settings.ResolveScanConfig` combines stored rows with boot-pinned
environment/config values and reports each pin to the API. A save that changes
a pinned value fails before the writer; accepted saves update only unpinned
rows, preserving the stored value beneath each deployment override.
`distro/app` validates the result against the boot-only legacy separator token
and publishes one atomic `postingest.ScanSplit` snapshot.
`core/pipeline/postingest` captures that snapshot once at job entry, so a
running PDF cannot mix settings while later jobs observe a successful save
without restart. Failed live validation restores the prior settings rows before
the API returns.

Verify these boundaries with `go test ./core/asn ./core/pipeline/docsplit
./core/pipeline/postingest ./core/ingest/fswatch ./core/ingest/sidecar
./core/api ./core/automations ./core/customfield ./core/db
./core/importer/bundle ./core/searchquery`, `make smoke-asn`, and the Document
Detail/Filing Tree browser workflows.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.