Skip to content

File Storage & Avatars

LaunchKit Pro includes workspace-scoped file storage with LOCAL storage by default and a provider-ready foundation for S3-compatible storage, Cloudflare R2, and Cloudinary. Profile avatars continue to work through the same backend storage service.

Local storage provider

SettingDefaultDescription
STORAGE_PROVIDERLOCALActive provider
STORAGE_UPLOAD_DIRuploadsDirectory on server disk
STORAGE_PUBLIC_BASE_URLhttp://localhost:4000/uploadsPublic base URL for local public assets
STORAGE_MAX_FILE_SIZE_MB25System upload ceiling
STORAGE_ALLOWED_MIME_TYPEScommon image/pdf/text/zip typesServer allow-list

Production example:

env
STORAGE_UPLOAD_DIR=/var/www/yourapp/shared/uploads
STORAGE_PUBLIC_BASE_URL=https://api.yourdomain.com/uploads

Production uploads directory

Use a shared folder outside release directories (e.g. /var/www/yourapp/shared/uploads) so deploys do not wipe uploaded files. The API process user must read/write this path.

Ensure the API process user can read/write this directory.

Do not ship buyer upload folders in marketplace ZIPs — uploads are created at runtime on the server.

Public asset URLs

LOCAL public assets are served by the API:

https://api.yourdomain.com/uploads/...

The frontend resolves asset URLs using NEXT_PUBLIC_API_URL. Example avatar URL:

https://api.yourdomain.com/uploads/avatars/<filename>.webp

Cross-origin loading from the app domain requires the API to allow browser access to /uploads (LaunchKit Pro sets appropriate static asset headers for avatar display).


Profile avatars

Frontend crop and resize

Before upload, the dashboard profile page (/dashboard/profile) opens a crop dialog:

  • User selects JPEG, PNG, or WebP
  • Square 1:1 crop with pan and zoom
  • Client exports 512×512 WebP (or JPEG fallback) at ~0.85 quality
  • Smaller payloads load faster in profile and topbar

API upload

bash
curl -s -X POST http://localhost:4000/profile/avatar \
  -H "Authorization: Bearer <accessToken>" \
  -F "avatar=@./avatar.webp"
ItemValue
MethodPOST /profile/avatar
Field nameavatar
Allowed types (client + server)JPEG, PNG, WebP

After upload, the avatar appears on the profile page and dashboard topbar. Initials fallback is used when no avatar is set or the image fails to load.

Display sizes (frontend)

LocationSize
Profile page128×128 (rounded)
Dashboard / admin topbar40×40 (rounded)

Fixed dimensions and object-fit: cover prevent layout shift.


Provider status

bash
curl -s http://localhost:4000/storage/provider-status \
  -H "Authorization: Bearer <accessToken>" | jq

The response includes only safe fields: provider, configured flag, upload limits, signed URL TTL, and a friendly message. It never exposes access keys or secrets.

Upload

bash
curl -s -X POST http://localhost:4000/workspaces/<workspaceId>/files \
  -H "Authorization: Bearer <accessToken>" \
  -F "file=@./sample.pdf" \
  -F "category=DOCUMENT" | jq

Requires files.upload permission.

Flat API aliases are also available for frontend and API testing:

bash
curl -s -X POST http://localhost:4000/storage/upload \
  -H "Authorization: Bearer <accessToken>" \
  -F "workspaceId=<workspaceId>" \
  -F "file=@./sample.pdf" \
  -F "category=DOCUMENT" | jq

List and download

bash
curl -s "http://localhost:4000/workspaces/<workspaceId>/files?page=1&limit=20" \
  -H "Authorization: Bearer <accessToken>" | jq

curl -s "http://localhost:4000/storage/files?workspaceId=<workspaceId>" \
  -H "Authorization: Bearer <accessToken>" | jq

curl -s http://localhost:4000/storage/files/<fileId>/signed-url \
  -H "Authorization: Bearer <accessToken>" | jq

curl -s -O -J http://localhost:4000/workspaces/<workspaceId>/files/<fileId>/download \
  -H "Authorization: Bearer <accessToken>"

Delete

bash
curl -s -X DELETE http://localhost:4000/workspaces/<workspaceId>/files/<fileId> \
  -H "Authorization: Bearer <accessToken>"

Requires files.delete. Demo mode blocks delete for protected demo users.

Allowed file types

Backend validates MIME types and categories per workspace storage config. Common categories: DOCUMENT, IMAGE, GENERAL, etc.

Check effective config:

bash
curl -s http://localhost:4000/workspaces/<workspaceId>/storage/config \
  -H "Authorization: Bearer <accessToken>" | jq

Upload size configuration

Effective limit follows:

min(STORAGE_MAX_FILE_SIZE_MB, plan limit, workspace upload setting)

Align:

  • Backend STORAGE_MAX_FILE_SIZE_MB
  • Frontend NEXT_PUBLIC_MAX_UPLOAD_SIZE_MB
  • Nginx client_max_body_size

See Permissions & Limits.

S3-compatible storage

Set:

env
STORAGE_PROVIDER=S3
S3_ACCESS_KEY_ID=...
S3_SECRET_ACCESS_KEY=...
S3_REGION=...
S3_BUCKET=...
S3_ENDPOINT=
S3_FORCE_PATH_STYLE=false
S3_PUBLIC_BASE_URL=

Only S3 settings are validated when S3 is active or used. Inactive provider keys may remain empty.

Cloudflare R2

env
STORAGE_PROVIDER=R2
R2_ACCESS_KEY_ID=...
R2_SECRET_ACCESS_KEY=...
R2_ACCOUNT_ID=...
R2_BUCKET=...
R2_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
R2_PUBLIC_BASE_URL=

R2 uses S3-compatible signing with region auto.

Cloudinary

env
STORAGE_PROVIDER=CLOUDINARY
CLOUDINARY_CLOUD_NAME=...
CLOUDINARY_API_KEY=...
CLOUDINARY_API_SECRET=...
CLOUDINARY_FOLDER=launchkit-pro

Cloudinary uploads use resource type auto. Signed/private download support is a foundation: the API returns a provider URL where supported and a friendly error if a provider is not configured.

Signed URLs and cleanup

env
STORAGE_SIGNED_URL_TTL_SECONDS=900
STORAGE_CLEANUP_ENABLED=false
STORAGE_ORPHAN_MAX_AGE_DAYS=7

GET /storage/files/:fileId/signed-url returns a public URL for public files or a signed/provider URL for private files after access checks. POST /storage/cleanup/orphans is admin-only and dry-run by default; it reports counts and does not delete files unless future cleanup behavior is explicitly enabled.

Provider secrets stay backend-only. The frontend only calls authenticated API routes.

Provider migration note

Switching from LOCAL to cloud affects new uploads. Migration of existing local files to S3/R2/Cloudinary is future work; keep local uploads backed up until you migrate them.

Backup note (local uploads)

Local files in LOCAL_UPLOAD_DIR are not in PostgreSQL backups. Include upload directory in:

  • Filesystem backups (rsync, snapshots)
  • Disaster recovery runbooks

If you only backup the database, uploaded files will be lost on restore.

LaunchKit Pro by LaunchKit Labs — buyer documentation. No secrets in this site.