{"_id":"@atherlabs/convex-object-storage","name":"@atherlabs/convex-object-storage","dist-tags":{"latest":"0.8.0"},"versions":{"0.8.0":{"name":"@atherlabs/convex-object-storage","description":"An object storage component for Convex supporting R2 and S3.","repository":{"type":"git","url":"git+https://github.com/sipherxyz/convex-object-storage.git"},"homepage":"https://github.com/sipherxyz/convex-object-storage#readme","bugs":{"url":"https://github.com/sipherxyz/convex-object-storage/issues","email":"tung.nguyen@atherlabs.com"},"version":"0.8.0","license":"Apache-2.0","keywords":["convex","component","object-storage","s3","r2","cloudflare","aws"],"type":"module","exports":{"./package.json":"./package.json",".":{"import":{"@convex-dev/component-source":"./src/client/index.ts","types":"./dist/esm/client/index.d.ts","default":"./dist/esm/client/index.js"},"require":{"@convex-dev/component-source":"./src/client/index.ts","types":"./dist/commonjs/client/index.d.ts","default":"./dist/commonjs/client/index.js"}},"./react":{"import":{"@convex-dev/component-source":"./src/react/index.ts","types":"./dist/esm/react/index.d.ts","default":"./dist/esm/react/index.js"},"require":{"@convex-dev/component-source":"./src/react/index.ts","types":"./dist/commonjs/react/index.d.ts","default":"./dist/commonjs/react/index.js"}},"./svelte":{"import":{"@convex-dev/component-source":"./src/svelte/index.ts","types":"./dist/esm/svelte/index.d.ts","default":"./dist/esm/svelte/index.js"},"require":{"@convex-dev/component-source":"./src/svelte/index.ts","types":"./dist/commonjs/svelte/index.d.ts","default":"./dist/commonjs/svelte/index.js"}},"./convex.config":{"import":{"@convex-dev/component-source":"./src/component/convex.config.ts","types":"./dist/esm/component/convex.config.d.ts","default":"./dist/esm/component/convex.config.js"}}},"peerDependencies":{"convex":"~1.16.5 || >=1.17.0 <1.35.0","convex-svelte":"^0.0.11","react":"^18 || ^19","react-dom":"^18 || ^19","svelte":">=5.0.0"},"devDependencies":{"@eslint/js":"^9.9.1","@types/node":"^18.17.0","@types/react":"^18.3.3","convex-test":"^0.0.33","eslint":"^9.9.1","globals":"^15.9.0","prettier":"3.2.5","typescript":"~5.8.0","typescript-eslint":"^8.4.0","vitest":"^3.2.4"},"main":"./dist/commonjs/client/index.js","types":"./dist/commonjs/client/index.d.ts","module":"./dist/esm/client/index.js","dependencies":{"@aws-sdk/client-s3":"^3.726.1","@aws-sdk/s3-request-presigner":"^3.726.1","@convex-dev/action-retrier":"^0.2.1","convex-helpers":"^0.1.104","file-type":"^20.4.1","uuid":"^11.1.0"},"scripts":{"build":"npm run build:esm && npm run build:cjs","build:esm":"tsc --project ./esm.json && echo '{\\n  \"type\": \"module\"\\n}' > dist/esm/package.json","build:cjs":"tsc --project ./commonjs.json && echo '{\\n  \"type\": \"commonjs\"\\n}' > dist/commonjs/package.json","dev":"cd example; npm run dev","typecheck":"tsc --noEmit"},"_id":"@atherlabs/convex-object-storage@0.8.0","_integrity":"sha512-PPEIb3KGRthtYZ58DGrJWDERGwKj+WLiBG5rMV4PdKtsntKRCyAAYQEl4p19zqajl+3MIuExwEnTT2SGD5PCRw==","_resolved":"/private/var/folders/l3/dzzpcscx5j964m1c_d8jff6h0000gn/T/e7c471d047ebdfe3797cb5b94b5a74f8/atherlabs-convex-object-storage-0.8.0.tgz","_from":"file:atherlabs-convex-object-storage-0.8.0.tgz","_nodeVersion":"22.17.1","_npmVersion":"10.9.2","dist":{"integrity":"sha512-PPEIb3KGRthtYZ58DGrJWDERGwKj+WLiBG5rMV4PdKtsntKRCyAAYQEl4p19zqajl+3MIuExwEnTT2SGD5PCRw==","shasum":"9cb3d8888fc627850bd6aac3558b5b8361602967","tarball":"https://registry.npmjs.org/@atherlabs/convex-object-storage/-/convex-object-storage-0.8.0.tgz","fileCount":90,"unpackedSize":246952,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIAk5vaBcbah0AsMVWw7FtcN1O7FbLjUvAsK//HC3YzJKAiBvnshnW6NDXnWhKf/tSZJenxZGBc1rOdIsWyIsP3awfw=="}]},"_npmUser":{"name":"sipher.admin","email":"tung.nguyen@sipher.xyz"},"directories":{},"maintainers":[{"name":"sipher.admin","email":"tung.nguyen@sipher.xyz"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/convex-object-storage_0.8.0_1758452324556_0.8632595209559271"},"_hasShrinkwrap":false}},"time":{"created":"2025-09-21T10:58:44.467Z","0.8.0":"2025-09-21T10:58:44.810Z","modified":"2025-09-21T10:58:45.092Z"},"maintainers":[{"name":"sipher.admin","email":"tung.nguyen@sipher.xyz"}],"description":"An object storage component for Convex supporting R2 and S3.","homepage":"https://github.com/sipherxyz/convex-object-storage#readme","keywords":["convex","component","object-storage","s3","r2","cloudflare","aws"],"repository":{"type":"git","url":"git+https://github.com/sipherxyz/convex-object-storage.git"},"bugs":{"url":"https://github.com/sipherxyz/convex-object-storage/issues","email":"tung.nguyen@atherlabs.com"},"license":"Apache-2.0","readme":"# Convex Object Storage Component\n\n[![npm version](https://badge.fury.io/js/@convex-dev%2Fobject-storage.svg)](https://badge.fury.io/js/@convex-dev%2Fobject-storage)\n\n<!-- START: Include on https://convex.dev/components -->\n\nStore and serve files with **Cloudflare R2** or **AWS S3**.\n\n```ts\n// Works with both R2 and S3!\nimport { useUploadFile } from \"@convex-dev/object-storage/react\";\n\n// Upload files from React\nconst uploadFile = useUploadFile(api.example);\n// ...in a callback\nconst key = await uploadFile(file);\n\n// Access files on the server\nconst url = await storage.getUrl(key);\nconst response = await fetch(url);\n```\n\n**Check out the [example app](example) for a complete example.**\n\n## Prerequisites\n\nChoose your provider and follow the setup instructions:\n\n### Option A: Cloudflare R2\n\n- [Create a Cloudflare account](https://cloudflare.com)\n- [Create an R2 bucket](https://developers.cloudflare.com/r2/buckets/create-buckets/)\n- [Add a CORS policy](https://developers.cloudflare.com/r2/buckets/cors/#add-cors-policies-from-the-dashboard) to the bucket allowing GET and PUT requests from your\n  Convex app. You can also use '\\*' to allow all origins (use with caution).\n  ```json\n  [\n    {\n      \"AllowedOrigins\": [\"http://localhost:5173\"],\n      \"AllowedMethods\": [\"GET\", \"PUT\"],\n      \"AllowedHeaders\": [\"Content-Type\"]\n    }\n  ]\n  ```\n- Create an API token\n  - On the main R2 page in your Cloudflare dashboard, click **Manage R2 API\n    Tokens**\n  - Click **Create API Token**\n  - Edit the token name\n  - Set permissions to **Object Read & Write**\n  - Under **Specify bucket**, select the bucket you created above\n  - Optionally change TTL\n  - Click **Create API Token**\n- On the next screen you'll be provided with values you'll need for environment variables\n\n### Option B: AWS S3\n\n- [Create an AWS account](https://aws.amazon.com)\n- [Create an S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-bucket.html)\n- [Configure CORS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html) on your bucket to allow uploads from your Convex app\n- [Create IAM credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) with S3 permissions for your bucket\n- Get your bucket name, region, access key ID, and secret access key\n\n### Convex App\n\nYou'll need a Convex App to use the component. Follow any of the [Convex quickstarts](https://docs.convex.dev/home) to set one up.\n\n## Installation\n\nInstall the component package:\n\n```sh\nnpm install @convex-dev/object-storage\n```\n\nCreate a `convex.config.ts` file in your app's `convex/` folder and install the component by calling `use`:\n\n```ts\n// convex/convex.config.ts\nimport { defineApp } from \"convex/server\";\nimport objectStorage from \"@convex-dev/object-storage/convex.config\";\n\nconst app = defineApp();\napp.use(objectStorage);\n\nexport default app;\n```\n\n## Configuration\n\nSet your credentials using environment variables. The component supports multiple prefixes:\n\n### For Cloudflare R2:\n\n```sh\nnpx convex env set R2_BUCKET your-bucket-name\nnpx convex env set R2_ENDPOINT https://your-account-id.r2.cloudflarestorage.com\nnpx convex env set R2_ACCESS_KEY_ID your-access-key-id\nnpx convex env set R2_SECRET_ACCESS_KEY your-secret-access-key\n```\n\n### For AWS S3:\n\n```sh\nnpx convex env set S3_BUCKET your-bucket-name\nnpx convex env set S3_REGION us-east-1\nnpx convex env set S3_ACCESS_KEY_ID your-access-key-id\nnpx convex env set S3_SECRET_ACCESS_KEY your-secret-access-key\n```\n\nThe component auto-detects the provider based on your environment variables, or you can specify it explicitly in code.\n\n## Uploading files\n\nFile uploads to object storage typically use signed urls. The component provides hooks for React and Svelte that handle the entire upload process:\n\n- generates the signed url\n- uploads the file to your storage provider (R2 or S3)\n- stores the file's metadata in your Convex database\n\n1. Instantiate an object storage client in a file in your app's `convex/` folder:\n\n   ```ts\n   // convex/example.ts\n   import { ObjectStorage } from \"@convex-dev/object-storage\";\n   import { components } from \"./_generated/api\";\n\n   export const storage = new ObjectStorage(components.objectStorage);\n\n   export const { generateUploadUrl, syncMetadata } = storage.clientApi({\n     checkUpload: async (ctx, bucket) => {\n       // const user = await userFromAuth(ctx);\n       // ...validate that the user can upload to this bucket\n     },\n    onUpload: async (ctx, bucket, key) => {\n      // ...do something with the key\n      // This technically runs in the `syncMetadata` mutation, as the upload\n      // is performed from the client side. Will run if using the `useUploadFile`\n      // hook, or if `syncMetadata` function is called directly. Runs after the\n      // `checkUpload` callback.\n     },\n   });\n   ```\n\n2. Use the `useUploadFile` hook in your component to upload files:\n\n   React:\n\n   ```tsx\n   // src/App.tsx\n   import { FormEvent, useRef, useState } from \"react\";\n   import { useAction } from \"convex/react\";\n   import { api } from \"../convex/_generated/api\";\n   import { useUploadFile } from \"@convex-dev/object-storage/react\";\n\n   export default function App() {\n     // Passing the entire api exported from `convex/example.ts` to the hook.\n     // This must include `generateUploadUrl` and `syncMetadata` from the storage client api.\n     const uploadFile = useUploadFile(api.example);\n     const imageInput = useRef<HTMLInputElement>(null);\n     const [selectedImage, setSelectedImage] = useState<File | null>(null);\n\n     async function handleUpload(event: FormEvent) {\n       event.preventDefault();\n\n       // The file is uploaded to object storage, metadata is synced to the database, and the\n       // key of the newly created object is returned.\n       await uploadFile(selectedImage!);\n       setSelectedImage(null);\n       imageInput.current!.value = \"\";\n     }\n     return (\n       <form onSubmit={handleUpload}>\n         <input\n           type=\"file\"\n           accept=\"image/*\"\n           ref={imageInput}\n           onChange={(event) => setSelectedImage(event.target.files![0])}\n           disabled={selectedImage !== null}\n         />\n         <input\n           type=\"submit\"\n           value=\"Upload\"\n           disabled={selectedImage === null}\n         />\n       </form>\n     );\n   }\n   ```\n\n   Svelte:\n\n   ```svelte\n   <script lang=\"ts\">\n      import { useUploadFile } from \"@convex-dev/object-storage/svelte\";\n      import { api } from \"../convex/_generated/api\";\n\n      const uploadFile = useUploadFile(api.example);\n\n      let selectedImage = $state<File | null>(null);\n\n      async function handleUpload(file: File) {\n        await uploadFile(file);\n        selectedImage = null;\n      }\n    </script>\n\n    <form\n      onsubmit={() => {\n        if (selectedImage) handleUpload(selectedImage);\n      }}\n    >\n      <input\n        type=\"file\"\n        accept=\"image/*\"\n        onchange={(e) => {\n          selectedImage = e.currentTarget.files?.[0] ?? null;\n        }}\n        disabled={selectedImage !== null}\n      />\n      <button type=\"submit\" disabled={selectedImage === null}> Upload </button>\n    </form>\n   ```\n\n### Using a custom object key\n\nThe `storage.generateUploadUrl` function generates a uuid to use as the object key by\ndefault, but a custom key can be provided if desired. Note: the `generateUploadUrl`\nfunction returned by `storage.clientApi` does not accept a custom key, as that\nfunction is a mutation to be called from the client side and you don't want your\nclient defining your object keys. Providing a custom key requires making your\nown mutation that calls the `generateUploadUrl` method of the `storage` instance.\n\n```ts\n// convex/example.ts\nimport { ObjectStorage } from \"@convex-dev/object-storage\";\nimport { components } from \"./_generated/api\";\n\nexport const storage = new ObjectStorage(components.objectStorage);\n\n// A custom mutation that creates a key from the user id and a uuid. If the key\n// already exists, the mutation will fail.\nexport const generateUploadUrlWithCustomKey = mutation({\n  args: {},\n  handler: async (ctx) => {\n    // Replace this with whatever function you use to get the current user\n    const currentUser = await getUser(ctx);\n    if (!currentUser) {\n      throw new Error(\"User not found\");\n    }\n    const key = `${currentUser.id}.${crypto.randomUUID()}`;\n    return storage.generateUploadUrl(key);\n  },\n});\n```\n\n## Storing Files from Actions\n\nFiles can be stored in object storage directly from actions using the `storage.store` method. This is useful when you need to store files that are generated or downloaded on the server side.\n\n```ts\n// convex/example.ts\nimport { internalAction } from \"./_generated/server\";\nimport { ObjectStorage } from \"@convex-dev/object-storage\";\n\nconst storage = new ObjectStorage(components.objectStorage);\n\nexport const store = internalAction({\n  handler: async (ctx) => {\n    // Download a random image from picsum.photos\n    const url = \"https://picsum.photos/200/300\";\n    const response = await fetch(url);\n    const blob = await response.blob();\n\n    // This function call is the only required part, it uploads the blob to object storage,\n    // syncs the metadata, and returns the key. The key is a uuid by default, but\n    // an optional custom key can be provided in the options object. A MIME type\n    // can also be provided, which will override the type inferred for blobs.\n    const key = await storage.store(ctx, blob, {\n      key: \"my-custom-key\",\n      type: \"image/jpeg\",\n    });\n\n    // Example use case, associate the key with a record in your database\n    await ctx.runMutation(internal.example.insertImage, { key });\n  },\n});\n```\n\nThe `store` method:\n\n- Takes a `Blob`, `Buffer`, or `Uint8Array` and stores it in object storage\n- Syncs metadata to your Convex database\n- Returns the key that can be used to access the file later\n\n## Serving Files\n\nFiles stored in object storage can be served to your users by generating a URL pointing to a given file.\n\n### Generating file URLs in queries\n\nThe simplest way to serve files is to return URLs along with other data required by your app from queries and mutations.\n\nA file URL can be generated from a object key by the `storage.getUrl` function of the\nobject storage component client.\n\n```ts\n// convex/listMessages.ts\nimport { components } from \"./_generated/api\";\nimport { query } from \"./_generated/server\";\nimport { ObjectStorage } from \"@convex-dev/object-storage\";\n\nconst storage = new ObjectStorage(components.objectStorage);\n\nexport const list = query({\n  args: {},\n  handler: async (ctx) => {\n    // In this example, messages have an imageKey field with the object key\n    const messages = await ctx.db.query(\"messages\").collect();\n    return Promise.all(\n      messages.map(async (message) => ({\n        ...message,\n        imageUrl: await storage.getUrl(\n          message.imageKey,\n          // Options object is optional, can be omitted\n          {\n            // Custom expiration time in seconds, default is 900 (15 minutes)\n            expiresIn: 60 * 60 * 24, // 1 day\n          }\n        ),\n      }))\n    );\n  },\n});\n```\n\nFile URLs can be used in img elements to render images:\n\n```tsx\n// src/App.tsx\nfunction Image({ message }: { message: { url: string } }) {\n  return <img src={message.url} height=\"300px\" width=\"auto\" />;\n}\n```\n\n## Deleting Files\n\nFiles stored in object storage can be deleted from actions or mutations via the `storage.deleteObject` function, which accepts an object key.\n\n```ts\n// convex/images.ts\nimport { v } from \"convex/values\";\nimport { mutation } from \"./_generated/server\";\nimport { ObjectStorage } from \"@convex-dev/object-storage\";\n\nconst storage = new ObjectStorage(components.objectStorage);\n\nexport const deleteObject = mutation({\n  args: {\n    key: v.string(),\n  },\n  handler: async (ctx, args) => {\n    return await storage.deleteObject(ctx, args.key);\n  },\n});\n```\n\n## Accessing File Metadata\n\nFile metadata can be accessed from actions via `storage.getMetadata`:\n\n```ts\n// convex/images.ts\nimport { v } from \"convex/values\";\nimport { query } from \"./_generated/server\";\nimport { ObjectStorage } from \"@convex-dev/object-storage\";\n\nconst storage = new ObjectStorage(components.objectStorage);\n\nexport const getMetadata = query({\n  args: {\n    key: v.string(),\n  },\n  handler: async (ctx, args) => {\n    return await storage.getMetadata(ctx, args.key);\n  },\n});\n```\n\nThis is an example of the returned document:\n\n```json\n{\n  \"ContentType\": \"image/jpeg\",\n  \"ContentLength\": 125338,\n  \"LastModified\": \"2024-03-20T12:34:56Z\"\n}\n```\n\nThe returned document has the following fields:\n\n- `ContentType`: the ContentType of the file if it was provided on upload\n- `ContentLength`: the size of the file in bytes\n- `LastModified`: the last modified date of the file\n\n### Listing and paginating metadata\n\nMetadata can be listed from actions via `storage.listMetadata`.\n\n```ts\n// convex/example.ts\nimport { query } from \"./_generated/server\";\nimport { ObjectStorage } from \"@convex-dev/object-storage\";\n\nconst storage = new ObjectStorage(components.objectStorage);\n\nexport const list = query({\n  args: {\n    limit: v.optional(v.number()),\n  },\n  handler: async (ctx, args) => {\n    return storage.listMetadata(ctx, args.limit);\n  },\n});\n```\n\n### Accessing metadata after upload\n\nThe `onSyncMetadata` callback can be used to run a mutation after every metadata\nsync. The `useUploadFile` hook syncs metadata after every upload, so this\nfunction will run each time as well.\n\nBecause this runs after metadata sync, the `storage.getMetadata` can be used to\naccess the metadata of the newly uploaded file.\n\n```ts\n// convex/example.ts\nimport { ObjectStorage, type ObjectStorageCallbacks } from \"@convex-dev/object-storage\";\nimport { components } from \"./_generated/api\";\n\nexport const storage = new ObjectStorage(components.objectStorage);\n\nconst callbacks: ObjectStorageCallbacks = internal.example;\n\nexport const { generateUploadUrl, syncMetadata, onSyncMetadata } = storage.clientApi(\n  {\n    // Pass the functions from this file back into the component.\n    // Technically only an object with `onSyncMetadata` is required, the recommended\n    // pattern is just for convenience.\n    callbacks,\n\n    onSyncMetadata: async (ctx, args) => {\n      // args: { bucket: string; key: string; isNew: boolean }\n      // args.isNew is true if the key did not previously exist in your Convex\n      // metadata table\n      const metadata = await storage.getMetadata(ctx, args.key);\n      // log metadata of synced object\n      console.log(\"metadata\", metadata);\n    },\n  }\n);\n```\n\n<!-- END: Include on https://convex.dev/components -->\n","readmeFilename":"README.md","_rev":"1-7a98d306562910389a5ca311ec3b46ed"}