Self-hosting

Storage: S3, MinIO, R2

Configure object storage for uploads, generated assets, exports and snapshots — the bundled MinIO, AWS S3, Cloudflare R2 or any S3-compatible store — including CORS and public URLs.

On this page

Desyn keeps file bytes in object storage and only metadata in the database. Browsers upload directly to the store with short-lived presigned URLs, and downloads use signed URLs, so files never pass through the web app.

What is stored#

PrefixContents
uploads/Images, SVGs, fonts, video and audio members upload
AI resultsGenerated and transformed images, SVGs and video
exports/Rendered export files (cleaned up automatically after they expire)
snapshots/<designId>/<seq>.json.zstCompressed design snapshots used for fast loading and version history
ThumbnailsDesign and template previews

Assets no design references any more are removed by a scheduled cleanup job.

Variables#

VariableNotes
S3_BUCKETBucket name
AWS_REGIONBucket region; any value such as us-east-1 for stores without regions, auto for R2
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYCredentials with read, write, list and delete on the bucket
S3_ENDPOINTUnset for AWS S3; the store's URL otherwise
S3_FORCE_PATH_STYLEtrue for MinIO and most self-hosted stores
S3_PUBLIC_BASE_URLOptional public/CDN base for files that may be public

Set them in the Convex environment (deploy/compose/convex.env in the quickstart). See the configuration reference.

CORS#

Because browsers upload directly, the bucket must allow your app's origin. The bundled MinIO is configured for this automatically (MINIO_API_CORS_ALLOW_ORIGIN follows DESYN_APP_URL); for any other store, apply rules like these:

cors.json
{
  "CORSRules": [
    {
      "AllowedOrigins": ["https://design.example.org"],
      "AllowedMethods": ["GET", "PUT", "HEAD"],
      "AllowedHeaders": ["*"],
      "ExposeHeaders": ["ETag"],
      "MaxAgeSeconds": 3600
    }
  ]
}
Shell
aws --endpoint-url "$S3_ENDPOINT" s3api put-bucket-cors --bucket desyn --cors-configuration file://cors.json

Omit --endpoint-url for AWS S3. Without CORS, uploads fail in the browser with a network error while everything else works.

Provider notes#

The quickstart stack runs MinIO (minio service, data in the minio_data volume) and creates the bucket on start (minio-init). The root credentials come from MINIO_ROOT_USER and MINIO_ROOT_PASSWORD in deploy/compose/.env; the backend uses the same pair:

deploy/compose/convex.env
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=desyn
AWS_SECRET_ACCESS_KEY=<MINIO_ROOT_PASSWORD>
S3_BUCKET=desyn
S3_ENDPOINT=https://s3.example.org
S3_PUBLIC_BASE_URL=https://s3.example.org/desyn
S3_FORCE_PATH_STYLE=true

Route s3.example.org to 127.0.0.1:9000 at the reverse proxy. The MinIO console is on 127.0.0.1:9001; reach it through an SSH tunnel.

Public files#

Without S3_PUBLIC_BASE_URL, every file is served through signed URLs that expire, which is the safest default. If you set it — typically to a CDN in front of the bucket — files that are public by nature, such as template thumbnails, use plain URLs and cache better. Members' uploads and designs stay private either way.

Upload limits#

The maximum size per file comes from your plan configuration (uploadMbPerFile, 1 GB for self-hosted). If your reverse proxy also fronts the storage endpoint, allow request bodies at least that large there — see reverse proxy.

Popular pages

QuickstartGet started
Keyboard shortcutsEditor
The agentAI
Docker Compose quickstartSelf-hosting
Configuration referenceSelf-hosting
Agent API (v1)Developers