Skip to content
LazyFrog

Files and uploads

Where an app on LazyFrog keeps uploaded and generated files: a file store that survives publishes, public file links and an S3-compatible API.

  • /data is the environment's file store: it survives restarts and publishes, and is in every snapshot. Files under /data/public/ are served to everyone at /files/<path> (5-minute cache). Other files are private.
  • An S3-compatible API over /data is at S3_ENDPOINT (http://s3.local), bucket S3_BUCKET, keys S3_ACCESS_KEY/S3_SECRET_KEY, region S3_REGION; the AWS SDK variables are set too, so new S3Client({ forcePathStyle: true }) or boto3.client("s3") work with no options. An object's key is its path under /data (public/avatars/1.png is /files/avatars/1.png).
  • Visitor uploads and private downloads: presign on the server with a second client whose endpoint is S3_PUBLIC_ENDPOINT (the site's own address + /_lf/s3) and forcePathStyle: true; the browser then PUTs or GETs that URL on the same origin (no CORS). A test or app on another origin (an e2e suite at http://localhost:4173) is refused until routing.json has a cors rule for /_lf/s3/* allowing that origin (a preview-only test origin is fine; the URL's signature stays the credential), or presign through the app's own origin. Presigned URLs last at most 15 minutes (expiresIn ≤ 900). Each request is capped at 100,000,000 bytes (95 MiB) at the edge: use multipart with parts of 64 MiB or less (@aws-sdk/lib-storage Upload, boto3 upload_fileobj) for larger files. Sign the type (signableHeaders: new Set(["content-type"])) and upload visitors' files to a private key, then check them on the server (type, size, re-encode images) and copy them to a new final key (under public/ or private) that you serve: a presigned PUT can't cap the size, and the upload key stays writable until its URL expires, so never serve the key that was uploaded to. Files served from /files never run as the site (they are sandboxed, and HTML downloads).
  • A deleted or replaced public file stays in the edge cache up to its max-age (5 minutes unless the object or routing sets another): to take a photo down at once, give it a new name and stop linking the old one. Content-hashed names can be cached for a year.
  • /files/* and /_lf/* are the platform's; an app's own /files/... route still works for any path with no file in /data/public/.
  • The user's photos, and images your app made in the chat, come in with fs_import (see "Their own photos and files"). An image your app's image generation made always takes generated: true (anything you created, not a file the user attached), or it is recorded as the user's own photo; the same file again with generated: true corrects that.

For your AI: this page as Markdown · llms.txt