File lifecycle
Thalix Vault maintains separate histories for content and classification. Content changes create immutable versions; classification and lifecycle changes create immutable metadata revisions.
Coordinated upload
sequenceDiagram
participant C as tv CLI
participant S as Service
participant O as Object storage
participant D as SQL catalog
participant A as Audit log
C->>S: Request upload with Root Folder and metadata
S->>S: Authenticate, authorize, validate metadata
S-->>C: Operation-scoped upload authorization
C->>O: Stream bytes and SHA-256
C->>S: Complete upload with idempotency key
S->>O: Verify object, byte size, and checksum
S->>D: Commit file/version or advance current version
S->>A: Record success or failure with correlation ID
S-->>C: Stable file ID and version number
The uploaded object remains unavailable to readers until verification and catalog completion succeed. Retryable completion requests are idempotent and cannot create duplicate versions.
Content versions
- The first upload creates a logical file and version
1. - Later byte changes add sequential immutable versions.
- A version records original filename, MIME type, size, SHA-256, opaque storage key, uploader, and UTC creation time.
- Prior objects and rows are never overwritten.
- Downloads of any accessible version verify received byte count and checksum.
Metadata revisions
Changing display name, Root Folder, description, custom metadata, or archive state does not duplicate file bytes. It produces an immutable revision with actor, time, optional reason, changed fields, and before/after values.
Tag revisions (issue #58)
Tags are a permanent, user-managed name=value model, deliberately kept on separate storage from typed metadata: file_tags (current active state, one row per file per normalized name) and file_tag_revisions (append-only history, its own per-file revision_no sequence) never touch metadata_definitions, file_metadata_values, or file_metadata_revisions, and a tag mutation creates no file content version -- exactly like a typed-metadata change.
A tag revision's changeType is always the constant "tag", letting it slot into the same vocabulary metadata revisions already use ("metadata_values", "folder", "lifecycle") when tv file info --revisions/tv file revisions merges both streams into one changedAt-ordered list -- the merge happens in the application layer, not the database, and each stream's revisionNo stays scoped to its own table (a tag revision's number is never comparable to a metadata revision's). before/after on a "tag" entry are single-tag {name, value} snapshots (an empty object where the tag did not yet exist, or no longer exists), distinct from a "metadata_values" entry's full metadata-map snapshot. A no-op set (byte-identical name and value already active) or unset (an already-absent name) writes no revision at all, matching the codebase's established idempotency convention for archive_file/restore_file.
Set/unset require file owner or effective write access -- one level below typed metadata's manage requirement; list requires only read. An administrator with no grant on the file gets an unconditional override on all three tag operations, the same flagless model issue #55 established for folder/file read access, always recorded as its own admin.content.access audit event.
Archive and restore
stateDiagram-v2
[*] --> Active: first verified upload
Active --> Active: new content version
Active --> Active: metadata revision
Active --> Archived: archive
Archived --> Active: restore
Archived files disappear from normal search. Owners, managers, and administrators may discover them only through explicit archived-file queries. Restore changes visibility without changing content history. Permanent purge is excluded from MVP.
Failure and reconciliation
Object storage and SQL cannot share one transaction. The implementation therefore needs explicit pending and complete states, idempotency keys, timeout recovery, orphan-object cleanup, and reconciliation for completed rows with missing objects or objects without usable catalog records.
A recoverable error returns a correlation ID. No failure state may expose a partially completed version to readers.
Implementation (MVP-008)
Version add (tv file version add) reuses the coordinated-upload protocol above unchanged: begin_file_version_upload allocates the next sequential pending version on an existing file, and MVP-006's commit_file_upload/fail_file_upload commit or fail it exactly as they already did for a file's first version. tv file versions (list_file_versions) and tv file get --version <n> (begin_file_version_download) read committed history without ever touching the current-version-only tv file get/begin_file_download path. tv file archive/tv file restore (archive_file/restore_file) flip files.status and write one file_metadata_revisions row (change_type = 'lifecycle'), reusing the metadata-revision model above rather than a separate history table; search_files's --status/--archived seam is the only reader that treats archived differently from active. Both are available to the file's owner or an organization global_admin/admin -- the "manager" stand-in until MVP-009 delivers delegated manage grants.
Permanent purge has no RPC, HTTP route, or CLI command anywhere in this implementation. files/file_versions additionally carry a BEFORE DELETE (and, for a committed version, BEFORE UPDATE) trigger that unconditionally rejects the operation for every database role, including service_role -- ordinary RLS/privilege revocation does not stop a role with BYPASSRLS, but a trigger has no such exception.
Implementation (issue #58)
tv file tag set/unset/list call set_file_tag/unset_file_tag/list_file_tags; tv file info --revisions/tv file revisions additionally call list_file_tag_revisions and merge its rows with list_file_metadata_revisions's output by changedAt. All four RPCs lock the parent files row for update for the duration of a mutation (identical to update_file_metadata), so two concurrent tag mutations on the same file serialize into correctly-ordered, non-colliding file_tag_revisions.revision_no values rather than losing an update. file_tag_revisions (never file_tags, which stays a normal mutable current-state table) carries the identical BEFORE UPDATE/BEFORE DELETE-rejection trigger pattern the rest of this codebase's append-only tables use.