Confluye
Features

Workspace Files

Immutable scanned blobs with a three-step upload, byte-range download, and deferred deletion.

Workspace Files are workspace-owned blobs. Metadata is created first (pending), bytes are written once, completion verifies size and SHA-256, then a scanner must mark the object ready before download or attachment. Content is not rewritten in place.

The browser uses /api/files with a signed-in session and workspaceId. API clients use /api/v1/files with a workspace API key. Both path prefixes share the same handlers.

Upload lifecycle

statusMeaning
pendingMetadata exists. Only this state accepts PUT .../content.
pending_scanBytes persisted and checksums match. A scan lease is required before ready.
readyScanner returned clean. Download, Command attachments, and presentation inputs require this.
quarantinedScanner returned infected. Content cannot be opened.
deletion_requestedSoft-delete. Blob remains until GC. New references are rejected.
deletedGC removed the blob and marked the row.

Create (POST) with name, mimeType, sizeBytes (0–100 MiB), optional sha256 (64 hex). The response includes uploadUrl and completeUrl derived from the request path (session URLs keep ?workspaceId=).

Then PUT the raw body to .../{id}/content. Content-Length above 100 MiB is 413 before the body is stored. Missing body: 400. Only pending files accept content.

Then POST .../{id}/complete with optional sizeBytes / sha256. The handler completes the upload and, while status is pending_scan, runs scanFile in the same request. HTTP status is 202 with the serialized file (often ready after a clean scan). Replay of the same Idempotency-Key returns the stored 202 without a second scan.

The UI client polls while any listed file is pending_scan. Only ready files are selectable as chat attachments.

Authorization

SurfacePrincipalWorkspace id
/api/filesBrowser sessionQuery workspaceId, else the session workspace
/api/v1/filesWorkspace API key (Bearer)Always the key's workspace

Session access is membership-checked. A workspaceId the user does not belong to is rejected. v1 missing/invalid bearer: 401 authentication_required. Personal or copilot key: 403 authorization_failed (A workspace API key is required.). A file id in another workspace is 404 (Workspace file {id} was not found.).

Preview capabilities: files.list, files.get, files.read (bounded chunk, default 32 KiB, max 64 KiB), files.delete (confirmation and idempotency required). files.delete also asserts write readiness.

Storage and scanning

Default blob store is S3 (WORKSPACE_FILE_BLOB_STORE unset or s3). postgres is allowed only outside NODE_ENV=production. Production requires the private S3 store.

S3 profiles: minio (local), railway (non-local Confluye), aws (legacy, rejected on Confluye deployments). Minio/railway require endpoint and credentials. Railway rejects WORKSPACE_FILES_S3_SSE. Object key is the file id (optional WORKSPACE_FILES_S3_PREFIX).

Scanner: WORKSPACE_FILE_SCANNER clamav (default in production) or noop (non-production only). ClamAV uses CLAMAV_HOST (default 127.0.0.1), CLAMAV_PORT (3310), CLAMAV_TIMEOUT_MS (30s). Scan lease default is 5 minutes; a stale running scan is marked Scanner lease expired. before a new claim. Infected → quarantined. Scanner exception → scan row error and the HTTP complete fails.

encryptionKeyId is stored as null on create. Serialized file objects omit storage keys.

Download

GET .../{id}/content returns the raw stream (not the JSON envelope): 200 or 206 with accept-ranges: bytes, content-type from metadata, content-disposition attachment (quotes and newlines in the name become _), etag "sha256-{hex}". Range: bytes={start}-{end} only (one explicit range). Non-ready files: 423 conflict.

Deletion and GC

DELETE .../{id} is idempotent at the service layer: already deletion_requested or deleted returns the current row. HTTP 202. Blobs are not removed in that request.

GC (runWorkspaceFileGc): orphans older than 7 days with no live reference (lease null or unexpired) in pending / pending_scan / ready / quarantined are marked deletion_requested. Rows already deletion_requested for 30 days have the blob deleted then status deleted. runDueWorkspaceFileGc runs only when the deployment environment is production, at most once per UTC day after a complete batch (default batch 100).

Active references (run, message, document, skill, mail, artifact, research_source) block GC mark/remove. Command job attachments use resourceType: "run" with a default 24h lease.

Idempotency and retries

Mutations require header Idempotency-Key (withApiIdempotency, required: true). Missing or invalid key: 400 idempotency_key_invalid. Same key, different body (including file id on complete/content/delete): 409 idempotency_replay_mismatch. Successful replay sets Idempotency-Replayed: true and does not re-execute.

Blob put accepts the same key for content uploads. Complete of an already pending_scan or ready file is a no-op unless completion metadata conflicts (409 size/checksum mismatch).

Degradation

ConditionBehavior
Preview/production primary S3 or ClamAV probe fails503; retryable true for files_primary_store_unreachable / files_scanner_unreachable
Config missing (S3 parse fail or scanner not ClamAV+host)Availability blocked (or setup_required when local)
Production backup not ready or restore not approvedCapability blocked (files_backup_store_not_configured / files_restore_test_required)
Infected scanquarantined; open content 423
Active scan leaseCannot scan a file in … / already has an active scan (409)
Content before persistCannot complete … Upload content has not been persisted. (409)

Next steps