Storage and tenancy
Where bytes land — one database file and one object prefix per workspace, two blob backends, a ciphertext cache and fail-closed scanning.
One workspace, one file, one prefix
Each workspace is its own SQLite database under DATA_DIR and its own key prefix in the blob store. Isolation is physical rather than a WHERE clause: there is no shared table for a query to cross. The control database holds organizations, memberships, tokens, the wrapped workspace keys and the audit trail — never workspace content.
A service or agent token is minted for one workspace and refused if presented to another. A personal token and an AI client's OAuth grant act for their person instead: every request is re-scoped to the workspace it addresses, checked against that person's live membership there and capped at the member role, so a token can never carry more than the person's membership would.
The server keeps a pool of open tenant handles (VITRINKA_TENANT_POOL_MAX, 64 by default); a workspace is opened on first use and its key unwrapped at that moment.
Blob backends
BLOB_BACKEND | Where blobs live | Notes |
|---|---|---|
fs (default) | DATA_DIR/blobs | One volume holds everything; back it up with the databases. |
s3 | The bucket named by S3_BUCKET at S3_ENDPOINT | All five S3_* variables are required and validated at boot; the bucket is created if missing. |
Either way every object is written sealed under the workspace key (Encryption). With S3, a read-through cache under DATA_DIR/blobcache (BLOB_CACHE_MAX_GB, 20 by default, 0 disables) is pre-warmed at boot and evicted oldest-first; its entries are the same ciphertext the bucket holds.
Size limits
MAX_SET_MB (200 by default) caps both the request body and the unpacked size of a published set. Image and document decoding is limit-guarded, so a decompression bomb is refused rather than expanded.
Malware scanning
Set CLAMD_ADDR to a clamd daemon and every blob write is scanned before it is persisted, fail-closed: a scanner that cannot answer refuses the write rather than storing an unscanned file. CLAMD_MAX_MB (100 by default) is the largest file the scanner accepts; larger uploads are refused, never persisted unscanned. When CLAMD_ADDR is unset, no scanning happens.
The hosted service pins a clamd sidecar in its compose. A self-hosted deployment that wants the same posture runs its own daemon on the same network — the address is the only configuration.