Self-hosting
Object storage
SeaweedFS, S3, R2 and other S3-compatible stores, bring-your-own bucket per workspace, and region pinning.
critlab stores bytes — package files, voice notes, screenshots and thumbnails — in any S3-compatible object store, and everything else in Postgres. It uses the S3 API only, so the same install runs on SeaweedFS, AWS S3, Cloudflare R2, Ceph RGW or MinIO.
The platform bucket
Every install has one bucket, configured on web and sandbox with the same values:
| Variable | Example |
|---|---|
S3_ENDPOINT | http://seaweedfs:9000, https://s3.eu-central-1.amazonaws.com, https://<account>.r2.cloudflarestorage.com |
S3_BUCKET | critlab |
S3_REGION | us-east-1 (default); your bucket's region on AWS |
S3_ACCESS_KEY, S3_SECRET_KEY | the key pair, kept in a Secret |
S3_FORCE_PATH_STYLE | true (default) for SeaweedFS, MinIO and most self-hosted stores; false for AWS virtual-hosted addressing |
Create the bucket before first start; critlab does not create it (the Helm chart's bundled SeaweedFS has a job that does). Keep the bucket private. Browsers never talk to it directly — web and sandbox fetch and stream every object after their own access checks — so it needs no public access and no CORS rules.
Package files are stored under their SHA-256 content hash, once per bucket, so publishing a new version only uploads the files that changed. Files no version references any more are removed by a garbage-collection sweep after a grace period.
Which store to choose
| Store | Use it when |
|---|---|
| SeaweedFS | The default for self-hosted: one container (weed server -s3), bundled by the chart and the Compose file. |
| Ceph RGW (e.g. through Rook) | You need a highly available store on your own hardware. |
| AWS S3, Cloudflare R2 | You already run in a cloud. |
| MinIO | You already run it. Its community edition is archived upstream, so critlab no longer bundles it. |
Bring your own bucket per workspace
A workspace on a licensed install can send its own uploads to a bucket it owns. An owner configures it in Settings → Storage (or crit storage configure, crit storage verify):
- Save the endpoint, bucket, region, optional prefix and key pair. Nothing is routed yet.
- Verify runs a probe:
HeadBucket, then writes, reads back byte-for-byte and deletes 16 random bytes undercritlab-verify/, then lists with the prefix. Each step reports the store's own error (AccessDenied,NoSuchBucket,SignatureDoesNotMatch…). - From the moment it passes, new uploads land in that bucket. If a later check fails, uploads are refused, never silently redirected to the platform bucket.
Grant the key at least s3:GetObject, s3:PutObject, s3:DeleteObject on the bucket (or prefix) and s3:ListBucket. Every stored object records which bucket it lives in, so disconnecting or switching buckets later never moves or strands existing files: old versions keep reading from where they were written. critlab never deletes objects from a bucket you own, on disconnect or otherwise.
To move a workspace's existing files into its new bucket, run on a machine with access to both:
pnpm --filter @critlab/web storage:migrate <workspace-slug> --dry-run
pnpm --filter @critlab/web storage:migrate <workspace-slug>It copies what is missing, then re-points rows only once every byte is in place; it is safe to re-run.
Operator settings for workspace buckets
STORAGE_SECRETencrypts the stored key pairs (AES-256-GCM; falls back toAUTH_SECRET). Rotating it invalidates every stored credential — each workspace must re-enter its keys.STORAGE_ALLOW_PRIVATE_ENDPOINTS=1on web and sandbox lets a workspace bucket live on a private network address. Off by default, because the endpoint is a URL the server signs requests to. Cloud metadata addresses stay blocked either way.- The sandbox gets a workspace bucket's credentials from web over
/api/internal/storage-target, authenticated with the internal lookup secret, which must match on both (INTERNAL_LOOKUP_SECRET, elseDOMAIN_LOOKUP_SECRET, elseAUTH_SECRET). SetINTERNAL_ORIGINso the sandbox reaches web on the internal network.
Region pinning
Each workspace has a data region (EU first). When an eu workspace connects a bucket in a us-* region, uploads are refused with region_mismatch unless the owner explicitly allows cross-region storage. This pins stored files only: comments, transcripts and workspace records live in whichever Postgres your deployment runs, which on a self-hosted install is wherever you put it. The settings page states the region as, for example, "EU (storage: eu-central-1)".