Skip to content

storage-imagekit

Package @theoven/storage-imagekit
Adds to context ctx.storage — it is a driver for storage
Endpoints none
Creates files whatever you upload, in your media library
Creates tables none
Status shipped
Terminal window
bun add @theoven/storage @theoven/storage-imagekit
src/app.ts
import { storage } from '@theoven/storage'
import { imagekitStorage } from '@theoven/storage-imagekit'
const media = imagekitStorage({
privateKey: env.string('IMAGEKIT_PRIVATE_KEY'),
urlEndpoint: env.string('IMAGEKIT_URL_ENDPOINT'),
})
export const app = createApp().use(storage(media))

Puts ImageKit behind the storage contract, so ctx.storage works exactly as it does on S3 or disk. On top of that the driver exposes url(), which builds ImageKit’s transformation URLs — resize, crop, reformat on delivery.

This brick adds no endpoints.

Uploading is ctx.storage.upload() — the same call as every other driver:

src/routes/avatar.post.ts
import { z } from 'zod'
import { media } from '../app'
export const auth = true
export const body = z.object({ file: z.file() })
export default async ({ body, storage, user }) => {
const key = `avatars/${user.id}.png`
await storage.upload(key, body.file)
return { url: media.url(key, { transform: { width: 200, height: 200, format: 'auto' } }) }
}

Upload the original once, then serve whatever size you need from the URL — that is the pattern ImageKit exists for, and why you do not store thumbnails yourself.

await storage.upload('photos/a.png', bytes, { type: 'image/png' })
const blob = storage.download('photos/a.png') // lazy — no request until read
await storage.exists('photos/a.png')
await storage.stat('photos/a.png')
await storage.list({ prefix: 'photos/', limit: 50 })
await storage.delete('photos/a.png')

Full detail on each is on the storage page; they behave the same here.

Nothing in your repository. Files go to your ImageKit media library.

The reason to use ImageKit rather than plain object storage:

media.url('avatars/1.png', { transform: { width: 200, height: 200, format: 'auto' } })
// https://ik.imagekit.io/demo/avatars/1.png?tr=w-200,h-200,f-auto

format: 'auto' lets ImageKit serve webp or avif per browser, which is most of the win.

Deliberately on the driver, not on ctx.storage: resizing an image is not something a storage contract should have an opinion about, and inventing one would mean every other driver pretending it could crop.

media.url('private/report.png', { transform: { width: 800 }, expiresIn: 300 })

The signature covers the transformation as well as the path — so a client cannot edit tr= to request a 10 000px render off a signed URL, which matters on a product billed per transformation.

Option Default
privateKey — required; never send it to a browser
urlEndpoint — required, e.g. https://ik.imagekit.io/your_id

Uploads set useUniqueFileName: false, because ImageKit otherwise appends a random suffix — and then the key you asked for is not the key you get, which silently breaks every other method in the contract.

upload, download, delete, exists, stat, list ✓
media.url() with transformations and signing ✓
presignDownload / presignUpload ✗ — use media.url({ expiresIn }) for signed delivery
directUpload ✗ — depends on presignUpload
Multiple buckets via storage.bucket() ✗ — one media library per driver
  • No browser-direct uploads. ImageKit’s browser upload uses a token/signature triple posted to their endpoint, not a PUT-able URL, so presignUpload is unavailable rather than misleading. Server-side upload() is unaffected — send the file to a route, as in Usage.
  • An extra lookup on remove, exists and stat — see above.
  • Paging is offset-based, so next is a count rather than a key.
  • Uploads are buffered, since the upload API is multipart.

URL building, transformation syntax and signing are deterministic and fully tested — including that changing a transformation changes the signature. Request shapes and the fileId lookup are tested against an injected fetcher.

That ImageKit accepts those requests is not verified: there is no account in CI.