# EdgeStore Docs: Agents URL: https://edgestore.dev/docs/agents Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/agents.mdx ## Give your agent the skill Read [https://edgestore.dev/SKILL.md](https://edgestore.dev/SKILL.md) and add file uploads to this application. For ongoing use, install the plugin or use the CLI below. ## Use a plugin The [repository plugin](https://github.com/edgestorejs/edgestore) bundles the skill and MCP connection for [Codex](https://developers.openai.com/plugins/build/plugins), [Claude Code](https://code.claude.com/docs/en/plugins), and [Cursor](https://cursor.com/docs/plugins). Follow your client's plugin installation instructions, then sign in through its MCP controls. If you use the plugin, skip the CLI setup below. If you already configured MCP directly, disable one of the two connections in your client. ## Set up with the CLI Install the CLI with Node.js 22.22.0 or later: ```sh npm install --global @edgestore/cli@rc ``` Then run this in your application directory: ```sh edgestore agent setup --client codex ``` Use `claude` for Claude Code or `cursor` for Cursor. This installs the setup skill and configures MCP for the project. Add `--skills-only` to skip MCP, or `--global` to install for all projects. Restart your client or open a new task to load the skill. Setup does not sign you in. The skill reads API references bundled with your installed EdgeStore packages. After upgrading the CLI, update the skill with `edgestore agent update --client codex`. This leaves your application packages unchanged. ### MCP only ```sh edgestore mcp setup --client codex ``` This configures account and project tools without installing the skill. ## Configure manually Add EdgeStore to your client's project configuration. Merge the entry into an existing config rather than replacing other servers. <> ```toml title=".codex/config.toml" [mcp_servers.edgestore] url = "https://api.edgestore.dev/mcp" ``` ```json title=".mcp.json" { "mcpServers": { "edgestore": { "type": "http", "url": "https://api.edgestore.dev/mcp" } } } ``` ```json title=".cursor/mcp.json" { "mcpServers": { "edgestore": { "url": "https://api.edgestore.dev/mcp" } } } ``` ### Sign in Restart your client after saving the config, then connect EdgeStore: * Codex requires project trust for `.codex/config.toml`. Run `codex mcp login edgestore`, then use `/mcp` to check the connection. * In Claude Code, approve the project's MCP configuration, then use `/mcp` to select EdgeStore and sign in. * In Cursor, open MCP settings and connect the EdgeStore server. MCP sign-in is separate from `edgestore login`. After signing in, ask your agent to list your EdgeStore projects to check tool access. Client instructions: [Codex](https://learn.chatgpt.com/docs/extend/mcp), [Claude Code](https://code.claude.com/docs/en/mcp), and [Cursor](https://prod.cursor.com/help/customization/mcp). ## Give your agent the docs Start with [Quick start](/docs/quick-start) or the adapter for [Next.js App or Pages Router](/docs/adapters/next), [TanStack Start](/docs/adapters/tanstack-start), [Remix / React Router](/docs/adapters/remix), [Astro](/docs/adapters/astro), [Hono](/docs/adapters/hono), [Express](/docs/adapters/express), or [Fastify](/docs/adapters/fastify). React-only applications need a backend for the server adapter. * [llms.txt](/llms.txt) lists the docs. * [llms-full.txt](/llms-full.txt) includes all pages in one Markdown response. * Add `.md` to a page URL, such as [quick-start.md](/docs/quick-start.md), or use its Copy Markdown button. Run `edgestore agent context --json` to find installed versions and their local `agent-docs/` references. # EdgeStore Docs: Backend Client URL: https://edgestore.dev/docs/backend-client Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/backend-client.mdx Sometimes you might want to use the EdgeStore functionality directly from your backend. Things like deleting, uploading or even listing files can be done with the use of the backend client. ## Setup The router exposes a type-safe backend client through `router.client`. The HTTP handler and backend client share the same provider. EdgeStore is the default; chain `.provider(myProvider)` on the router to use a different provider. Since Next.js doesn't allow exports in the api route, you will need to move your router to an external file. ```ts title="src/lib/edgestore-server.ts" import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreNextHandler } from '@edgestore/server/adapters/next/app'; const es = initEdgeStore.create(); export const router = es.router({ publicFiles: es.fileBucket(), }); export const handler = createEdgeStoreNextHandler({ router, }); export const backendClient = router.client; ``` Then you will need to update your api route to use the exported handler. ```ts title="src/app/api/edgestore/[...edgestore]/route.ts" import { handler } from '@/lib/edgestore-server'; export { handler as GET, handler as POST }; ``` You can find an example of the backend client usage in the [next-advanced](https://github.com/edgestorejs/edgestore/tree/main/examples/next-advanced) example. The backend client is privileged. It validates router input and applies file type, size, transform, path, and metadata rules, but it does not run `accessControl`, `beforeUpload`, or `beforeDelete`. Perform authorization in the server code that calls it. The hosted `edgestore()` provider supports the complete backend client. S3 and Azure Blob Storage support `upload`, `get`, `delete`, `deleteMany`, and private-read `createSignedUrl` / `createSignedUrls`, accepting key or URL references. Unsupported operations, such as listing and confirmation, remain absent from each provider's client type. ## Backend Upload You can use the `upload` function to upload files from your backend. ### Upload a text file The simplest use case would be to just upload a `txt` file: ```ts const res = await backendClient.publicFiles.upload({ content: 'some text content', }); ``` ### Upload a blob You can also upload a more complex file using the Blob object. And there are also all the other options available in the normal upload. ```ts const res = await backendClient.publicFiles.upload({ content: { blob: new Blob(['col1,col2,col2'], { type: 'text/csv' }), extension: 'csv', }, options: { temporary: true, }, ctx: { userId: '123', userRole: 'admin', }, input: { type: 'post', }, signal, onProgress: ({ percentage, phase }) => { console.log(phase, `${percentage}%`); }, }); console.log(res.id, res.key, res.sizeBytes); ``` ### Copy an existing file You can use an existing file's URL to copy it into the EdgeStore bucket. This can be an external file (from outside of EdgeStore) or an existing EdgeStore file. ```ts const res = await backendClient.publicFiles.upload({ content: { url: 'https://some-url.com/file.txt', extension: 'txt', }, }); ``` ### Transform a file before upload You can transform backend uploads before EdgeStore validates and uploads them. The transform receives the resolved `Blob` and extension, and returns the new `Blob` and extension. For example, you can use `sharp` to convert an image to WebP before upload: npm pnpm yarn bun ```bash npm install sharp ``` ```bash pnpm add sharp ``` ```bash yarn add sharp ``` ```bash bun add sharp ``` ```ts import sharp from 'sharp'; const res = await backendClient.publicImages.upload({ content: { url: 'https://some-url.com/image.jpg', extension: 'jpg', }, options: { transform: async ({ blob }) => { const input = Buffer.from(await blob.arrayBuffer()); const output = await sharp(input).webp({ quality: 80 }).toBuffer(); return { blob: new Blob([output], { type: 'image/webp' }), extension: 'webp', }; }, }, }); ``` ### Confirm a temporary file upload Save the upload result's ID/key and URL in your database, then confirm the temporary file. Hosted EdgeStore accepts confirmation during processing; a preceding `get()` is unnecessary and can return 404 until processing finishes. If saving fails, leave the file temporary. ```ts const res = await backendClient.publicFiles.confirm({ id: file.id, }); ``` File operations accept a stable file ID, storage key, or URL. Singular operations throw `EdgeStoreFileMutationError` when that file fails. Use the plural form when partial success should be preserved: ```ts const result = await backendClient.publicFiles.confirmMany({ refs: [{ id: first.id }, { key: second.key }], }); for (const failure of result.failed) { console.error(failure.ref, failure.error.code); } ``` ## Backend Delete You can use the `delete` function to delete files from your backend. ```ts const res = await backendClient.publicFiles.delete({ id: file.id, }); ``` `deleteMany`, `restore`, and `restoreMany` use the same singular and partial-batch semantics. ## Backend List Files (search) You can use the `list` function to list files from your backend. It's also possible to filter the results by path, metadata or upload timing. ```ts // simple usage // get the first 20 files in the bucket const res = await backendClient.publicFiles.list(); // with filter and pagination const res = await backendClient.publicFiles.list({ filter: { metadata: { role: 'admin', }, path: { type: 'post', }, uploadedAt: { gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7), // past 7 days }, }, cursor: 'cursor-from-previous-response', limit: 50, // default: 20 (max: 100) }); for (const file of res.items) { console.log(file.id, file.url); } if (res.hasMore) { console.log('Next cursor:', res.nextCursor); } ``` ## Private read URLs Providers with signed-read support expose `createSignedUrl` on protected buckets: ```ts const access = await backendClient.privateFiles.createSignedUrl({ url: { id: file.id }, expiresIn: 15 * 60, }); console.log(access.signedUrl, access.expiresAt); ``` Use `createSignedUrls` to sign several references in one provider call. ## Reusing backend client types Derive a single method's exact input or output directly from the configured client: ```ts type UploadInput = Parameters< typeof router.client.publicFiles.upload >[0]; type UploadOutput = Awaited< ReturnType >; ``` This includes the router's context, input, path, and metadata configuration and the provider's file and reference types. Use `InferClientInputs` and `InferClientOutputs` when you need a reusable map of every bucket and method: ```ts title="src/lib/edgestore-server.ts" import type { InferClientInputs, InferClientOutputs, } from '@edgestore/server'; export type BackendInputs = InferClientInputs; export type BackendOutputs = InferClientOutputs; type UploadInput = BackendInputs['publicFiles']['upload']; type UploadOutput = BackendOutputs['publicFiles']['upload']; ``` The helpers infer capabilities and provider-specific types directly from the router, including custom providers. Use `EdgeStoreClient` for the complete backend client type. # EdgeStore Docs: Configuration URL: https://edgestore.dev/docs/configuration Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/configuration.mdx ## Router and Provider Import `initEdgeStore` from `@edgestore/server`. The router uses the hosted EdgeStore provider by default and exposes its backend client as `router.client`: ```ts const es = initEdgeStore.create(); const router = es.router({ publicFiles: es.fileBucket(), }); const handler = createEdgeStoreNextHandler({ router }); const backendClient = router.client; ``` Chain `.provider()` to use another provider or custom credentials: ```ts import { s3 } from '@edgestore/server/providers/s3'; const router = es.router({ publicFiles: es.fileBucket(), }).provider(s3()); ``` `.provider()` returns a new router. It preserves bucket definitions, while leaving existing routers, clients, and handlers bound to their original provider. The backend client exposes only methods supported by the selected provider. The default hosted provider is initialized when handling requests or when `router.client` is first accessed. Defining a router or overriding its provider does not require hosted EdgeStore credentials. Each router reuses its provider and backend client after initialization. Keep the router object on the server; import only its type into frontend code. ## Bucket Types There are two types of file buckets: `IMAGE` and `FILE`. Both types of buckets work basically the same way, but the `IMAGE` bucket only accepts [certain mime types](#image-bucket-accepted-mime-types). IMAGE buckets automatically generate a thumbnail version of the image file if the file is bigger than 200px in width or height. In case a thumbnail was generated, the url will be included in the response of the upload request. ```ts const router = es.router({ publicFiles: es.fileBucket(), publicImages: es.imageBucket(), }); ``` ## Basic File Validation You can set the maximum file size and the accepted mime types for every file bucket. ```ts const router = es.router({ publicFiles: es.fileBucket({ maxSize: 1024 * 1024 * 10, // 10MB accept: ['image/jpeg', 'image/png'], // wildcard also works: ['image/*'] }), }); ``` ## Context Many of the functions that you can use to configure your file buckets receive a `context` object as an argument. This object is generated by the `createContext` function that you pass to your router configuration. ```ts import { initEdgeStore } from '@edgestore/server'; import { type CreateContextOptions, createEdgeStoreNextHandler, } from '@edgestore/server/adapters/next/app'; import { z } from 'zod'; type Context = { userId: string; userRole: 'admin' | 'user'; }; async function createContext({ req }: CreateContextOptions): Promise { const { id, role } = await getUserSession(req); // replace with your own session logic return { userId: id, userRole: role, }; } const es = initEdgeStore.context().create(); // ... export default createEdgeStoreNextHandler({ router, /** * The context is generated and saved to a cookie * in the first load of the page. */ createContext, }); ``` You might need to refresh the context (e.g. when the user logs in or logs out). You can do this by calling the `reset` function from the `useEdgeStore` hook. ```tsx const { edgestore, reset } = useEdgeStore(); async function runAfterAuthChange() { await reset(); // this will re-run the createContext function } ``` ## Metadata & File Path Every uploaded file can hold two types of data: `metadata` and `path`. You can use this data for access control or for filtering files. The `metadata` and `path` can be generated from the context (`ctx`) or from the `input` of the upload request. The `.input()` method accepts any object schema that implements [Standard Schema](https://standardschema.dev/), including schemas from Zod, Valibot, and ArkType. ```ts import { initEdgeStore } from '@edgestore/server'; import { type CreateContextOptions, createEdgeStoreNextHandler, } from '@edgestore/server/adapters/next/app'; import { z } from 'zod'; type Context = { userId: string; userRole: 'admin' | 'user'; }; async function createContext({ req }: CreateContextOptions): Promise { const { id, role } = await getUserSession(req); // replace with your own session logic return { userId: id, userRole: role, }; } const es = initEdgeStore.context().create(); const router = es.router({ publicFiles: es .fileBucket() // this input will be required for every upload request .input( z.object({ category: z.string(), }), ) // e.g. /publicFiles/{category}/{author} .path(({ ctx, input }) => [ { category: input.category }, { author: ctx.userId }, ]) // this metadata will be added to every file in this bucket .metadata(({ ctx, input }) => ({ userRole: ctx.userRole, })), }); ``` ## Lifecycle Hooks You can use the `beforeUpload` and `beforeDelete` hooks to allow or deny file uploads and deletions. The `beforeDelete` hook must be defined if you want to delete files directly from the client. ```ts import { initEdgeStore } from '@edgestore/server'; import { type CreateContextOptions, createEdgeStoreNextHandler, } from '@edgestore/server/adapters/next/app'; import { z } from 'zod'; type Context = { userId: string; userRole: 'admin' | 'user'; }; async function createContext({ req }: CreateContextOptions): Promise { const { id, role } = await getUserSession(req); // replace with your own session logic return { userId: id, userRole: role, }; } const es = initEdgeStore.context().create(); const router = es.router({ publicFiles: es .fileBucket() /** * return `true` to allow upload * By default every upload from your app is allowed. */ .beforeUpload(({ ctx, input, fileInfo }) => { console.log('beforeUpload', ctx, input, fileInfo); return true; // allow upload }) /** * return `true` to allow delete * This function must be defined if you want to delete files directly from the client. */ .beforeDelete(({ ctx, fileInfo }) => { console.log('beforeDelete', ctx, fileInfo); return true; // allow delete }), }); ``` ## Access Control (Experimental) You can use the `accessControl` function to add bucket level logic to allow or deny access to files. If you have ever used Prisma, you will probably notice that the structure of the `accessControl` function is similar to how you would write a Prisma query. If you set the `accessControl` function, your bucket will automatically be configured as a **protected bucket**. You cannot change a protected bucket to a public bucket after it has been created. The opposite is also true, you cannot change a public bucket to a protected bucket. To access files from a **protected bucket** the user will need a specific encrypted cookie generated in your server by the EdgeStore package. Which means that they will only be able to access the files from within your app. Sharing the url of a protected file will not work. The access control check is performed on an edge function without running any database queries, so you won't need to worry about bad performance on your protected files. ```ts const filesBucket = es .fileBucket() .path(({ ctx }) => [{ author: ctx.userId }]) .accessControl({ OR: [ { // this will make sure that only the author of the file can access it userId: { path: 'author' }, }, { // or if the user is an admin userRole: { eq: 'admin', }, // same as { userRole: 'admin' } }, ], }); ``` Other available operators are: `eq`, `not`, `gt`, `gte`, `lt`, `lte`, `in`, `contains` The access control functionality uses a partitioned cookie that EdgeStore sets on the file origin when the provider initializes. Protected files load directly from that origin, including in local development. The `` component from `next/image` does not forward the cookies in the request, so protected images won't be displayed. You will need to use the `` tag instead. ## Limit parallel uploads When creating the provider, you can set the maximum number of concurrent uploads. EdgeStore's context provider will take care of queuing the uploads and will automatically upload the next file when the previous one is finished. ```ts const { EdgeStoreProvider, useEdgeStore } = createEdgeStoreProvider({ maxConcurrentUploads: 5, // default is 5 }); ``` ## Base Path In case your app is not hosted at the root of your domain, you can specify the base path. If you set this, make sure to set the full path to the EdgeStore API. e.g. `/my-app/api/edgestore` or `https://example.com/my-app/api/edgestore` ```tsx export default function App({ Component, pageProps }: AppProps) { return ( ); } ``` ## IMAGE bucket accepted mime types | mime type | | ------------- | | image/jpeg | | image/png | | image/gif | | image/webp | | image/svg+xml | | image/tiff | | image/bmp | | image/x-icon | # EdgeStore Docs: Error Handling URL: https://edgestore.dev/docs/error-handling Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/error-handling.mdx You might need to handle specific server errors in your application. Here is an example of how you can do that. ```tsx import { EdgeStoreApiClientError, UploadAbortedError, UploadCanceledError, UploadProcessingTimeoutError, } from '@edgestore/react/errors'; // ... ``` ## Error Codes * `BAD_REQUEST` * `FILE_TOO_LARGE` * `MIME_TYPE_NOT_ALLOWED` * `UNAUTHORIZED` * `UPLOAD_NOT_ALLOWED` * `DELETE_NOT_ALLOWED` * `CREATE_CONTEXT_ERROR` * `SERVER_ERROR` ## File mutation errors `confirm` and `delete` throw `EdgeStoreFileMutationError` when the provider rejects that file. It carries the provider error `code`, the `message`, and the `fileRef` it applied to. `confirmMany` and `deleteMany` do not throw for per-file failures; inspect the returned `failed` list instead. ```ts import { EdgeStoreFileMutationError } from '@edgestore/react/errors'; try { await edgestore.publicFiles.delete({ url }); } catch (error) { if (error instanceof EdgeStoreFileMutationError) { console.error(error.code, error.fileRef.url); } } ``` `EdgeStoreClientError` reports client-side failures, such as an EdgeStore API route that does not return JSON. # EdgeStore Docs: Logging URL: https://edgestore.dev/docs/logging Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/logging.mdx The EdgeStore package outputs some logs on the server-side. You can configure the log level by passing the `logLevel` option when creating the api handler. You can set it to `debug` to see in more details what is happening in the server. ```ts const handler = createEdgeStoreNextHandler({ logLevel: 'debug', // optional. defaults to 'error' in production and 'info' in development router, }); ``` ## Log Levels * `debug` * `info` * `warn` * `error` * `none` # EdgeStore Docs: Migrate to v1 URL: https://edgestore.dev/docs/migrate-to-v1 Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/migrate-to-v1.mdx EdgeStore v1 is a deliberate major-version redesign. The browser router DX is largely unchanged, while direct API and privileged backend code now use API v2 semantics without a runtime v1 fallback. ## Runtime and packages * All packages are ESM-only and require Node.js 22.22.0 or newer. * Zod is no longer a peer dependency. Bucket `input` accepts any [Standard Schema](https://standardschema.dev) library, including Zod, Valibot, and ArkType. * `@edgestore/server/core` is removed. Import types such as `InferClientOutputs` from `@edgestore/server`. * `@edgestore/react/shared` is removed. Import errors from `@edgestore/react/errors`. ## Configure EdgeStore once The HTTP handler and backend client now share the router's provider. The hosted provider is the default, so the basic handler setup stays the same. Replace `initEdgeStoreClient({ router })` with `router.client`, and configure custom providers by chaining `.provider(myProvider)` on `es.router(...)`. ```ts title="Before" import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreNextHandler } from '@edgestore/server/adapters/next/app'; import { initEdgeStoreClient } from '@edgestore/server/core'; const es = initEdgeStore.create(); const router = es.router({ documents: es.fileBucket(), }); export const handler = createEdgeStoreNextHandler({ router, }); export const backendClient = initEdgeStoreClient({ router, }); ``` ```ts title="After" import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreNextHandler } from '@edgestore/server/adapters/next/app'; const es = initEdgeStore.create(); const router = es.router({ documents: es.fileBucket(), }); export const handler = createEdgeStoreNextHandler({ router, }); export const backendClient = router.client; ``` ## Breaking changes | v0.8 | v1 | Required change | | ------------------------------------------- | -------------------------------------------- | ----------------------------------------------------------- | | Separate handler and client config | `es.router(...).provider(...)` | Configure the router and provider once. | | `initEdgeStoreClient` | `router.client` | Read the client from the configured router. | | `InferClientResponse` | `InferClientOutputs` | Rename the type helper. | | `EdgeStoreProvider()` | `edgestore()` | Import from `@edgestore/server/providers/edgestore`. | | `AWSProvider()` from `providers/aws` | `s3()` from `providers/s3` | Update the import and pass it to `.provider(...)`. | | `AzureProvider()` from `providers/azure` | `azureBlob()` from `providers/azure-blob` | Update the import and pass it to `.provider(...)`. | | S3 `accessKeyId` / `secretAccessKey` | S3 `credentials` | Pass credentials or keep `ES_AWS_*` variables. | | S3 `overwritePath` | S3 `path` | Return a key relative to the router bucket. | | URL-only identity | `{ id }`, `{ key }`, or `{ url }` | Prefer stable IDs in new code. | | Predicted upload result | Canonical processed file | Use `sizeBytes`, `id`, `key`, and the returned timestamps. | | `{ pagination: { currentPage, pageSize } }` | `{ cursor, limit }` | Replace page numbers with explicit cursor continuation. | | `result.data` | `result.items` | Read canonical file records from `items`. | | `{ success: boolean }` | Singular result or partial batch | Catch singular errors or inspect `failed`. | | React `confirmUpload` | React `confirm` | Use the resource-scoped lifecycle name. | | React upload `uploadedAt` | Removed | Read timestamps from the backend client when needed. | | React `disableDevProxy`, `/proxy-file` | Removed | Protected files load from their file origin in development. | | `cookieConfig.token` | Removed | The `edgestore-token` cookie is no longer set. | | Backend `getFile` | Backend `get` | Use the bucket-scoped read name. | | Backend `listFiles` | Backend `list` | Use the bucket-scoped list name. | | Backend `confirmUpload` | Backend `confirm` | Use the singular lifecycle name. | | Backend `confirmUploads` | Backend `confirmMany` | Use the `Many` suffix for batches. | | Backend `deleteFile` | Backend `delete` | Use the singular lifecycle name. | | Backend `deleteFiles` | Backend `deleteMany` | Use the `Many` suffix for batches. | | Backend `restoreFile(s)` | Backend `restore` / `restoreMany` | Use singular and `Many` lifecycle names. | | Backend `getSignedUrl` | Backend `createSignedUrl` | Make signed-URL creation explicit. | | Backend `getSignedUrls` | Backend `createSignedUrls` | Make batch signed-URL creation explicit. | | Nullable metadata values | Nullish values omitted | Treat those inferred keys as optional strings. | | Handcrafted server raw client | `@edgestore/sdk` | Migrate direct API consumers to the public SDK. | | `ES_AZURE_SAS_TOKEN` / `sasToken` | `ES_AZURE_ACCOUNT_KEY` / `storageAccountKey` | Give Azure signing authority only to the server. | | Azure `customBaseUrl` / `ES_AZURE_BASE_URL` | `endpoint` / `ES_AZURE_ENDPOINT` | Use `baseUrl` separately for file URLs, such as a CDN. | Pagination is no longer page-number based. Continue only when your application intentionally needs another page: ```ts title="Before (v0.8)" const page = await backendClient.documents.listFiles({ pagination: { currentPage: 1, pageSize: 50, }, }); ``` ```ts title="After (v1)" const firstPage = await backendClient.documents.list({ limit: 50 }); const secondPage = firstPage.hasMore ? await backendClient.documents.list({ cursor: firstPage.nextCursor ?? undefined, limit: 50, }) : undefined; ``` ```ts const page = await backendClient.documents.list({ limit: 50 }); const deleted = await backendClient.documents.deleteMany({ refs: page.items.map((file) => ({ id: file.id })), }); for (const failure of deleted.failed) { console.error(failure.ref, failure.error.code); } ``` Frontend route bodies and bucket input are now validated before any router hook or provider method runs. v0.8 did not validate upload input on the server, so requests that passed before can now fail with `BAD_REQUEST`. Hooks, paths, and metadata receive the schema's parsed output. The encrypted `edgestore-ctx` payload is also namespaced in v1; there is no decoder fallback for a v0.8 context cookie, so clients must complete the normal `/init` request after deployment. Azure users must replace the reusable SAS token with the storage account key. The provider now derives short-lived create/write upload URLs and independent read-only URLs for private files. Canonical file URLs never include the credential query string. ## Provider capabilities Every provider is defined through the same resource-oriented contract. The router backend client exposes exactly the methods present on that provider: | Provider | Router backend client | Adapter upload/delete | API v2 calls | | -------------------- | ---------------------------------------- | --------------------- | ------------------------ | | Hosted `edgestore()` | Full support | Full support | Through `@edgestore/sdk` | | `s3()` | Upload, get, delete, and signed reads | Full support | Never | | `azureBlob()` | Upload, get, delete, and signed reads | Full support | Never | | Custom provider | Inferred from its `defineProvider` shape | Provider-defined | Provider-defined | Omitted capabilities are absent from TypeScript and are not replaced by a runtime fallback to the hosted API. ## Staying on v0.8 v0.8 remains installable for applications that cannot migrate yet. Pin `@edgestore/server` and `@edgestore/react` to `^0.8.0` until you are ready. # EdgeStore Docs: Quick Start URL: https://edgestore.dev/docs/quick-start Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/quick-start.mdx ## Set up with your agent Read [https://edgestore.dev/SKILL.md](https://edgestore.dev/SKILL.md) and add file uploads to this application. [Install the skill or connect MCP](/docs/agents). ## Next.js Setup ### Install npm pnpm yarn bun ```bash npm install @edgestore/server@rc @edgestore/react@rc ``` ```bash pnpm add @edgestore/server@rc @edgestore/react@rc ``` ```bash yarn add @edgestore/server@rc @edgestore/react@rc ``` ```bash bun add @edgestore/server@rc @edgestore/react@rc ``` ### Environment Variables Copy your project's keys from the [dashboard](https://dashboard.edgestore.dev) to the backend env file: ```sh title=".env.local" EDGESTORE_ACCESS_KEY=your-access-key EDGESTORE_SECRET_KEY=your-secret-key ``` Use the app's existing env file, or `.env.local` for a new app. Keep it gitignored. ### Backend Choose your router below. This bucket's files are accessible to anyone with the link. <> ```ts title="src/app/api/edgestore/[...edgestore]/route.ts" import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreNextHandler } from '@edgestore/server/adapters/next/app'; const es = initEdgeStore.create(); /** * This is the main router for the EdgeStore buckets. */ const router = es.router({ publicFiles: es.fileBucket(), }); const handler = createEdgeStoreNextHandler({ router, }); export { handler as GET, handler as POST }; /** * This type is used to create the type-safe client for the frontend. */ export type EdgeStoreRouter = typeof router; ``` ```ts title="src/pages/api/edgestore/[...edgestore].ts" import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreNextHandler } from '@edgestore/server/adapters/next/pages'; const es = initEdgeStore.create(); /** * This is the main router for the edgestore buckets. */ const router = es.router({ publicFiles: es.fileBucket(), }); export default createEdgeStoreNextHandler({ router, }); /** * This type is used to create the type-safe client for the frontend. */ export type EdgeStoreRouter = typeof router; ``` ### Frontend Create the React provider: <> ```ts title="src/lib/edgestore.ts" 'use client'; import { createEdgeStoreProvider } from '@edgestore/react'; import { type EdgeStoreRouter } from '../app/api/edgestore/[...edgestore]/route'; const { EdgeStoreProvider, useEdgeStore } = createEdgeStoreProvider(); export { EdgeStoreProvider, useEdgeStore }; ``` ```ts title="src/lib/edgestore.ts" 'use client'; import { createEdgeStoreProvider } from '@edgestore/react'; import { type EdgeStoreRouter } from '../pages/api/edgestore/[...edgestore]'; const { EdgeStoreProvider, useEdgeStore } = createEdgeStoreProvider(); export { EdgeStoreProvider, useEdgeStore }; ``` Wrap your app with the provider: <> ```tsx title="src/app/layout.tsx" // [!code ++] import { EdgeStoreProvider } from '../lib/edgestore'; import './globals.css'; // ... export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( {/* [!code ++] */} {children} ); } ``` ```tsx title="src/pages/_app.tsx" import '../styles/globals.css'; import type { AppProps } from 'next/app'; // [!code ++] import { EdgeStoreProvider } from '../lib/edgestore'; export default function App({ Component, pageProps }: AppProps) { return ( ); } ``` ### Upload file You can use the `useEdgeStore` hook to access type-safe frontend client and use it to upload files. ```tsx 'use client'; import * as React from 'react'; import { useEdgeStore } from '../lib/edgestore'; export default function Page() { const [file, setFile] = React.useState(); const { edgestore } = useEdgeStore(); return (
{ setFile(e.target.files?.[0]); }} />
); } ``` ### Replace file By passing the `replaceTargetUrl` option, you can replace an existing file with a new one. It will automatically delete the old file after the upload is complete. You can also just upload the file using the same file name, but in that case, you might still see the old file for a while because of the CDN cache. ```tsx const res = await edgestore.publicFiles.upload({ file, // [!code ++:3] options: { replaceTargetUrl: oldFileUrl, }, }); ``` ### Delete file You can delete a file by passing its URL to the `delete` method. To be able to delete a file from a client component like this, you will need to set the `beforeDelete` [lifecycle hook](/docs/configuration#lifecycle-hooks) on the bucket. ```tsx await edgestore.publicFiles.delete({ url: urlToDelete, }); ``` Use `deleteMany` to send one request for multiple files. Storage failures are reported per URL: ```tsx const result = await edgestore.publicFiles.deleteMany({ urls: selectedFiles.map((file) => file.url), }); for (const failure of result.failed) { console.error(failure.url, failure.error.code); } ``` EdgeStore runs `beforeDelete` for every file before deleting any of them. If one file is unauthorized, the entire request is rejected without calling the storage provider. Once authorization succeeds, the provider may still return partial storage failures in `result.failed`. ### Cancel upload To cancel an ongoing file upload, you can use an AbortController the same way you would use it to cancel a fetch request. ```tsx // prepare a state for the AbortController const [abortController, setAbortController] = useState(); // ... // instantiate the AbortController and add the signal to the upload method const abortController = new AbortController(); setAbortController(abortController); const res = await edgestore.publicFiles.upload({ file, signal: abortController.signal, }); // ... // to cancel the upload, call the controller's abort method abortController?.abort(); ``` When you cancel an upload, an `UploadAbortedError` will be thrown.
You can catch this error and handle it as needed.
For more information, check the [Error Handling](/docs/error-handling) page.
### Transform files before upload You can transform a file before EdgeStore validates and uploads it by passing the `transform` option. If the transform keeps the same extension, you can return the transformed `File` or `Blob` directly. If the transform changes the file type, return the transformed file with its new extension. For example, you can convert JPEG and PNG images to WebP before upload: npm pnpm yarn bun ```bash npm install browser-image-compression ``` ```bash pnpm add browser-image-compression ``` ```bash yarn add browser-image-compression ``` ```bash bun add browser-image-compression ``` ```tsx import imageCompression from 'browser-image-compression'; const res = await edgestore.publicImages.upload({ file, options: { transform: async ({ file, extension, signal }) => { if (!['image/jpeg', 'image/png'].includes(file.type)) { return { file, extension }; } const compressedFile = await imageCompression(file, { fileType: 'image/webp', initialQuality: 0.8, useWebWorker: true, signal, }); return { file: compressedFile, extension: 'webp', }; }, }, }); ``` If you provide `manualFileName`, EdgeStore will use that exact file name. Make sure the file name extension matches the transformed file type. ### Temporary files You can upload temporary files by passing the `temporary` option to the `upload` method. Temporary files will be automatically deleted after 24 hours if they are not confirmed. ```tsx await edgestore.publicFiles.upload({ file: fileToUpload, // [!code ++:3] options: { temporary: true, }, }); ``` For forms, upload files as temporary and save their returned IDs/keys and URLs with your record before confirming them. If the database save fails, leave the files temporary. Confirmation does not require waiting for hosted processing. Use the `confirm` method after saving: ```tsx await edgestore.publicFiles.confirm({ url: urlToConfirm, }); ``` To confirm several temporary files in one request, use `confirmMany`: ```tsx const result = await edgestore.publicFiles.confirmMany({ urls: temporaryFiles.map((file) => file.url), }); ``` You can check if a file is temporary in the dashboard.
Temporary files are marked with a clock icon.
### Wait for processing `upload` resolves as soon as the file is transferred. EdgeStore then processes it in the background, for example to generate an image thumbnail. The file ID and URL are final right away, so most apps can save them without waiting. When you need processed details immediately, such as whether the image got a thumbnail, pass `waitForProcessing`. The upload then resolves with the processed file, including freshly signed URLs for buckets with `autoSignedUrls`, and `onPhaseChange` tells you when the transfer finishes and processing begins. ```tsx const res = await edgestore.publicImages.upload({ file, onProgressChange: (progress) => setProgress(progress), // [!code ++:6] onPhaseChange: (phase) => { if (phase === 'processing') setStatus('Processing…'); }, options: { waitForProcessing: true, // or { timeoutMs: 120_000 } }, }); ``` Processing failures reject with `UploadCanceledError`. If processing takes longer than `timeoutMs` (60 seconds by default), the upload rejects with `UploadProcessingTimeoutError`; the file may still finish processing. Both errors include the file `id`. See [Error Handling](/docs/error-handling). Waiting for processing does not confirm a [temporary file](#temporary-files). Confirm it separately once your app decides to keep it. Providers without background processing, such as S3 and Azure Blob, resolve once the transfer completes.
## Troubleshooting If you have any problems using EdgeStore, please check the [Troubleshooting](./troubleshooting) page. ## FAQ import { Accordion, Accordions } from 'fumadocs-ui/components/accordion'; EdgeStore is a type-safe file upload solution for React applications. It provides an easy-to-use API for uploading, managing, and serving files with features like progress tracking, file validation, and automatic cleanup of temporary files. Yes! The EdgeStore Provider has a free plan with generous limits, so you can get started without any cost. You can also use EdgeStore with your own infrastructure (like AWS S3 or Azure Blob Storage) if you prefer to manage your own storage. The EdgeStore packages (`@edgestore/server`, `@edgestore/react`, etc.) are open source and released under the MIT license. However, the EdgeStore Provider (cloud service) is not open source. EdgeStore supports multiple frameworks including Next.js (App Router and Pages Router), Astro, Express, Fastify, Hono, Remix, and TanStack Start. Check the [Adapters](/docs/adapters/next) section for setup guides. By default, EdgeStore accepts most common file types. You can customize allowed file types and maximum file sizes per bucket using the `accept` and `maxSize` options. See the [Configuration](/docs/configuration) page for details. Yes! EdgeStore supports AWS S3, Azure Blob Storage, and custom providers in addition to EdgeStore Cloud. Check the [Providers](/docs/providers/edgestore) section for setup instructions. EdgeStore is not recommended for real-time streaming of large files like videos and audio. It's optimized for file uploads and serving static files, not for streaming use cases. For video/audio streaming, consider using a dedicated streaming service or CDN. # EdgeStore Docs: Low-level SDK URL: https://edgestore.dev/docs/sdk Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/sdk.mdx `@edgestore/sdk` is the public, low-level client for EdgeStore API v2. Use it when you need API operations that are not tied to an EdgeStore router. For router-derived bucket names, input, path, and metadata types, use the [backend client](/docs/backend-client) instead. The SDK is server-only. Project secrets and management tokens must never be included in browser bundles. ## Install npm pnpm yarn bun ```bash npm install @edgestore/sdk@rc ``` ```bash pnpm add @edgestore/sdk@rc ``` ```bash yarn add @edgestore/sdk@rc ``` ```bash bun add @edgestore/sdk@rc ``` ## Project client Project credentials expose runtime operations for the credential's current project. The reserved project selector is handled internally. ```ts import { createEdgeStoreSdk } from '@edgestore/sdk'; const sdk = createEdgeStoreSdk({ credentials: { accessKey: process.env.EDGESTORE_ACCESS_KEY!, secretKey: process.env.EDGESTORE_SECRET_KEY!, }, }); const file = await sdk.runtime.uploads.upload({ bucket: 'documents', source: pdfBlob, fileName: 'invoice.pdf', metadata: { invoiceId: invoice.id }, signal, onProgress: ({ percentage, phase }) => { console.log(phase, percentage); }, }); ``` The upload helper selects single or multipart upload automatically, reports progress, supports cancellation, and waits for upload processing to finish. Upload creation is never retried because it is not idempotent. Signed storage transfers may be retried safely, and processing polls follow `Retry-After`. You can also use `runtime.uploads.request`, `createParts`, and `completeMultipart` when you need to manage transfer details yourself. Sources can be text, a `Blob`, an `ArrayBuffer` or typed-array view, or a known-size Web `ReadableStream`: ```ts await sdk.runtime.uploads.upload({ bucket: 'archives', source: { stream, sizeBytes }, fileName: 'archive.tar', }); await sdk.runtime.uploads.uploadFromUrl({ bucket: 'imports', url: 'https://example.com/report.csv', }); ``` The defaults are a 100 MiB multipart threshold, 16 MiB parts, concurrency 4, a 30-second control timeout, no transfer timeout, and a 60-second processing timeout. Configure upload defaults with the `upload` option on `createEdgeStoreSdk`; pass an `AbortSignal` for per-operation cancellation or deadlines. ## Runtime resources ```ts const page = await sdk.runtime.files.search({ bucket: 'documents', filter: { metadata: { ownerId: user.id } }, pagination: { limit: 50 }, }); if (page.pagination.hasMore) { const nextPage = await sdk.runtime.files.search({ bucket: 'documents', pagination: { cursor: page.pagination.nextCursor ?? undefined, limit: 50, }, }); } const { signedUrls } = await sdk.runtime.files.generateSignedReadUrls({ bucket: 'documents', urls: page.files.map((file) => file.url), expiresIn: 15 * 60, }); await sdk.runtime.files.confirm({ file: { id: file.file.id } }); await sdk.runtime.files.delete({ file: { key: file.file.key } }); const batch = await sdk.runtime.files.deleteMany({ files: [{ id: file.file.id }, { url: legacyFileUrl }], }); for (const result of batch.results) { if (!result.success) console.error(result.fileRef, result.error.code); } ``` Runtime resources also include projects, buckets, file lookup, signed read URLs, access tokens, upload inspection, cancellation, and singular or plural restore operations. Singular mutations throw `EdgeStoreFileMutationError` for an item failure; plural mutations return the complete partial result. ## Management client A management token uses Bearer authentication and exposes account, project, credential, token, and membership resources. Runtime calls can select a project per operation or create an eagerly scoped runtime client once. ```ts const management = createEdgeStoreSdk({ credentials: { token: process.env.EDGESTORE_MANAGEMENT_TOKEN!, }, }); const projects = await management.management.projects.list({ account: 'account-id', }); const project = management.runtime.forProject(projects.projects[0]!.id); const buckets = await project.buckets.list(); // An explicit selector remains useful for one-off calls. const anotherProject = await management.runtime.projects.get({ project: 'another-project-id', }); const { accessUrls } = await management.management.files.generateAccessUrls({ project: projects.projects[0]!.id, files: [{ id: 'file-id' }], expiresIn: 15 * 60, }); ``` ## Reusing SDK types Common runtime workflows have named public input and result types: ```ts import type { RuntimeFileLookupInput, RuntimeFileLookupResult, RuntimeSignedReadUrlsGenerateInput, RuntimeSignedReadUrlsGenerateResult, RuntimeUploadInput, RuntimeUploadResult, } from '@edgestore/sdk'; ``` For any other operation, derive its friendly input and resolved output from the public SDK interface. This includes SDK selectors such as `account`, `project`, and `bucket`, plus `signal`. ```ts import type { ManagementEdgeStoreSdk } from '@edgestore/sdk'; type CreateProjectMethod = ManagementEdgeStoreSdk['management']['projects']['create']; type CreateProjectInput = Parameters[0]; type CreateProjectOutput = Awaited>; ``` You can use the same pattern with a configured SDK value: ```ts type LookupInput = Parameters[0]; type LookupOutput = Awaited< ReturnType >; ``` The generated OpenAPI operation types remain internal. Deriving from the public interface keeps application types aligned with the supported SDK surface. ## Errors ```ts import { EdgeStoreApiError, EdgeStoreNetworkError } from '@edgestore/sdk'; try { await sdk.runtime.files.lookup({ file: { url } }); } catch (error) { if (error instanceof EdgeStoreApiError) { console.error(error.status, error.code, error.requestId); } else if (error instanceof EdgeStoreNetworkError) { console.error('The API could not be reached', error.cause); } } ``` API errors preserve the HTTP status, machine-readable code, details, request ID, and retry guidance. Abort, network, upload, cancellation, and processing timeout errors have dedicated classes. ## Custom environments Use `apiUrl` for a compatible API v2 deployment and `fetch` to provide a custom server-side transport implementation. ```ts const sdk = createEdgeStoreSdk({ credentials: { accessKey, secretKey }, apiUrl: 'https://api.example.com/v2', fetch: instrumentedFetch, }); ``` `apiUrl` is the complete v2 URL. The SDK does not append `/v2` to an explicit value. # EdgeStore Docs: Troubleshooting URL: https://edgestore.dev/docs/troubleshooting Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/troubleshooting.mdx ## Ask your agent to debug it Read [https://edgestore.dev/docs/troubleshooting.md](https://edgestore.dev/docs/troubleshooting.md) and inspect my existing EdgeStore integration. Explain the cause before making changes. Problem: \[Describe the problem] Include the error message and steps to reproduce it. ## Route not found or method not allowed Open the handler's health endpoint. With the default Next.js route it is [http://localhost:3000/api/edgestore/health](http://localhost:3000/api/edgestore/health). Use your backend origin and mounted path for other adapters. For a 404 or 405, check the catch-all route and GET/POST exports. Match the React provider's `basePath` to the mounted endpoint, including any proxy prefix. ## Credentials or access denied Confirm the backend loads the expected env file before creating the storage provider, and that the keys belong to the intended project. For protected buckets, check the application's authentication context and access rules. ## Browser upload failures Check the browser console for errors and the failing request and response in the Network panel, then use [Error handling](/docs/error-handling) to identify validation, size, or access errors. Confirm that the React provider wraps the upload component. For separate frontend and backend origins, allow the intended frontend origin in CORS and check the adapter's cookie and deployment requirements. ## Enable server debug logging Enable [debug logging](/docs/logging) to inspect server-side failures. Debug output includes application context and cookies. Remove those values before sharing logs. ## Inspect the application with the CLI ```sh edgestore --cwd agent context --json edgestore --cwd doctor --offline --json ``` Use the backend directory for server checks in a split workspace. `agent context` reports installed versions and local references. `doctor --offline` inspects configuration without authenticating or running the application. ## Try an example app Run an [example close to your framework](https://github.com/edgestorejs/edgestore/tree/main/examples) to compare with your application. For Next.js: ```sh npx degit edgestorejs/edgestore/examples/next-basic#main edgestore-example cd edgestore-example npm install ``` Add your EdgeStore keys to `.env.local` as shown in [Quick start](/docs/quick-start#environment-variables), then run: ```sh npm run dev ``` ## Still blocked? Ask in [Discord](https://discord.gg/HvrnhRTfgQ) or [open an issue](https://github.com/edgestorejs/edgestore/issues) with your framework, installed EdgeStore versions, and the error. # EdgeStore Docs: Utils URL: https://edgestore.dev/docs/utils Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/utils.mdx ## Download links Sometimes the browser shows the file directly in the browser instead of downloading it. To force the browser to download the file, you can use the `getDownloadUrl` function. ```ts import { getDownloadUrl } from '@edgestore/react/utils'; getDownloadUrl( url, // the url of the file 'overwrite-file-name.jpg', // optional, the name of the file to download ); ``` ## Format file size You might want to display the file size in a human-readable format. You can use the `formatFileSize` function to do that. ```ts import { formatFileSize } from '@edgestore/react/utils'; formatFileSize(10485760); // => 10MB ``` # EdgeStore Docs: Avatar URL: https://edgestore.dev/docs/components/avatar Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/components/avatar.mdx import { DemoBlock } from '@/components/demo-block'; import { LimitedCode } from '@/components/ui/limited-code'; import { OpenTabs, OpenTabsContent, OpenTabsList, OpenTabsTrigger, } from '@/components/ui/open-tabs'; import AvatarDropzoneBlock from '@/components/upload/blocks/avatar-block'; import { Step, Steps } from 'fumadocs-ui/components/steps'; Click or drop onto the image to replace it. With `minDimension`, the image's pixel size is checked in the browser before it is uploaded. ## Installation CLI Manual Use the shadcn CLI to add the component to your project. npm pnpm yarn bun ```bash npx shadcn@latest add https://edgestore.dev/r/avatar-dropzone.json ``` ```bash pnpm dlx shadcn@latest add https://edgestore.dev/r/avatar-dropzone.json ``` ```bash yarn dlx shadcn@latest add https://edgestore.dev/r/avatar-dropzone.json ``` ```bash bun x shadcn@latest add https://edgestore.dev/r/avatar-dropzone.json ``` ### Setup for manual installation First you will need to follow the [manual install setup](./manual-install) guide. ### Install required components * [uploader-provider](./uploader-provider) * [dropzone](./dropzone) * [progress-circle](./progress-circle) * [shadcn/ui button](https://ui.shadcn.com/docs/components/button) ### Copy this component ````tsx title="components/upload/avatar-dropzone.tsx" 'use client'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { CameraIcon, Loader2Icon, UserIcon } from 'lucide-react'; import * as React from 'react'; import { type Accept } from 'react-dropzone'; import { dropzoneState, useUploadDropzone } from './dropzone'; import { ProgressCircle } from './progress-circle'; import { formatFileSize, IMAGE_ACCEPT, minImageSize, useObjectUrl, } from './upload-utils'; import { useUploader } from './uploader-provider'; /** * Props for the AvatarDropzone component. */ export type AvatarDropzoneProps = Omit< React.ComponentProps<'div'>, 'children' > & { /** * Label shown next to the image and used in accessible labels. * @default "Profile photo" */ label?: string; /** * Help text shown under the label. Defaults to a summary of the limits. */ description?: React.ReactNode; /** * Shape of the image. * @default "circle" */ shape?: 'circle' | 'square'; /** * Size of the image in pixels. * @default 88 */ size?: number; /** * Maximum file size in bytes. */ maxSize?: number; /** * Minimum width and height of the image in pixels. */ minDimension?: number; /** * Accepted image types. * @default PNG, JPG, WEBP and GIF */ accept?: Accept; /** * Human-readable list of accepted types, shown in the description and messages. * @default "PNG, JPG, WEBP or GIF" */ typesLabel?: string; /** * Shown when there is no image. */ placeholder?: React.ReactNode; /** * Whether the dropzone is disabled. */ disabled?: boolean; }; /** * A single image in a fixed shape, for profile photos and logos. * Click or drop onto the image to replace it. * * @example * ```tsx * * ``` */ export function AvatarDropzone({ label = 'Profile photo', description, shape = 'circle', size = 88, maxSize, minDimension, accept = IMAGE_ACCEPT, typesLabel = 'PNG, JPG, WEBP or GIF', placeholder, disabled, className, ...props }: AvatarDropzoneProps) { const { fileStates, removeFile, uploadFiles } = useUploader(); const fileState = fileStates[0]; const preview = useObjectUrl(fileState?.file); const src = preview ?? fileState?.url; const validator = React.useMemo( () => (minDimension ? minImageSize(minDimension) : undefined), [minDimension], ); const { getRootProps, getInputProps, open, isDragActive, isDragReject, isProcessing, errors, clearErrors, } = useUploadDropzone({ accept, maxSize, typesLabel, disabled, validator, replace: true, }); const isBusy = isProcessing || fileState?.status === 'UPLOADING'; const accessibleLabel = label.toLowerCase(); return (
{src ? ( ) : ( (placeholder ?? ( )) )} {!disabled && ( )} {isBusy && ( {isProcessing ? ( ) : ( = 72} className="text-[10px]" /> )} )}

{label}

{description ?? [ typesLabel, minDimension && `at least ${minDimension}Ă—${minDimension}px`, maxSize && `up to ${formatFileSize(maxSize)}`, ] .filter(Boolean) .join(', ')}

{!disabled && (
{fileState && ( )}
)} {fileState?.status === 'ERROR' && (

{fileState.error ?? 'Upload failed'}{' '} {!disabled && ( )}

)} {errors[0] && (

{errors[0]}

)}
); } ````
## Usage Install or copy the component from [Installation](#installation) before using this example. ```tsx 'use client'; import { AvatarDropzone } from '@/components/upload/avatar-dropzone'; import { UploaderProvider, type UploadFn, } from '@/components/upload/uploader-provider'; import { useEdgeStore } from '@/lib/edgestore'; import * as React from 'react'; export function AvatarDropzoneUsage() { const { edgestore } = useEdgeStore(); const uploadFn: UploadFn = React.useCallback( async ({ file, onProgressChange, signal }) => { const res = await edgestore.publicImages.upload({ file, signal, onProgressChange, }); // you can run some server action or api here // to add the necessary data to your database console.log(res); return res; }, [edgestore], ); return ( ); } ``` ## Props | Prop | Type | Default | Description | | -------------- | ---------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | `label` | `string` | `"Profile photo"` | Label shown next to the image and used in accessible labels. | | `description` | `ReactNode` | | Help text under the label. Defaults to a summary of the limits. | | `shape` | `"circle" \| "square"` | `"circle"` | Shape of the image. | | `size` | `number` | `88` | Size of the image in pixels. | | `minDimension` | `number` | | Minimum width and height of the image in pixels. | | `placeholder` | `ReactNode` | user icon | Shown when there is no image. | | `maxSize` | `number` | | Maximum file size in bytes. | | `accept` | `Accept` | PNG, JPG, WEBP, GIF | Accepted file types, in [react-dropzone's format](https://react-dropzone.js.org/#section-accepting-specific-file-types). | | `typesLabel` | `string` | `"PNG, JPG, WEBP or GIF"` | Human-readable list of accepted types, shown in the hint and in error messages. | | `disabled` | `boolean` | `false` | Disables the component. | # EdgeStore Docs: Dropzone URL: https://edgestore.dev/docs/components/dropzone Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/components/dropzone.mdx import { LimitedCode } from '@/components/ui/limited-code'; import { OpenTabs, OpenTabsContent, OpenTabsList, OpenTabsTrigger, } from '@/components/ui/open-tabs'; import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; `dropzone.tsx` exports: * `Dropzone`: a drop area that adds files to the nearest `UploaderProvider`. * `useUploadDropzone`: [react-dropzone](https://react-dropzone.js.org/)'s `useDropzone`, connected to the `UploaderProvider`. It enforces `maxFiles` across drops, adds what fits, and returns readable messages about skipped files. * `DropzonePrompt`, `DropzoneOverlay` and `UploadErrors`: the building blocks for the empty state, the drag overlay and the error notice. * `dropzoneVariants` and `dropzoneState`: the classes for a drop area and the data attributes that drive its drag styles. If you are installing the other upload components via the CLI, this component will be installed automatically. You can skip the following steps. ## Installation CLI Manual Use the shadcn CLI to add the component to your project. npm pnpm yarn bun ```bash npx shadcn@latest add https://edgestore.dev/r/dropzone.json ``` ```bash pnpm dlx shadcn@latest add https://edgestore.dev/r/dropzone.json ``` ```bash yarn dlx shadcn@latest add https://edgestore.dev/r/dropzone.json ``` ```bash bun x shadcn@latest add https://edgestore.dev/r/dropzone.json ``` ### Setup for manual installation First you will need to follow the [manual install setup](./manual-install) guide. ### Install required components * [uploader-provider](./uploader-provider) ### Copy this component ````tsx title="components/upload/dropzone.tsx" 'use client'; import { cn } from '@/lib/utils'; import { AlertCircleIcon, UploadIcon, XIcon } from 'lucide-react'; import * as React from 'react'; import { useDropzone, type DropzoneOptions } from 'react-dropzone'; import { describeLimits, rejectionMessages, uploadErrorMessage, } from './upload-utils'; import { useUploader } from './uploader-provider'; /** * Options for the `useUploadDropzone` hook. */ export type UseUploadDropzoneOptions = Omit< DropzoneOptions, 'onDrop' | 'getErrorMessage' | 'maxFiles' > & { /** * Maximum number of files the uploader can hold in total. * Extra files in a drop are skipped with a message. */ maxFiles?: number; /** * Swap the current file for the dropped one instead of adding it. * Implies `multiple: false`. */ replace?: boolean; /** * Human-readable list of accepted types, used in messages. e.g. "PNG or JPG" */ typesLabel?: string; /** * Called with the messages for files that were not added. */ onRejected?: (messages: string[]) => void; }; /** * `useDropzone` wired to the nearest `UploaderProvider`. * Adds what fits, skips the rest and keeps a list of messages about skipped files. * * @example * ```tsx * const { getRootProps, getInputProps, errors } = useUploadDropzone({ maxFiles: 5 }); * ``` */ export function useUploadDropzone({ maxFiles, replace, typesLabel, onRejected, disabled, ...options }: UseUploadDropzoneOptions = {}) { const { fileStates, addFiles, removeFile } = useUploader(); const [errors, setErrors] = React.useState([]); const isFull = !replace && maxFiles !== undefined && fileStates.length >= maxFiles; const multiple = options.multiple ?? !replace; const dropzone = useDropzone({ ...options, multiple, disabled: disabled || isFull, getErrorMessage: uploadErrorMessage({ maxSize: options.maxSize, minSize: options.minSize, maxFiles: multiple ? maxFiles : 1, typesLabel, }), onDrop: (accepted, rejected) => { const messages = rejectionMessages(rejected); if (replace) { if (accepted.length > 0) { fileStates.forEach((fileState) => removeFile(fileState.key)); addFiles(accepted.slice(0, 1)); } } else { // react-dropzone's `maxFiles` only counts a single drop, so the total is enforced here. const room = maxFiles === undefined ? accepted.length : Math.max(maxFiles - fileStates.length, 0); if (accepted.length > room) { messages.push( `You can add up to ${maxFiles} files. ${accepted.length - room} skipped.`, ); } addFiles(accepted.slice(0, room)); } setErrors(messages); if (messages.length > 0) onRejected?.(messages); }, }); const clearErrors = React.useCallback(() => setErrors([]), []); return { ...dropzone, errors, clearErrors, isFull }; } /** * Data attributes that drive the drag styles of `dropzoneVariants`. */ export function dropzoneState({ isDragActive, isDragReject, disabled, }: { isDragActive?: boolean; isDragReject?: boolean; disabled?: boolean; }) { return { 'data-dragging': isDragActive || undefined, 'data-rejected': isDragReject || undefined, 'data-disabled': disabled || undefined, }; } /** * Base classes for a drop area. Pair with `dropzoneState()`. */ export const dropzoneVariants = 'group/dropzone relative flex cursor-pointer flex-col items-center justify-center rounded-xl border-[1.5px] border-dashed border-muted-foreground/30 bg-muted/40 text-center outline-none transition-[border-color,background-color,box-shadow] hover:border-primary/50 hover:bg-muted/70 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[dragging]:border-solid data-[dragging]:border-primary data-[dragging]:bg-primary/5 data-[rejected]:border-destructive data-[rejected]:bg-destructive/5 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-60 data-[disabled]:hover:border-muted-foreground/30 data-[disabled]:hover:bg-muted/40'; /** * Icon, title and hint shown inside an empty drop area. */ export function DropzonePrompt({ icon = , title, hint, isDragActive, isDragReject, activeText = 'Drop to upload', rejectText = 'File type not supported', className, }: { icon?: React.ReactNode; title: React.ReactNode; hint?: React.ReactNode; isDragActive?: boolean; isDragReject?: boolean; activeText?: string; rejectText?: string; className?: string; }) { return (
{icon}

