Skip to main content
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 a linked 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

The DriveSourceProvider type is a closed set matching the registered provider abstraction keys.

Drive source

A DriveSource 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.
The raw proof nonce is never projected as its own API or domain field. While a source is pending_proof, registration returns a derived proofFileName so the administrator can complete verification.

Sync connection

A SyncConnection 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

A SyncRun 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’s relatedFolder 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’s estimateId 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

The POST /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

The sync 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

The endpoint is idempotent: registering the same source URL returns the existing live source. A soft-deleted source is reactivated in pending_proof with a fresh proof file name. A disabled source must be enabled with PATCH /drive-sources/:id.

Verify a drive source

Used for initial proof-of-control confirmation and for recovery from auth_error or error states.

Create a sync connection

The response is a three-way discriminated union reflecting the three possible settled outcomes:
Narrow on 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 uses FolderEntry items returned by GET /drive-sources/:id/folders.
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. 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: processable can be fetched, routed and processed; viewable can be fetched and downloaded but /process answers 409; drive_only is above the 2 GB ceiling or not downloadable and opens only in the drive.
  • version: the held copy’s version, with a label such as Version 2 · modified 12 Sep. null for 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 (EntityRef carries module, entityType, entityId, label and frozen).
  • newerInDrive: the drive’s latest version when it is newer than this row’s. blockedReason is 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

Discriminate on 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 new drive_prefetch_requested event, so the window does not slide.
  • Organisation budget (100 jobs per hour). When the budget is spent, the call returns 429 prefetch_throttled with retryAfterSeconds. Only this limit returns 429.
  • Per call (25 jobs). Documents beyond the per-call or remaining hourly budget are skipped with prefetch_limit.

Usage registry

A 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.
Each 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.