VibeAny Docs

Storage and AI Image Generation

Configure S3-compatible storage and image providers with their security, credit, and persistence boundaries.

Object storage and AI image generation are separate integrations. Image generation can run without storage, but durable generated-image URLs require both.

Object storage configuration

The storage selector accepts r2, s3, or minio and composes the same S3-compatible adapter.

Cloudflare R2 example:

STORAGE_PROVIDER=r2
S3_ENDPOINT=https://account-id.r2.cloudflarestorage.com
S3_ACCESS_KEY_ID=replace_with_access_key_id
S3_SECRET_ACCESS_KEY=replace_with_secret_access_key
S3_BUCKET_NAME=vibeany-files
S3_REGION=auto
S3_PUBLIC_URL=https://cdn.example.com

The four endpoint/credential/bucket values are a complete set. With none of them, runtime storage is disabled. A partial set is rejected as incomplete. npm run preflight also treats a configured STORAGE_PROVIDER as an intent to enable storage, so remove or comment that variable when deliberately postponing storage setup.

S3_PUBLIC_URL is optional for private-only use, but it is strongly recommended for public files. The adapter does not create a public bucket policy, custom domain, object ACL, or private prefix policy for you. The access value only controls whether the application returns a public URL or a signed URL; it does not change provider-side object visibility.

Do not mix sensitive private objects into a bucket that is anonymously exposed through S3_PUBLIC_URL. Keep the whole bucket private when every object is private. If the product needs both public and private objects, use separate public and private buckets, or implement and verify an equivalent provider/CDN policy that blocks anonymous access to every private prefix. The starter currently configures one bucket, so a two-bucket design requires an application extension.

Prepare an R2 or S3-compatible bucket

Before enabling browser uploads:

  1. create the bucket;
  2. create credentials limited to the required bucket operations;
  3. allow your application origins and required PUT headers in bucket CORS;
  4. configure a public/custom domain only for objects intended to be public;
  5. verify upload, metadata lookup, download, and deletion against the real provider.

The repository's Docker Compose file does not start MinIO, and the current adapter has no S3_FORCE_PATH_STYLE setting. Treat MinIO as an integration to configure and verify, not as a bundled zero-configuration service.

Browser upload flow

Authenticated uploads use three steps:

  1. POST /api/storage/upload with filename, contentType, contentLength, and optional folder and access;
  2. upload the bytes directly to the returned presigned URL with PUT and the same content type;
  3. POST /api/storage/confirm with the returned fileId.

The confirm step checks that the object exists and that size and content type match the pending database record.

Defaults:

SettingDefault
Accessprivate
Upload URL lifetime5 minutes
Private download URL lifetime1 hour
Maximum file size10 MB
Allowed typesJPEG, PNG, GIF, WebP, SVG, PDF
Folderuploads

Private objects use user-scoped keys and the application returns signed download URLs. That response behavior is not a storage isolation guarantee. Public uploads use unguessable object names by default and return S3_PUBLIC_URL/<key> when a public base URL is configured.

The default content-type allowlist includes SVG. Treat untrusted SVG as active content: either remove SVG from the accepted types or serve it from an isolated asset origin with restrictive response headers and no application cookies.

Deleting a file is not transactional across object storage and PostgreSQL. The current adapter treats provider deletion as best-effort, so production systems should monitor and reconcile orphaned objects. A repeated upload confirmation returns a conflict rather than an idempotent success.

Enable image generation

NEXT_PUBLIC_ENABLE_IMAGE_GENERATION=true

# Optional explicit selector: fal, modelscope, or dashscope
IMAGE_PROVIDER=fal
FAL_KEY=replace_with_provider_key

# Alternatives
# MODELSCOPE_API_KEY=replace_with_provider_key
# DASHSCOPE_API_KEY=replace_with_provider_key

If IMAGE_PROVIDER is unset, selection priority is:

  1. Fal
  2. ModelScope
  3. DashScope

An explicit selector must have its matching credential. Otherwise preflight fails and generation is unavailable. Only configured providers participate in execution and fallback.

FAL_KEY, MODELSCOPE_API_KEY, and DASHSCOPE_API_KEY are server-side provider secrets and must not be exposed to browser code.

The public surfaces are:

  • page: /{locale}/demo/image-generator;
  • API: POST /api/generate-image.

The API returns 404 when the feature flag is off, 401 for an unauthenticated request, 402 for insufficient credits, 400 for invalid input, and 500 when generation cannot be completed.

Credits, retries, and persistence

  • A request can generate 1–4 images.
  • Each image costs 10 credits.
  • Credits are deducted atomically before calling the model provider.
  • If provider generation fails, the full request cost is added back as a refund transaction.
  • The router retries retryable failures and can try another configured provider.
  • Invalid requests and authentication errors are not intentionally retried.

After successful generation, the application attempts to fetch each remote image and save it as a public object under the generated folder. This persistence step is best-effort:

  • if storage is disabled, the provider URL is returned;
  • if fetching or saving fails, the provider URL is returned;
  • a storage failure does not refund credits because image generation succeeded;
  • provider URLs may be temporary, so configure storage before promising durable galleries.

Verify the integration

npm run preflight
npm run test:unit
npm run test:credit-ledger:integration
npm run build

Then verify in the real runtime:

  1. the disabled route is hidden and returns 404;
  2. a configured provider generates 1 and 4 images;
  3. insufficient balance returns 402 without a negative balance;
  4. a simulated provider failure restores the deducted credits;
  5. generated files are readable through the configured public domain;
  6. storage failure falls back to the provider URL and is visible in logs;
  7. a direct anonymous request for a private object's key returns 403 or 404 through every public/CDN domain; if it does not, move private data to a separate private bucket before launch.

See Customize the Starter for catalog and provider extension rules.

On this page