Logo VitNode
Documentation

Storage

Storage

Upload, serve, and delete files in VitNode with pluggable storage adapters - local disk, AWS S3, Cloudflare R2, or Supabase Storage.

VitNode provides an integrated storage layer accessible via c.get("storage"). It handles file validation, automatic WebP image optimization, database indexing in core_files, and persistent storage across multiple backends.

Quick start

1. Register a Storage Adapter

In apps/api/src/vitnode.api.config.ts, configure the local disk adapter:

apps/api/src/vitnode.api.config.ts
import { LocalStorageAdapter } from "@vitnode/core/api/adapters/storage/local"
import { buildApiConfig } from "@vitnode/core/vitnode.config"

export const vitNodeApiConfig = buildApiConfig({
  storage: {
    adapter: LocalStorageAdapter(), 
  },
})

2. Upload Files in an API Route

Handle uploads in a Hono route using c.get("storage").upload():

plugins/blog/src/api/modules/posts/routes/upload-cover.route.ts
import { z } from "@hono/zod-openapi"

import { buildRoute } from "@/api/builders"

export const uploadCoverRoute = buildRoute({
  route: {
    method: "post",
    path: "/cover",
    request: {
      body: {
        content: {
          "multipart/form-data": {
            schema: z.object({
              file: z.instanceof(File),
            }),
          },
        },
      },
    },
  },
  handler: async (c) => {
    const { file } = await c.req.parseBody()

    const uploaded = await c.get("storage").upload({
      file: file as File,
      folder: "covers",
      maxBytes: 5 * 1024 * 1024, // 5 MB
      allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"],
    })

    return c.json(uploaded)
  },
})

upload() saves the file, creates a record in core_files, and returns { id, url, width, height, size }.


Supported Storage Adapters


Image Optimization

Turn on storage.image and VitNode re-encodes JPEG, PNG, AVIF, TIFF and WebP uploads with sharp, converts them to WebP, and records their pixel dimensions:

apps/api/src/vitnode.api.config.ts
export const vitNodeApiConfig = buildApiConfig({
  storage: {
    adapter: LocalStorageAdapter(),
    image: {
      quality: 85, // 1-100, defaults to 85
      webp: true, // set to false to keep the original format
      maxPixels: 40_000_000, // width x height, defaults to 40 million
    },
  },
})

maxPixels is your guard against image bombs - a tiny PNG that claims to be 16383x16383 pixels and unpacks into gigabytes of memory. Anything over the limit is refused with a 400 before a single pixel is decoded. The default fits an 8K photo with room to spare, so most installs never need to touch it.

What gets checked on every upload

Whether or not storage.image is set, an upload declared as JPEG, PNG, WebP, GIF, AVIF or TIFF must actually be that format. The type a client sends is just a claim (a browser guesses it from the file name), so VitNode reads the file's first bytes and refuses a mismatch - say, an HTML page named cat.gif - with a 400. SVG and non-image files have no fixed signature and are not affected; limit them with allowedMimeTypes.


Deleting Files

Remove files and clean up storage using delete():

await c.get("storage").delete({ fileId: 42 })

This removes the file from the configured storage bucket and deletes its row from core_files.


Verifying in AdminCP

Test your storage adapter anytime in the AdminCP:

  1. Navigate to System → Integrations (/admin/core/system/integrations).
  2. On the Storage card, click Test Storage to verify bucket credentials and upload capabilities.
  3. Inspect uploaded files anytime under System → Files (/admin/core/system/files).

Learn More