{isDragReject ? rejectText : isDragActive ? activeText : title}

{hint &&

{hint}

}
); } /** * Covers a drop area that already shows files while something is dragged over it. */ export function DropzoneOverlay({ isDragReject, children, }: { isDragReject?: boolean; children: React.ReactNode; }) { return (
{children}
); } /** * Lists messages about files that were not added. */ export function UploadErrors({ errors, onDismiss, className, }: { errors: string[]; onDismiss?: () => void; className?: string; }) { if (errors.length === 0) return null; return (
    {errors.map((error) => (
  • {error}
  • ))}
{onDismiss && ( )}
); } /** * Props for the Dropzone component. */ export type DropzoneProps = Omit, 'title'> & { /** * Options passed to `useUploadDropzone` (and react-dropzone). */ dropzoneOptions?: UseUploadDropzoneOptions; /** * Whether the dropzone is disabled. */ disabled?: boolean; /** * Icon shown above the title. */ icon?: React.ReactNode; /** * Title shown when idle. */ title?: React.ReactNode; /** * Hint shown below the title. Defaults to a summary of the limits. */ hint?: React.ReactNode; /** * Title shown while files are dragged over the dropzone. */ activeText?: string; }; /** * A drop area that adds files to the nearest `UploaderProvider`. * * @example * ```tsx * * ``` */ export function Dropzone({ dropzoneOptions, disabled, icon, title, hint, activeText = 'Drop files to upload', className, ...props }: DropzoneProps) { const { getRootProps, getInputProps, isDragActive, isDragReject, errors, clearErrors, isFull, } = useUploadDropzone({ ...dropzoneOptions, disabled }); return (
Click to upload {' '} or drag and drop )) } hint={hint ?? describeLimits(dropzoneOptions ?? {})} />
); } ```` ````tsx title="components/upload/upload-utils.ts" 'use client'; import * as React from 'react'; import { type Accept, type FileError, type FileRejection, } from 'react-dropzone'; export const IMAGE_ACCEPT: Accept = { 'image/png': ['.png'], 'image/jpeg': ['.jpg', '.jpeg'], 'image/webp': ['.webp'], 'image/gif': ['.gif'], }; export const DOCUMENT_ACCEPT: Accept = { 'application/pdf': ['.pdf'], 'application/msword': ['.doc'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': [ '.docx', ], }; export const SPREADSHEET_ACCEPT: Accept = { 'text/csv': ['.csv'], 'application/vnd.ms-excel': ['.xls'], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [ '.xlsx', ], }; /** * Formats a file size in bytes to a human-readable string. * * @example * ```ts * formatFileSize(1024); // "1 KB" * formatFileSize(1024 * 1024 * 2.5); // "2.5 MB" * ``` */ export function formatFileSize(bytes?: number) { if (!bytes) return '0 B'; if (bytes < 1024) return `${Math.round(bytes)} B`; const units = ['KB', 'MB', 'GB', 'TB']; let value = bytes / 1024; let i = 0; while (value >= 1024 && i < units.length - 1) { value /= 1024; i++; } return `${value < 10 ? Number(value.toFixed(1)) : Math.round(value)} ${units[i]}`; } export function fileExtension(name: string) { const dot = name.lastIndexOf('.'); return dot > 0 ? name.slice(dot + 1).toLowerCase() : ''; } export type FileKind = | 'image' | 'video' | 'audio' | 'pdf' | 'doc' | 'sheet' | 'slides' | 'archive' | 'code' | 'other'; const KIND_BY_EXTENSION: Record = { pdf: 'pdf', doc: 'doc', docx: 'doc', txt: 'doc', md: 'doc', rtf: 'doc', xls: 'sheet', xlsx: 'sheet', csv: 'sheet', ppt: 'slides', pptx: 'slides', key: 'slides', zip: 'archive', rar: 'archive', '7z': 'archive', tar: 'archive', gz: 'archive', js: 'code', ts: 'code', tsx: 'code', jsx: 'code', json: 'code', html: 'code', css: 'code', py: 'code', }; export function fileKind(file: File): FileKind { if (file.type.startsWith('image/')) return 'image'; if (file.type.startsWith('video/')) return 'video'; if (file.type.startsWith('audio/')) return 'audio'; return KIND_BY_EXTENSION[fileExtension(file.name)] ?? 'other'; } export type UploadLimits = { maxSize?: number; minSize?: number; maxFiles?: number; /** Human-readable list of accepted types, e.g. "PNG or JPG". */ typesLabel?: string; }; /** * Friendlier rejection messages than react-dropzone's defaults * ("File is larger than 1048576 bytes"). Pass it to the `getErrorMessage` option. */ export function uploadErrorMessage(limits: UploadLimits) { return (error: FileError, file: File): string => { switch (error.code) { case 'file-too-large': return `${file.name} is ${formatFileSize(file.size)}. The limit is ${formatFileSize(limits.maxSize)}.`; case 'file-too-small': return `${file.name} is smaller than ${formatFileSize(limits.minSize)}.`; case 'file-invalid-type': return limits.typesLabel ? `${file.name} isn't supported. Use ${limits.typesLabel}.` : `${file.name} isn't a supported file type.`; case 'too-many-files': if (limits.maxFiles === 1) return 'Choose a single file.'; return limits.maxFiles ? `You can add up to ${limits.maxFiles} files.` : 'Too many files.'; default: return error.message; } }; } /** Flattens rejections into unique, human-readable messages. */ export function rejectionMessages(rejections: readonly FileRejection[]) { return [ ...new Set(rejections.flatMap((r) => r.errors.map((e) => e.message))), ]; } /** Short summary of the limits, e.g. "PNG or JPG · up to 2 MB · 5 max". */ export function describeLimits({ typesLabel, maxSize, maxFiles, }: UploadLimits) { return [ typesLabel, maxSize && `up to ${formatFileSize(maxSize)}${maxFiles === 1 ? '' : ' each'}`, maxFiles && maxFiles > 1 && `${maxFiles} max`, ] .filter(Boolean) .join(' · '); } /** * Async validator that rejects images smaller than `min` pixels on either side. * Pass it to the `validator` option. */ export function minImageSize(min: number) { return async (file: File): Promise => { // Let `accept` report the type error. if (!file.type.startsWith('image/')) return null; const url = URL.createObjectURL(file); try { const img = new Image(); img.src = url; await img.decode(); if (img.naturalWidth < min || img.naturalHeight < min) { return { code: 'image-too-small', message: `${file.name} is ${img.naturalWidth}×${img.naturalHeight}px. Use at least ${min}×${min}px.`, }; } return null; } catch { return { code: 'image-unreadable', message: `${file.name} couldn't be read as an image.`, }; } finally { URL.revokeObjectURL(url); } }; } /** * Returns an object URL for previewing `file`, created once per file and * revoked when the file changes or the component unmounts. */ export function useObjectUrl(file?: File | null) { const [url, setUrl] = React.useState(); React.useEffect(() => { if (!file) return; const objectUrl = URL.createObjectURL(file); setUrl(objectUrl); return () => { URL.revokeObjectURL(objectUrl); setUrl(undefined); }; }, [file]); return url; } ````
## Usage Install or copy the component from [Installation](#installation) before using this example. ```tsx 'use client'; import { Dropzone } from '@/components/upload/dropzone'; import { UploaderProvider, type UploadFn, } from '@/components/upload/uploader-provider'; import { useEdgeStore } from '@/lib/edgestore'; import * as React from 'react'; export function DropzoneUsage() { const { edgestore } = useEdgeStore(); const uploadFn: UploadFn = React.useCallback( async ({ file, onProgressChange, signal }) => { const res = await edgestore.publicFiles.upload({ file, signal, onProgressChange, }); // you can run some server action or api here // to add the necessary data to your database console.log(res); return res; }, [edgestore], ); return ( {/* You can create a component that uses the provider context */} {/* (from the `useUploader` hook) to show a custom file list here */} ); } ``` ### Custom dropzones Use `useUploadDropzone` to build your own drop area. Spread `dropzoneState()` on the root element so the `data-dragging`, `data-rejected` and `data-disabled` attributes drive the styles in `dropzoneVariants`: ```tsx import { dropzoneState, dropzoneVariants, UploadErrors, useUploadDropzone, } from '@/components/upload/dropzone'; function MyDropzone() { const { getRootProps, getInputProps, isDragActive, isDragReject, errors } = useUploadDropzone({ maxFiles: 3, maxSize: 1024 * 1024 }); return ( <>

Drop files here

); } ``` `upload-utils.ts` has the helpers the components share: `formatFileSize`, accept presets (`IMAGE_ACCEPT`, `DOCUMENT_ACCEPT`, `SPREADSHEET_ACCEPT`), the `minImageSize` validator and the `useObjectUrl` preview hook. # EdgeStore Docs: File Field URL: https://edgestore.dev/docs/components/file-field Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/components/file-field.mdx import { DemoBlock } from '@/components/demo-block'; import { LimitedCode } from '@/components/ui/limited-code'; import { OpenTabs, OpenTabsContent, OpenTabsList, OpenTabsTrigger, } from '@/components/ui/open-tabs'; import FileFieldBlock from '@/components/upload/blocks/file-field-block'; import { Step, Steps } from 'fumadocs-ui/components/steps'; Once a file is chosen, the field shows it with its progress and a remove button instead of the drop area. ## Installation CLI Manual Use the shadcn CLI to add the component to your project. npm pnpm yarn bun ```bash npx shadcn@latest add https://edgestore.dev/r/file-field.json ``` ```bash pnpm dlx shadcn@latest add https://edgestore.dev/r/file-field.json ``` ```bash yarn dlx shadcn@latest add https://edgestore.dev/r/file-field.json ``` ```bash bun x shadcn@latest add https://edgestore.dev/r/file-field.json ``` ### Setup for manual installation First you will need to follow the [manual install setup](./manual-install) guide. ### Install required components * [uploader-provider](./uploader-provider) * [dropzone](./dropzone) * [multi-file](./multi-file) ### Copy this component ````tsx title="components/upload/file-field.tsx" 'use client'; import { cn } from '@/lib/utils'; import { UploadIcon } from 'lucide-react'; import * as React from 'react'; import { type Accept } from 'react-dropzone'; import { dropzoneState, dropzoneVariants, useUploadDropzone } from './dropzone'; import { FileListItem } from './multi-file'; import { formatFileSize } from './upload-utils'; import { useUploader } from './uploader-provider'; /** * Props for the FileField component. */ export type FileFieldProps = Omit, 'children'> & { /** * Accepted file types. */ accept?: Accept; /** * Human-readable list of accepted types, shown in the hint and messages. * * @example "PDF, DOC or DOCX" */ typesLabel?: string; /** * Maximum file size in bytes. */ maxSize?: number; /** * Whether the field is disabled. */ disabled?: boolean; /** * Id for the hidden file input, so a ` ## Usage Install or copy the component from [Installation](#installation) before using this example. ```tsx 'use client'; import { FileField } from '@/components/upload/file-field'; import { DOCUMENT_ACCEPT } from '@/components/upload/upload-utils'; import { UploaderProvider, type UploadFn, } from '@/components/upload/uploader-provider'; import { useEdgeStore } from '@/lib/edgestore'; import * as React from 'react'; export function FileFieldUsage() { const { edgestore } = useEdgeStore(); const uploadFn: UploadFn = React.useCallback( async ({ file, onProgressChange, signal }) => { const res = await edgestore.publicFiles.upload({ file, signal, onProgressChange, }); // you can run some server action or api here // to add the necessary data to your database console.log(res); return res; }, [edgestore], ); return ( ); } ``` ## Props | Prop | Type | Default | Description | | ----------------- | --------- | --------- | ------------------------------------------------------------------------------------------------------------------------ | | `inputId` | `string` | | Id for the hidden file input, so a `