Skip to content

R2 or local storage

Choose Cloudflare R2 (S3-compatible) or local disk for workspace media uploads; configure env vars and optional browser direct uploads.

7 min read

Connect your agent today

Draft from chat, review in your calendar, and publish only what you approve.

Start for $0

Storage backends

OpenQuok can store workspace media (account media library, composer attachments, and similar) in one of two ways:

OptionBest forNotes
Cloudflare R2Production and teams that want durable object storageS3-compatible API; optional public hostname and bucket CORS if the browser uploads directly to R2.
Local diskLocal development or a single-machine deploy without object storageFiles live under UPLOAD_DIRECTORY on the API host; the API serves them at /uploads/*. No R2 credentials.

Avatars and blog inline images still use Supabase Storage (separate buckets), regardless of this choice.

Pick one STORAGE_PROVIDER value for the API (r2 = object storage, local = disk). That is where the API stores files. Separately, the web app sets VITE_MEDIA_LIBRARY_UPLOAD to choose how the browser uploads (direct multipart to R2, via API to disk, or full file through the API). See Web environment variables.

Cloudflare R2

Create account and open R2

Sign in to the Cloudflare dashboard, then open R2 Object Storage. If this is your first time using Cloudflare for this domain, finish the domain onboarding and DNS setup first so you can later attach a custom hostname such as media.yourdomain.com.

Setup Domain

Create a bucket

Create a new R2 bucket. For OpenQuok, this bucket stores user-uploaded or generated files in R2.

Create a Bucket

Copy your Bucket Name for later.

Create your R2 token

  • In Cloudflare, go to Storage & DatabaseR2 Object StorageOverview. Then look at Account Details

R2 Object Storage

Copy your Account ID for later, then choose Create API token and create an Account API token.

Create account api Token

Under Permissions, grant Object Read & Write. Under bucket scope, choose only the bucket you created for OpenQuok media uploads.

Set permissions

Copy these values for the backend:

  • STORAGE_R2_ACCOUNT_ID
  • STORAGE_R2_ACCESS_KEY_ID
  • STORAGE_R2_SECRET_ACCESS_KEY
  • STORAGE_R2_BUCKET

Add backend keys

Set the R2 credentials in backend/.env.development.local (or your production secret store). Use STORAGE_R2_REGION = APAC.

STORAGE_PROVIDER=r2
STORAGE_R2_ACCOUNT_ID=your_account_id
STORAGE_R2_ACCESS_KEY_ID=your_r2_access_key_id
STORAGE_R2_SECRET_ACCESS_KEY=your_r2_secret_access_key
STORAGE_R2_BUCKET=your_bucket_name
STORAGE_R2_REGION=WNAM

Restart the backend after changing these values.

Configure Custom Domain and CORS policies

Go to configuration and connect a custom domain. If you do not have one yet, you can use the Cloudflare-managed public URL first. If you are still setting up the zone in Cloudflare, follow the earlier Create account and open R2 step before attaching a hostname.

Add the public hostname to your env file:

STORAGE_R2_PUBLIC_BASE_URL=https://customdomain.com

Add backend and web public URL envs

Set both apps to the same public origin (no trailing slash):

VITE_STORAGE_R2_PUBLIC_BASE_URL=https://customdomain.com

Use backend/.env.development.local for the backend value and web/.env.development.local for the web value. Restart both the backend and Vite after changes.

Web uploader (account media library)

When the API uses STORAGE_PROVIDER = r2, set the web app so the media library uses direct multipart uploads to R2 (presigned URLs from the API):

VITE_MEDIA_LIBRARY_UPLOAD=direct

Synonyms for VITE_MEDIA_LIBRARY_UPLOAD=direct: multipart, presigned. If VITE_MEDIA_LIBRARY_UPLOAD is omitted, it defaults to VITE_MEDIA_LIBRARY_UPLOAD=direct.

Configure bucket CORS

Click to edit the CORS policy and add the following JSON if you plan to upload directly from the browser to R2 (multipart presigned URLs):

[
  {
    "AllowedOrigins": [
      "https://localhost:5173",
      "http://localhost:5173",
      "https://www.openquok.com"
    ],
    "AllowedMethods": [
      "GET",
      "POST",
      "HEAD",
      "PUT",
      "DELETE"
    ],
    "AllowedHeaders": [
      "Authorization",
      "content-type",
      "x-amz-*"
    ],
    "ExposeHeaders": [
      "ETag",
      "Location"
    ],
    "MaxAgeSeconds": 3600
  }
]

If uploads go only through the backend API (XHR to the API), bucket CORS is optional for that path.

Cors

Local disk storage

Use this when you do not want to configure R2 (typical local development).

Backend

  1. Set an absolute directory on the machine running the API. The process must be able to create and write files there.
STORAGE_PROVIDER=local
UPLOAD_DIRECTORY=/absolute/path/to/openquok-uploads
  1. Set FRONTEND_DOMAIN_URL (or BACKEND_DOMAIN_URL) so public URLs for saved media resolve correctly. The API builds public URLs as <origin>/uploads/<storage-key> (same origin as the app in dev is fine, e.g. https://localhost:5173).

  2. Restart the API. When STORAGE_PROVIDER is local, the server exposes files under /uploads from UPLOAD_DIRECTORY.

Web app (account media)

For direct-to-R2 multipart uploads, the browser must talk to R2 (bucket CORS). For local storage, uploads should go through the API instead.

Set a web env var so the account media uploader uses the server upload flow (XHR to the API) instead of presigned R2 URLs:

VITE_MEDIA_LIBRARY_UPLOAD=local

When local storage is not enough

Local disk is a single-host solution. For production scale, redundancy, and CDN-friendly URLs, use R2 (or another object store) and keep STORAGE_PROVIDER=r2.

Overview

OpenQuok stores workspace media objects in Cloudflare R2 when STORAGE_PROVIDER is r2 and the R2 variables are set. With STORAGE_PROVIDER = local, objects are stored on disk under UPLOAD_DIRECTORY and served at /uploads/*.

Avatars and blog inline images continue to use Supabase Storage (avatars, blog_images).

Backend environment variables

Set these in backend/.env.development.local (or your host secret store). Restart the API after changes.

R2 (STORAGE_PROVIDER=r2)

VariablePurpose
STORAGE_PROVIDERSet to r2 (default in many setups).
STORAGE_R2_ACCOUNT_IDCloudflare account id (R2 overview).
STORAGE_R2_ACCESS_KEY_IDR2 API token access key id.
STORAGE_R2_SECRET_ACCESS_KEYR2 API token secret.
STORAGE_R2_BUCKETBucket name.
STORAGE_R2_REGIONOptional; default APAC (S3 client region for R2).
STORAGE_R2_PUBLIC_BASE_URLOptional; public origin for objects (no trailing slash). Use your bucket custom domain or r2.dev URL, not the S3 API endpoint https://<accountId>.r2.cloudflarestorage.com.
STORAGE_PROVIDER=r2
STORAGE_R2_ACCOUNT_ID=your_account_id
STORAGE_R2_ACCESS_KEY_ID=your_r2_access_key_id
STORAGE_R2_SECRET_ACCESS_KEY=your_r2_secret_access_key
STORAGE_R2_BUCKET=your_bucket_name
STORAGE_R2_REGION=WNAM
STORAGE_R2_PUBLIC_BASE_URL=https://media.yourdomain.com

Local disk (STORAGE_PROVIDER=local)

VariablePurpose
STORAGE_PROVIDERSet to local.
UPLOAD_DIRECTORYAbsolute path where uploaded files are written.
FRONTEND_DOMAIN_URLUsed to build public /uploads/… URLs (no trailing slash).
STORAGE_PROVIDER=local
UPLOAD_DIRECTORY=/absolute/path/to/openquok-uploads
FRONTEND_DOMAIN_URL=https://localhost:5173

Web environment variables

Set these in web/.env.development.local (or your production web env). Restart Vite after changes. See also Vite (SvelteKit).

Do not confuse backend STORAGE_PROVIDER r2 (where the API stores objects) with browser upload mode VITE_MEDIA_LIBRARY_UPLOAD=direct (how the client sends bytes).

VariableBrowser uploadValues
VITE_MEDIA_LIBRARY_UPLOADHow the browser sends files. Default if unset: direct.VITE_MEDIA_LIBRARY_UPLOAD=direct — presigned multipart to R2 (needs bucket CORS). VITE_MEDIA_LIBRARY_UPLOAD=local — XHR to the API for disk storage (use with STORAGE_PROVIDER=local). VITE_MEDIA_LIBRARY_UPLOAD=api — full multipart POST to /api/v1/media/upload through the API (no R2 CORS; higher load on the server).
VITE_STORAGE_R2_PUBLIC_BASE_URLPublic URLs for R2-hosted objectsSame origin as your bucket custom domain (no trailing slash).

Example for R2 + direct browser uploads:

VITE_MEDIA_LIBRARY_UPLOAD=direct
VITE_STORAGE_R2_PUBLIC_BASE_URL=https://media.yourdomain.com

Example for local disk (with backend STORAGE_PROVIDER=local):

VITE_MEDIA_LIBRARY_UPLOAD=local
Search documentation
Find a docs page
Discord Support