stralos-domain exports the authoritative shared types for the cloud-drive document sync feature. The implementation (endpoints, serialisers, worker logic) lives in api-documents. This page documents the domain contracts that cross service or client boundaries.
Some wire shapes on this page are no longer domain exports. EstimateDocument, ProcessDocumentResponse, DriveVersionConflictCode, and the document-usage event types were removed in domain 4.0.0 because no repo imported them. The shapes are still what api-documents serves and accepts; each consumer declares its own type.
Overview
Cloud-drive sync lets a Stralos organisation connect a shared Google Drive or a Microsoft SharePoint library to one or more projects. The sync engine crawls the connected folder, records each file as alinked document row with no bytes, and keeps the links up to date as the source changes. A file’s bytes are fetched when someone opens or processes it. See Link-first drive sync below.
Provider support
DriveSourceProvider type is a closed set matching the registered provider abstraction keys.
Drive source
ADriveSource represents a registered shared drive or SharePoint library.
DriveSourceConfig carries the drive-level identifiers resolved at registration: { driveId } for Google Drive, { tenantId, siteId, driveId } for Microsoft Graph.
DriveSourceListItem extends DriveSource with a mappingCount field that reflects the number of active sync connections pointing to this source, and a proofFileName field that carries the proof-of-control file name when the source is in pending_proof status (otherwise null or absent).
Source lifecycle
pending_proof— newly registered; ownership is not yet confirmed. The source is unusable until the proof-of-control nonce file is verified.active— fully operational.auth_error/error— the source has degraded and can only recover via/verify.disabled— temporarily paused; re-enabled via a PATCH request.deleted— soft-deleted; not shown in normal listings.
pending_proof, registration returns a derived proofFileName so the administrator can complete verification.
Sync connection
ASyncConnection maps a source (or a sub-folder within it) to a project.
SyncConnectionListItem extends SyncConnection with latestRun: SyncRun | null.
SyncConnectionDetail bundles the connection, its recent runs, and its notable file list:
Connection lifecycle
crawlKind is non-null during a scheduled full crawl. null indicates steady state (incremental changes only).
Sync run
ASyncRun captures the visibility counters for one sync pass.
earlyStop: true means the run hit its per-run budget and yielded before draining the change feed. A subsequent run will resume from the saved cursor.
Synced file status
error is transient: the file could not be placed (for example, its ancestry could not be resolved) and is retried on the next sync cycle.
ingesting and failed are copy-era states. The link-first engine no longer writes them; it re-evaluates a mapping still in either state on its next change and links the file.
SyncedFileSummary is the shape used in the connection-detail file list (restricted to the settled-but-notable statuses: skipped, error, failed, excluded).
Document folder filing
When a file is ingested the sync engine resolves its source folder path and writes it to the document’srelatedFolder as a /-separated breadcrumb (scope-relative, up to
2048 characters). A confirmed scope-root or over-limit resolved path becomes null
(unfiled). An unresolvable path is unknown: a new document starts unfiled, while an
existing or superseded document keeps its previous filing.
The relatedFolderSetBy column ('user' | 'sync' | null) tracks which actor last wrote the folder. Sync only writes when it owns the value: the marker is sync, or both the marker and relatedFolder are null. A user-set folder (via PATCH /api/documents/:id) is never overwritten by sync — even when the source file moves.
See Cloud drive sync for the folder resolution algorithm and the post-deploy backfill procedure.
Document routing and the nullable estimateId
A document’sestimateId is EntityId | null. A synced or project-uploaded document is project-scoped but not yet routed into an estimate — null means “not routed”. Consumers must handle the unrouted case before reading estimate-specific fields.
Routing a document
targetAreas set implies process: true. Processing is skipped (with a RouteProcessingSkipReason) when the document is not in the stored resting state or processing was not requested.
For a linked document, /route with processing requested creates or joins a first_fetch job and reports fetching with its jobId. The job processes the document once the fetched copy scans clean.
api-documents also answers reason: "store_only" when a store-only document is routed with processing requested. That value is not yet in the domain union.
Processing a document
ThePOST /documents/:id/process response has an additional already_processing skip reason that is not present on RouteProcessingResult, and a fetching arm for linked documents:
already_processing is the idempotent lost-response retry case: an earlier /process call already started the pipeline, so the retry settles 200 rather than a 409 conflict. Documents that are genuinely un-processable (not routed, not scan-clean) still surface as a 409 ConflictError.
fetching (HTTP 202) means the document is a linked drive file. The endpoint created or joined a first_fetch job that processes the document once the fetched copy scans clean.
Document sync provenance
Thesync key in a document’s docMetadata jsonb records the sync engine’s provenance for synced files. It is owned exclusively by the sync engine and always written complete.
modifiedAt, modifiedBy and sizeBytes are the “used” half of the version delta. They are absent on rows linked or fetched before link-first; treat absence as unknown when building version labels or deltas.
DocMetadata uses an index signature for the other pipeline-owned keys (summaries, thumbnails, takeoff pages) so each stage can own its own keys without enumerating them all in one type.
Drive source registration and connection creation
Register a drive source
pending_proof with a fresh proof file name. A disabled source must be enabled with PATCH /drive-sources/:id.
Verify a drive source
auth_error or error states.
Create a sync connection
created and reactivated to distinguish the three arms. Neither flag being true means the connection already existed and was already live.
Folder browsing
The mapping picker usesFolderEntry items returned by GET /drive-sources/:id/folders.
Link-first drive sync
stralos-domain carries the link-first wire types. Instead of copying each drive file into S3 during a sync run, the engine writes a linked row with no bytes, and a user’s open fetches the file. For the engine and worker design, see Cloud drive sync.
Document status: linked
linked is a resting status like stored. The row has a reserved S3 key but no object, so it is settled (stop polling) and every serve path fails closed until a fetch job completes. A consumer with an exhaustive switch over its own status subset adds linked when it adopts this type.
Drive link read model
driveLink appears on document list and detail rows. It is null for a document that did not come from a drive.
hasCopy: Stralos holds fetched bytes for this row.fetchVerdict:processablecan be fetched, routed and processed;viewablecan be fetched and downloaded but/processanswers 409;drive_onlyis above the 2 GB ceiling or not downloadable and opens only in the drive.version: the held copy’s version, with a label such asVersion 2 · modified 12 Sep.nullfor a row that holds no copy.versions: present on project-scoped document lists and on document detail, one entry per version of the same drive file.usedBy: the entities using the document, from the usage registry (EntityRefcarriesmodule,entityType,entityId,labelandfrozen).newerInDrive: the drive’s latest version when it is newer than this row’s.blockedReasonis set when a fetch of that version was refused.activeFetch: set while a fetch job is live.
Fetch jobs
first_fetch fills the linked row as Version 1, refetch refills an evicted row at its own version, and new_version adds Version N+1 as a new row beside the old one. resultDocumentId is the row the bytes land in and is null until the worker chooses it. processingOutcome records what requested processing did once the fetched copy scanned clean; awaiting_conversion is a DWG waiting for its PDF.
GET /documents/drive-fetches/:jobId returns { job: DriveFetchJob; download?: DocumentDownload }. download ({ url, expiresAt }) is present only when the job is ready.
Opening a document
state. fetching is HTTP 202 and the rest are 200. decision_required lists the non-frozen entities that use the document and must choose to keep or sync. versionCheck: "unavailable" means the live version check timed out or failed and Stralos served its copy anyway.
Version deltas and drive-version actions
/drive-version answers 409 with version_changed, no_newer_version, entity_frozen, or document_superseded. sync adds the drive’s newer version beside the one the entity used. The entity is required and must not be frozen, and an estimate entity gets the new version routed into it. fetch_version fetches the version without routing it. A 409 version_changed carries latest, and document_superseded carries currentDocumentId.
Prefetch
POST /documents/prefetch takes { entityType, entityId } and queues fetch jobs for the entity’s linked documents when the entity is first opened. It returns 202 { queued, joined, skipped: [{ documentId, reason }] }. Limits are in src/sync/linkFirstPolicy.ts:
- Per-entity cooldown (30 minutes). A repeat call for the same entity inside the window returns
202 { queued: 0, joined: 0, skipped: [] }. It records no newdrive_prefetch_requestedevent, so the window does not slide. - Organisation budget (100 jobs per hour). When the budget is spent, the call returns
429 prefetch_throttledwithretryAfterSeconds. Only this limit returns429. - Per call (25 jobs). Documents beyond the per-call or remaining hourly budget are skipped with
prefetch_limit.
Usage registry
DocumentUsagesChanged v1 event detail carries module, entityType, entityId, label (string | null), projectId (required on the event), frozen, usageVersion (monotonic per entity), and usages: an array of { documentId, purpose, count }, where purpose is a DocumentUsagePurpose (for example "routed", "takeoff_measurement", or "work_item_source"). api-estimate declares the publisher side in src/events/documentUsages.ts; api-documents validates the set with usageSetSchema in src/usage/registry.ts.
usages is the full current set for the entity; a document absent from it is no longer used. The consumer applies an event only when usageVersion is above the value it holds, then replaces that module’s rows for the entity. On the estate bus the detail also carries the envelope fields version: 1, eventId, occurredAt, orgId, and optional actorId and correlationId.
PUT /documents/usages/:module/:entityType/:entityId accepts the same body for a module without a bus publisher, under a service:<module> token only. There, label and projectId are optional and nullable. It returns { applied: boolean }.
An acknowledgement records a per-entity decision about one flag:
POST /documents/usages/:entityType/:entityId/acknowledgements takes { documentId, kind, versionMarker? } and returns 404 no_such_usage or 409 version_changed on conflict. DELETE …/acknowledgements/:documentId/:kind (204) shows the flag again.
Usage changes
GET /documents/usages/:entityType/:entityId/changes is the generic module contract. The estimate banner, submit check, item flags and Variations panel read it.
DocumentUsageSource carries the document’s driveLink, the delta, any newerVersion already in Stralos, removedFromDrive, the entity’s acknowledged kinds, and three flags:
promptWorthy: the entity is not frozen, the source is newer in the drive or has a newer Stralos version, and it has not been kept out.flagItems: the module should flag the items that came from this document.possibleVariation: the entity is frozen and the source changed or was removed.
Processing recommendations
POST /documents/projects/:projectId/processing-recommendations asks the AI to suggest which documents to process, from file names and folder paths only. Nothing is ticked or processed until the user confirms through /route.

