# EdgeStore Docs: Backend Client URL: /docs/backend-client Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/backend-client.mdx *** title: Backend Client description: Interact with EdgeStore from your backend. ------------------------------------------------------- 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 You can use your EdgeStore router to initialize the backend client. 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 { initEdgeStoreClient } from '@edgestore/server/core'; // ... export const handler = createEdgeStoreNextHandler({ router: edgeStoreRouter, }); // ... // [!code ++:3] export const backendClient = initEdgeStoreClient({ router: edgeStoreRouter, }); ``` 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. ## 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', }, }); ``` ### 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', }, }); ``` ### Confirm a temporary file upload If you upload a temporary file, you can confirm it by using the `confirmUpload` function. ```ts const res = await backendClient.publicFiles.confirmUpload({ url: fileUrl, }); ``` ## Backend Delete You can use the `deleteFile` function to delete files from your backend. ```ts const res = await backendClient.publicFiles.deleteFile({ url: fileUrl, }); ``` ## Backend List Files (search) You can use the `listFiles` 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 page (20 files) of all files in the bucket const res = await backendClient.publicFiles.listFiles(); // with filter and pagination const res = await backendClient.publicFiles.listFiles({ filter: { metadata: { role: 'admin', }, path: { type: 'post', }, uploadedAt: { gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7), // past 7 days }, }, pagination: { currentPage: 1, // default: 1 pageSize: 50, // default: 20 (max: 100) }, }); ``` ## TypeScript Helpers Sometimes you might want to have the response type of the backend client functions, so you can use it to build your own functions. You can use the `InferClientResponse` helper type to infer the response type of the backend client. ```ts title="src/lib/edgestore.ts" import { type InferClientResponse } from '@edgestore/server/core'; // ... /** * This helper type can be used to infer the response type of the backend client */ export type ClientResponse = InferClientResponse; ``` And you can use it like this: ```ts export const getServerSideProps: GetServerSideProps<{ files: ClientResponse['publicFiles']['listFiles']['data']; }> = async () => { const res = await backendClient.publicFiles.listFiles(); return { props: { files: res.data } }; }; ``` # EdgeStore Docs: Configuration URL: /docs/configuration Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/configuration.mdx *** title: Configuration description: Learn EdgeStore's configuration options. ----------------------------------------------------- ## 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 edgeStoreRouter = 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 edgeStoreRouter = 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: edgeStoreRouter, /** * 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. ```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 edgeStoreRouter = 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 edgeStoreRouter = 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 third party cookies. Since third party cookies are not supported in localhost (without https), in development, all the protected files will be proxied through your app's api so that the cookies can be forwarded to the file request. Also, the `` component from `next/image` does not forward the cookies in the request, so protected images won't be displayed. You will need ot 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: /docs/error-handling Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/error-handling.mdx *** title: Error Handling description: Learn how to handle specific server errors in your EdgeStore application. -------------------------------------------------------------------------------------- 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, } 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` # EdgeStore Docs: LLMs & Vibe Coding URL: /docs/llms-vibe-coding Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/llms-vibe-coding.mdx *** title: LLMs & Vibe Coding description: Learn how to leverage EdgeStore with Large Language Models (LLMs) and AI-powered IDEs, including VibeStack CLI support. ------------------------------------------------------------------------------------------------------------------------------------ EdgeStore has first-class support for Large Language Models (LLMs) and AI-powered IDEs. ## 1. llms.txt & llms-full.txt EdgeStore provides an [llms.txt](/llms.txt) file that contains the links to each page in the documentation. EdgeStore also provides an [llms-full.txt](/llms-full.txt) file that contains the whole documentation in a single markdown. ## 2. Copy Markdown Every page in the documentation contains a "Copy Markdown" button that allows you to copy the markdown formatted content of the page, so you can directly use it in your LLM. ## 3. `.md` suffix You can also just add a `.md` suffix to the end of the URL to get the markdown content of the page. For example: [https://edgestore.dev/docs/quick-start.md](/docs/quick-start.md) ## 4. VibeStack CLI Support EdgeStore supports [VibeStack](https://vibestack.app), which automatically adds framework-specific markdown instruction files to your project. Just run this command in your project directory: ```bash npx vibestack@latest add https://edgestore.dev/vibe.json ``` ```bash pnpm dlx vibestack@latest add https://edgestore.dev/vibe.json ``` ```bash yarn dlx vibestack@latest add https://edgestore.dev/vibe.json ``` ```bash bun x vibestack@latest add https://edgestore.dev/vibe.json ``` This will add multiple markdown instruction files specific to your framework, making it easier for AI coding assistants to understand how to implement EdgeStore in your codebase. # EdgeStore Docs: Logging URL: /docs/logging Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/logging.mdx *** title: Logging description: Learn how to configure logging levels for EdgeStore server-side operations. ---------------------------------------------------------------------------------------- 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: edgeStoreRouter, }); ``` ## Log Levels * `debug` * `info` * `warn` * `error` * `none` # EdgeStore Docs: Quick Start URL: /docs/quick-start Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/quick-start.mdx *** title: Quick Start description: Implement file uploads in your app with EdgeStore. --------------------------------------------------------------- ## Next.js Setup ### Install Let's start by installing the required packages. ```bash npm install @edgestore/server @edgestore/react zod ``` ```bash pnpm add @edgestore/server @edgestore/react zod ``` ```bash yarn add @edgestore/server @edgestore/react zod ``` ```bash bun add @edgestore/server @edgestore/react zod ``` ### Environment Variables Then go to your [Dashboard](https://dashboard.edgestore.dev), create a new project and copy the keys to your environment variables. ```sh title=".env" EDGE_STORE_ACCESS_KEY=your-access-key EDGE_STORE_SECRET_KEY=your-secret-key ``` Make sure you add `.env` to your `.gitignore` file.
You don't want to commit your secret keys to your repository.
### Backend Now we can create the backend code for our Next.js app.
EdgeStore is compatible with both types of Next.js apps (`pages router` and `app router`). The example below is the simplest bucket you can create with EdgeStore. Just a simple file bucket with no validation that will be accessible by anyone with the link. You can have multiple buckets in your app, each with its own configuration. ```ts tab="app" 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 edgeStoreRouter = es.router({ publicFiles: es.fileBucket(), }); const handler = createEdgeStoreNextHandler({ router: edgeStoreRouter, }); export { handler as GET, handler as POST }; /** * This type is used to create the type-safe client for the frontend. */ export type EdgeStoreRouter = typeof edgeStoreRouter; ``` ```ts tab="pages" 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 edgeStoreRouter = es.router({ publicFiles: es.fileBucket(), }); export default createEdgeStoreNextHandler({ router: edgeStoreRouter, }); /** * This type is used to create the type-safe client for the frontend. */ export type EdgeStoreRouter = typeof edgeStoreRouter; ``` ### Frontend Now let's initiate our context provider. ```ts tab="app" title="src/lib/edgestore.ts" 'use client'; import { type EdgeStoreRouter } from '../app/api/edgestore/[...edgestore]/route'; import { createEdgeStoreProvider } from '@edgestore/react'; const { EdgeStoreProvider, useEdgeStore } = createEdgeStoreProvider(); export { EdgeStoreProvider, useEdgeStore }; ``` ```ts tab="pages" title="src/lib/edgestore.ts" 'use client'; import { type EdgeStoreRouter } from '../pages/api/edgestore/[...edgestore]'; import { createEdgeStoreProvider } from '@edgestore/react'; const { EdgeStoreProvider, useEdgeStore } = createEdgeStoreProvider(); export { EdgeStoreProvider, useEdgeStore }; ``` And then wrap our app with the provider. ```tsx tab="app" 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 tab="pages" 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 ( {/* [!code ++] */} {/* [!code ++] */} ); } ``` ### 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, }); ``` ### 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.
### 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, }, }); ``` To confirm a temporary file, you can use the `confirmUpload` method. ```tsx await edgestore.publicFiles.confirmUpload({ url: urlToConfirm, }); ``` You can check if a file is temporary in the dashboard.
Temporary files are marked with a clock icon.
## Troubleshooting If you have any problems using EdgeStore, please check the [Troubleshooting](./troubleshooting) page. # EdgeStore Docs: Troubleshooting URL: /docs/troubleshooting Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/troubleshooting.mdx *** title: Troubleshooting description: Learn how to troubleshoot common issues with EdgeStore. -------------------------------------------------------------------- If you followed the docs to get started with EdgeStore, but you are having issues, here are some things you can try to find out what the problem might be. ## Check if the API is correctly configured You can try to access the `/health` endpoint of your edgestore API from the browser. The default URL is: [http://localhost:3000/api/edgestore/health](http://localhost:3000/api/edgestore/health) If you can see `OK` on the page, then the API is configured in the correct path. ## Set the log level to `debug` You can set the [log level](./logging) to `debug` to see in more details what is happening in the server. (These logs are for the server-side and will not be visible in the browser console) ## Check the browser console and network tab Open the developer tools in your browser and check the console and network tab to see if there are any helpful error messages. ## Try to run one of the example apps You can try to run one of the example apps on your local machine to see if it works. * clone the repo * `git clone https://github.com/edgestorejs/edgestore.git` * cd into the example app * `cd examples/next-basic` * install dependencies * `npm install` * add your environment variables * `examples/next-basic/.env.local` * run the app * `npm run dev` * access the app * [http://localhost:3000](http://localhost:3000) There are [other example apps](https://github.com/edgestorejs/edgestore/tree/main/examples) that you can try. Try to find the one that is closer to your use case. ## Tried everything and still not working? If you tried everything and still can't figure out what is wrong, you can reach for support in the [Discord server](https://discord.gg/HvrnhRTfgQ) or [open an issue](https://github.com/edgestorejs/edgestore/issues) in the GitHub repo. # EdgeStore Docs: Utils URL: /docs/utils Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/utils.mdx *** title: Utils description: Useful utilities methods for EdgeStore. ---------------------------------------------------- ## 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: Astro URL: /docs/adapters/astro Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/adapters/astro.mdx *** title: Astro description: Learn how to integrate EdgeStore with your Astro applications using the dedicated Astro adapter. ------------------------------------------------------------------------------------------------------------- EdgeStore supports Astro applications through our dedicated Astro adapter, allowing you to use EdgeStore's file upload and management capabilities with your Astro project. ## Setup ### Install Let's start by installing the required packages. ```bash npm install @edgestore/server @edgestore/react zod ``` ```bash pnpm add @edgestore/server @edgestore/react zod ``` ```bash yarn add @edgestore/server @edgestore/react zod ``` ```bash bun add @edgestore/server @edgestore/react zod ``` ### Environment Variables Then go to your [Dashboard](https://dashboard.edgestore.dev), create a new project and copy the keys to your environment variables. ```sh title=".env" EDGE_STORE_ACCESS_KEY=your-access-key EDGE_STORE_SECRET_KEY=your-secret-key ``` Make sure you add `.env` to your `.gitignore` file.
You don't want to commit your secret keys to your repository.
### Backend Now we can create the API endpoint in our Astro app. Create a file at `src/pages/api/edgestore/[...edgestore].ts`: ```ts import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreAstroHandler } from '@edgestore/server/adapters/astro'; export const prerender = false; const es = initEdgeStore.create(); const edgeStoreRouter = es.router({ publicFiles: es.fileBucket(), }); const handler = createEdgeStoreAstroHandler({ router: edgeStoreRouter, }); export { handler as GET, handler as POST }; export type EdgeStoreRouter = typeof edgeStoreRouter; ``` The example above is the simplest bucket you can create with EdgeStore. Just a simple file bucket with no validation that will be accessible by anyone with the link. You can have multiple buckets in your app, each with its own configuration. ### Frontend Now let's create our context provider: ```tsx title="src/lib/edgestore.ts" import { createEdgeStoreProvider } from '@edgestore/react'; import type { EdgeStoreRouter } from '../pages/api/edgestore/[...edgestore].ts'; const { EdgeStoreProvider, useEdgeStore } = createEdgeStoreProvider(); export { EdgeStoreProvider, useEdgeStore }; ``` And then let's create out upload component: ```tsx title="src/components/FileUploader.tsx" import { useState } from 'react'; import { EdgeStoreProvider, useEdgeStore } from '../lib/edgestore'; export function FileUploader() { return ( ); } function FileUploaderInner() { const [file, setFile] = useState(null); const { edgestore } = useEdgeStore(); return (
{ setFile(e.target.files?.[0] ?? null); }} />
); } ``` Finally, let's use the `FileUploader` component in our app. ```astro title="src/pages/index.astro" --- import { FileUploader } from '../components/FileUploader'; --- {/* ... */}

EdgeStore with Astro

``` ## Usage To upload or use the other functionalities of EdgeStore, you can look at the main [Quick Start](/docs/quick-start) guide. The usage should be the same. # EdgeStore Docs: Express URL: /docs/adapters/express Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/adapters/express.mdx *** title: Express description: Learn how to integrate EdgeStore with your Express.js backend and React frontend. ---------------------------------------------------------------------------------------------- Some apps are built with React on the frontend (e.g. using `create-react-app` or `vite`) and have an `express.js` backend. You can also use EdgeStore in these cases, even without using Next.js. ## Setup ### Install Let's start by installing the required packages. ```bash npm install @edgestore/server @edgestore/react zod ``` ```bash pnpm add @edgestore/server @edgestore/react zod ``` ```bash yarn add @edgestore/server @edgestore/react zod ``` ```bash bun add @edgestore/server @edgestore/react zod ``` ### Environment Variables Then go to your [Dashboard](https://dashboard.edgestore.dev), create a new project and copy the keys to your environment variables. ```sh title=".env" EDGE_STORE_ACCESS_KEY=your-access-key EDGE_STORE_SECRET_KEY=your-secret-key ``` Make sure you add `.env` to your `.gitignore` file.
You don't want to commit your secret keys to your repository.
### Backend Now we can create the backend code in our express app. The example below is the simplest bucket you can create with EdgeStore. Just a simple file bucket with no validation that will be accessible by anyone with the link. You can have multiple buckets in your app, each with its own configuration. ```ts import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreExpressHandler } from '@edgestore/server/adapters/express'; import bodyParser from 'body-parser'; import cookieParser from 'cookie-parser'; import cors from 'cors'; import express from 'express'; // --- EXPRESS CONFIG --- const PORT = process.env.PORT ?? 3001; const app = express(); /** * Your express app is probably running in a different port than your frontend app. * To avoid CORS issues, we should use the cors middleware. */ app.use( cors({ // Change this to your frontend origin for better security origin: true, credentials: true, }), ); /** * EdgeStore uses cookies to store the context token. * We need to use the cookie parser middleware to parse the cookies. */ app.use(cookieParser()); /** * We need to have access to the json request body. * We can use the body parser middleware to parse the request. */ app.use(bodyParser.json()); // --- EDGESTORE ROUTER CONFIG --- const es = initEdgeStore.create(); const edgeStoreRouter = es.router({ publicFiles: es.fileBucket(), }); export type EdgeStoreRouter = typeof edgeStoreRouter; const handler = createEdgeStoreExpressHandler({ router: edgeStoreRouter, }); // --- EXPRESS ROUTES --- app.get('/', (req, res) => { console.log(req), res.send('Hello from server!'); }); // set the get and post routes for the edgestore router app.get('/edgestore/*', handler); app.post('/edgestore/*', handler); app.listen(PORT, () => { console.log(`⚡Server is running here 👉 http://localhost:${PORT}`); }); ``` ### Frontend Now let's initiate our context provider in the frontend app. ```tsx title="src/lib/edgestore.ts" // You can import it from the other project if it's just the type import { type EdgeStoreRouter } from '../../../path/to/express-backend/src'; import { createEdgeStoreProvider } from '@edgestore/react'; const { EdgeStoreProvider, useEdgeStore } = createEdgeStoreProvider(); export { EdgeStoreProvider, useEdgeStore }; ``` And then wrap our app with the provider. ```tsx title="src/App.tsx" // [!code ++] import { EdgeStoreProvider } from '../lib/edgestore'; function App() { return ( {/* [!code ++] */} {/* Rest of your app */} {/* [!code ++] */} ); } ``` ## Usage To upload or use the other functionalities of EdgeStore, you can look the main [Quick Start](/docs/quick-start) guide. The usage should be the same. ## Limitations For EdgeStore to work properly in your deployed production app, Your frontend and backend should be in the same domain. If you are deploying to Vercel, you can take a look at the [Rewrites settings](https://vercel.com/docs/edge-network/rewrites). In case you are using Apache or Nginx, you can setup a reverse proxy to make sure your frontend and backend are in the same domain. # EdgeStore Docs: Fastify URL: /docs/adapters/fastify Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/adapters/fastify.mdx *** title: Fastify description: Learn how to integrate EdgeStore with your Fastify backend and React frontend. ------------------------------------------------------------------------------------------- Some applications are built with React on the frontend (e.g. using `create-react-app` or `vite`) and have a `fastify` backend. You can use EdgeStore in these cases, even without using Next.js. ## Setup ### Install Let's start by installing the required packages. ```bash npm install @edgestore/server @edgestore/react zod ``` ```bash pnpm add @edgestore/server @edgestore/react zod ``` ```bash yarn add @edgestore/server @edgestore/react zod ``` ```bash bun add @edgestore/server @edgestore/react zod ``` ### Environment Variables Then go to your [Dashboard](https://dashboard.edgestore.dev), create a new project and copy the keys to your environment variables. ```sh title=".env" EDGE_STORE_ACCESS_KEY=your-access-key EDGE_STORE_SECRET_KEY=your-secret-key ``` Make sure you add `.env` to your `.gitignore` file.
You don't want to commit your secret keys to your repository.
### Backend Now we can create the backend code in our Fastify app. The example below is the simplest bucket you can create with EdgeStore. Just a simple file bucket with no validation that will be accessible by anyone with the link. You can have multiple buckets in your app, each with its own configuration. ```ts import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreFastifyHandler } from '@edgestore/server/adapters/fastify'; import cors from '@fastify/cors'; import cookie from '@fastify/cookie'; import fastify from 'fastify'; // --- FASTIFY CONFIG --- const PORT = process.env.PORT ?? 3001; const app = fastify(); /** * Your fastify app is probably running in a different port than your frontend app. * To avoid CORS issues, we should use the cors plugin. */ await app.register(cors, { // Change this to your frontend origin for better security origin: true, credentials: true, }); /** * EdgeStore uses cookies to store the context token. * We need to use the cookie plugin to parse the cookies. */ await app.register(cookie); // --- EDGESTORE ROUTER CONFIG --- const es = initEdgeStore.create(); const edgeStoreRouter = es.router({ publicFiles: es.fileBucket(), }); export type EdgeStoreRouter = typeof edgeStoreRouter; const handler = createEdgeStoreFastifyHandler({ router: edgeStoreRouter, }); // --- FASTIFY ROUTES --- app.get('/', (request, reply) => { console.log('Request received'); reply.send('Hello from server!'); }); // set the get and post routes for the edgestore router app.get('/edgestore/*', handler); app.post('/edgestore/*', handler); app.listen({ port: PORT }, (err) => { if (err) { console.error(err); process.exit(1); } console.log(`⚡Server is running here 👉 http://localhost:${PORT}`); }); ``` ### Frontend Now let's initiate our context provider in the frontend app. ```tsx title="src/lib/edgestore.ts" // You can import it from the other project if it's just the type import { type EdgeStoreRouter } from '../../../path/to/fastify-backend/src'; import { createEdgeStoreProvider } from '@edgestore/react'; const { EdgeStoreProvider, useEdgeStore } = createEdgeStoreProvider(); export { EdgeStoreProvider, useEdgeStore }; ``` And then wrap our app with the provider. ```tsx title="src/App.tsx" // [!code ++] import { EdgeStoreProvider } from '../lib/edgestore'; function App() { return ( {/* [!code ++] */} {/* Rest of your app */} {/* [!code ++] */} ); } ``` ## Usage To upload or use the other functionalities of EdgeStore, you can look at the main [Quick Start](/docs/quick-start) guide. The usage should be the same. ## Limitations For EdgeStore to work properly in your deployed production app, Your frontend and backend should be in the same domain. If you are deploying to Vercel, you can take a look at the [Rewrites settings](https://vercel.com/docs/edge-network/rewrites). In case you are using Apache or Nginx, you can set up a reverse proxy to make sure your frontend and backend are in the same domain. # EdgeStore Docs: Hono URL: /docs/adapters/hono Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/adapters/hono.mdx *** title: Hono description: Learn how to integrate EdgeStore with your Hono backend and React frontend. ---------------------------------------------------------------------------------------- Some applications are built with React on the frontend (e.g. using `create-react-app` or `vite`) and have a `hono` backend. You can use EdgeStore in these cases, even without using Next.js. ## Setup ### Install Let's start by installing the required packages. ```bash npm install @edgestore/server @edgestore/react zod ``` ```bash pnpm add @edgestore/server @edgestore/react zod ``` ```bash yarn add @edgestore/server @edgestore/react zod ``` ```bash bun add @edgestore/server @edgestore/react zod ``` ### Environment Variables Then go to your [Dashboard](https://dashboard.edgestore.dev), create a new project and copy the keys to your environment variables. ```sh title=".env" EDGE_STORE_ACCESS_KEY=your-access-key EDGE_STORE_SECRET_KEY=your-secret-key ``` Make sure you add `.env` to your `.gitignore` file.
You don't want to commit your secret keys to your repository.
### Backend Now we can create the backend code in our Hono app. The example below is the simplest bucket you can create with EdgeStore. Just a simple file bucket with no validation that will be accessible by anyone with the link. You can have multiple buckets in your app, each with its own configuration. ```ts import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreHonoHandler } from '@edgestore/server/adapters/hono'; import { serve } from '@hono/node-server'; import { Hono } from 'hono'; import { cors } from 'hono/cors'; // --- HONO CONFIG --- const PORT = process.env.PORT ?? 3001; const app = new Hono(); /** * Your Hono app is probably running in a different port than your frontend app. * To avoid CORS issues, we should use the cors middleware. */ app.use( '*', cors({ // Change this to your frontend origin for better security origin: (origin) => origin, credentials: true, }), ); // --- EDGESTORE ROUTER CONFIG --- const es = initEdgeStore.create(); const edgeStoreRouter = es.router({ publicFiles: es.fileBucket(), }); export type EdgeStoreRouter = typeof edgeStoreRouter; const handler = createEdgeStoreHonoHandler({ router: edgeStoreRouter, }); // --- HONO ROUTES --- app.get('/', (c) => { return c.text('Hello from Hono server!'); }); // Route for EdgeStore app.all('/edgestore/*', handler); // Start the server serve( { fetch: app.fetch, port: Number(PORT), }, (info) => { console.log(`⚡Server is running here 👉 http://localhost:${info.port}`); }, ); ``` ### Frontend Now let's initiate our context provider in the frontend app. ```tsx title="src/lib/edgestore.ts" // You can import it from the other project if it's just the type import { type EdgeStoreRouter } from '../../../path/to/hono-backend/src'; import { createEdgeStoreProvider } from '@edgestore/react'; const { EdgeStoreProvider, useEdgeStore } = createEdgeStoreProvider(); export { EdgeStoreProvider, useEdgeStore }; ``` And then wrap our app with the provider. ```tsx title="src/App.tsx" // [!code ++] import { EdgeStoreProvider } from '../lib/edgestore'; function App() { return ( {/* [!code ++] */} {/* Rest of your app */} {/* [!code ++] */} ); } ``` ## Usage To upload or use the other functionalities of EdgeStore, you can look at the main [Quick Start](/docs/quick-start) guide. The usage should be the same. ## Limitations For EdgeStore to work properly in your deployed production app, Your frontend and backend should be in the same domain. If you are deploying to Vercel, you can take a look at the [Rewrites settings](https://vercel.com/docs/edge-network/rewrites). In case you are using Apache or Nginx, you can set up a reverse proxy to make sure your frontend and backend are in the same domain. # EdgeStore Docs: Next Adapter URL: /docs/adapters/next Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/adapters/next.mdx *** title: Next Adapter description: An overview of the EdgeStore Next.js adapters for App Router and Pages Router. ------------------------------------------------------------------------------------------- There are two adapters available for Next.js. The `next-app` (for the app-router) and the `next-pages` (for the pages-router). The steps to set up the adapter are in the main [Quick Start](/docs/quick-start) guide. # EdgeStore Docs: Remix URL: /docs/adapters/remix Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/adapters/remix.mdx *** title: Remix description: Learn how to integrate EdgeStore with your Remix applications, enabling robust file management capabilities. ------------------------------------------------------------------------------------------------------------------------- React Router integrates seamlessly with EdgeStore, enabling you to build type-safe, full-stack React applications with robust file management capabilities. ## Setup ### Install Let's start by installing the required packages. ```bash npm install @edgestore/server @edgestore/react zod ``` ```bash pnpm add @edgestore/server @edgestore/react zod ``` ```bash yarn add @edgestore/server @edgestore/react zod ``` ```bash bun add @edgestore/server @edgestore/react zod ``` ### Environment Variables Then go to your [Dashboard](https://dashboard.edgestore.dev), create a new project and copy the keys to your environment variables. ```sh title=".env" EDGE_STORE_ACCESS_KEY=your-access-key EDGE_STORE_SECRET_KEY=your-secret-key ``` Make sure you add `.env` to your `.gitignore` file.
You don't want to commit your secret keys to your repository.
### Backend In your Remix application, create an API route for EdgeStore with the following content: ```ts title="app/routes/api/edgestore.ts" import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreRemixHandler } from '@edgestore/server/adapters/remix'; const es = initEdgeStore.create(); const edgeStoreRouter = es.router({ publicFiles: es.fileBucket(), }); export type EdgeStoreRouter = typeof edgeStoreRouter; const handler = createEdgeStoreRemixHandler({ router: edgeStoreRouter, }); export { handler as loader, handler as action }; ``` Add the EdgeStore API route to your routes file. ```ts title="app/routes.ts" import { index, route, type RouteConfig } from '@react-router/dev/routes'; export default [ index('routes/home.tsx'), // [!code ++] route('api/edgestore/*', 'routes/api/edgestore.ts'), ] satisfies RouteConfig; ``` ### Frontend Now let's initiate our context provider in the frontend app. ```tsx title="app/lib/edgestore.ts" import { createEdgeStoreProvider } from '@edgestore/react'; import { type EdgeStoreRouter } from '~/routes/api/edgestore'; const { EdgeStoreProvider, useEdgeStore } = createEdgeStoreProvider(); export { EdgeStoreProvider, useEdgeStore }; ``` And then wrap your app with the provider in your root component. ```tsx title="app/root.tsx" // [!code ++] import { EdgeStoreProvider } from '~/lib/edgestore'; // ... export default function App() { return ( {/* [!code ++] */} {/* [!code ++] */} ); } ``` ## Usage To upload or use the other functionalities of EdgeStore, you can look the main [Quick Start](/docs/quick-start) guide. The usage should be the same. # EdgeStore Docs: TanStack Start URL: /docs/adapters/tanstack-start Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/adapters/tanstack-start.mdx *** title: TanStack Start description: Learn how to integrate EdgeStore with your TanStack Start applications for type-safe, full-stack React file management. ------------------------------------------------------------------------------------------------------------------------------------ TanStack Start integrates seamlessly with EdgeStore, enabling you to build type-safe, full-stack React applications with robust file management capabilities. ## Setup ### Install Let's start by installing the required packages. ```bash npm install @edgestore/server @edgestore/react zod ``` ```bash pnpm add @edgestore/server @edgestore/react zod ``` ```bash yarn add @edgestore/server @edgestore/react zod ``` ```bash bun add @edgestore/server @edgestore/react zod ``` ### Environment Variables Then go to your [Dashboard](https://dashboard.edgestore.dev), create a new project and copy the keys to your environment variables. ```sh title=".env" EDGE_STORE_ACCESS_KEY=your-access-key EDGE_STORE_SECRET_KEY=your-secret-key ``` Make sure you add `.env` to your `.gitignore` file.
You don't want to commit your secret keys to your repository.
### Backend In your TanStack Start application, create an API route for EdgeStore with the following content: ```ts title="app/routes/api/edgestore.$.ts" import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreStartHandler } from '@edgestore/server/adapters/start'; import { createAPIFileRoute } from '@tanstack/start/api'; const es = initEdgeStore.create(); const edgeStoreRouter = es.router({ publicFiles: es.fileBucket(), }); export type EdgeStoreRouter = typeof edgeStoreRouter; const handler = createEdgeStoreStartHandler({ router: edgeStoreRouter, }); export const APIRoute = createAPIFileRoute('/api/edgestore/$')({ GET: handler, POST: handler, }); ``` ### Frontend Now let's initiate our context provider in the frontend app. ```tsx title="app/utils/edgestore.ts" import { createEdgeStoreProvider } from '@edgestore/react'; import { type EdgeStoreRouter } from '../routes/api/edgestore.$'; const { EdgeStoreProvider, useEdgeStore } = createEdgeStoreProvider(); export { EdgeStoreProvider, useEdgeStore }; ``` And then wrap our app with the provider. ```tsx title="app/routes/__root.tsx" // [!code ++] import { EdgeStoreProvider } from '~/utils/edgestore'; // ... function RootComponent() { return ( {/* [!code ++] */} {/* [!code ++] */} ); } ``` ## Usage To upload or use the other functionalities of EdgeStore, you can look the main [Quick Start](/docs/quick-start) guide. The usage should be the same. # EdgeStore Docs: Dropzone URL: /docs/components/dropzone Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/components/dropzone.mdx *** title: Dropzone description: The Dropzone component provides a user-friendly drag-and-drop interface for file uploads that integrates seamlessly with the UploaderProvider. ----------------------------------------------------------------------------------------------------------------------------------------------------------- import { LimitedCode } from '@/components/ui/limited-code'; import { OpenTabs, OpenTabsContent, OpenTabsList, OpenTabsTrigger, } from '@/components/ui/open-tabs'; import { Callout } from 'fumadocs-ui/components/callout'; If you are installing the other dropzone components via the CLI, this component will be installed automatically. You can skip the following steps. ## Installation CLI Manual ```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 ``` ### Copy this component ````tsx 'use client'; import { cn } from '@/lib/utils'; import { AlertCircleIcon, UploadCloudIcon } from 'lucide-react'; import * as React from 'react'; import { useDropzone, type DropzoneOptions } from 'react-dropzone'; import { formatFileSize, useUploader } from './uploader-provider'; const DROPZONE_VARIANTS = { base: 'relative rounded-md p-4 w-full flex justify-center items-center flex-col cursor-pointer border-2 border-dashed border-gray-400 dark:border-gray-600 transition-colors duration-200 ease-in-out', active: 'border-blue-500 dark:border-blue-400', disabled: 'bg-gray-100 dark:bg-gray-800 border-gray-400/50 dark:border-gray-600/50 cursor-default pointer-events-none opacity-50', accept: 'border-blue-500 dark:border-blue-400 bg-blue-100 dark:bg-blue-900/30', reject: 'border-red-500 dark:border-red-400 bg-red-100 dark:bg-red-900/30', }; /** * Props for the Dropzone component. * * @interface DropzoneProps * @extends {React.HTMLAttributes} */ export interface DropzoneProps extends React.HTMLAttributes { /** * Options passed to the underlying react-dropzone component. * Cannot include 'disabled' or 'onDrop' as they are handled internally. */ dropzoneOptions?: Omit; /** * Whether the dropzone is disabled. */ disabled?: boolean; /** * Message shown when files are being dragged over the dropzone. */ dropMessageActive?: string; /** * Default message shown when the dropzone is idle. */ dropMessageDefault?: string; } /** * A dropzone component for file uploads that integrates with the UploaderProvider. * * @component * @example * ```tsx * * ``` */ const Dropzone = React.forwardRef( ( { dropzoneOptions, className, disabled, dropMessageActive = 'Drop files here...', dropMessageDefault = 'drag & drop files here, or click to select', ...props }, ref, ) => { const { fileStates, addFiles } = useUploader(); const [error, setError] = React.useState(); const maxFiles = dropzoneOptions?.maxFiles; const maxSize = dropzoneOptions?.maxSize; const isMaxFilesReached = !!maxFiles && fileStates.length >= maxFiles; const isDisabled = disabled ?? isMaxFilesReached; const { getRootProps, getInputProps, isDragActive, isFocused, isDragAccept, isDragReject, } = useDropzone({ disabled: isDisabled, onDrop: (acceptedFiles, rejectedFiles) => { setError(undefined); // Handle rejections first if (rejectedFiles.length > 0) { if (rejectedFiles[0]?.errors[0]) { const error = rejectedFiles[0].errors[0]; const code = error.code; const messages: Record = { 'file-too-large': `The file is too large. Max size is ${formatFileSize( maxSize ?? 0, )}.`, 'file-invalid-type': 'Invalid file type.', 'too-many-files': `You can only add ${ maxFiles ?? 'multiple' } file(s).`, default: 'The file is not supported.', }; setError(messages[code] ?? messages.default); } return; // Exit early if there are any rejections } // Handle accepted files if (acceptedFiles.length === 0) return; // Check if adding these files would exceed maxFiles limit if (maxFiles) { const remainingSlots = maxFiles - fileStates.length; // If adding all files would exceed the limit, reject them all if (acceptedFiles.length > remainingSlots) { setError(`You can only add ${maxFiles} file(s).`); return; } } addFiles(acceptedFiles); }, ...dropzoneOptions, }); const dropZoneClassName = React.useMemo( () => cn( DROPZONE_VARIANTS.base, isFocused && DROPZONE_VARIANTS.active, isDisabled && DROPZONE_VARIANTS.disabled, isDragReject && DROPZONE_VARIANTS.reject, isDragAccept && DROPZONE_VARIANTS.accept, className, ), [isFocused, isDisabled, isDragAccept, isDragReject, className], ); return (
{isDragActive ? dropMessageActive : dropMessageDefault}
{(!!maxSize || !!maxFiles) && (
{maxFiles && maxFiles > 1 ? `Up to ${maxFiles} files` : ''} {maxFiles && maxFiles > 1 && maxSize ? ', ' : ''} {maxSize && `Max size: ${formatFileSize(maxSize)}`}
)}
{/* Error Text */} {error && (
{error}
)}
); }, ); Dropzone.displayName = 'Dropzone'; export { Dropzone }; ````
## Usage This section provides a guide on how to use the `Dropzone` component with the `UploaderProvider`. ### Basic Usage The Dropzone component must be used within an `UploaderProvider`. ```tsx import { Dropzone } from '@/components/ui/dropzone'; import { UploaderProvider } from '@/components/ui/uploader-provider'; import * as React from 'react'; export default function DropzoneExample() { 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 */} ); } ``` # EdgeStore Docs: Image URL: /docs/components/image Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/components/image.mdx *** title: Image description: A dropzone component for uploading images ------------------------------------------------------ import { DemoBlock } from '@/components/demo-block'; import { LimitedCode } from '@/components/ui/limited-code'; import { OpenTabs, OpenTabsContent, OpenTabsList, OpenTabsTrigger, } from '@/components/ui/open-tabs'; import SingleImageUploaderBlock from '@/components/upload/blocks/single-image-block.tsx'; import { Step, Steps } from 'fumadocs-ui/components/steps'; ## Installation CLI Manual Use the shadcn CLI to add the component to your project. ```bash npx shadcn@latest add https://edgestore.dev/r/single-image-dropzone.json ``` ```bash pnpm dlx shadcn@latest add https://edgestore.dev/r/single-image-dropzone.json ``` ```bash yarn dlx shadcn@latest add https://edgestore.dev/r/single-image-dropzone.json ``` ```bash bun x shadcn@latest add https://edgestore.dev/r/single-image-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) * [progress-circle](./progress-circle) ### Copy this component ````tsx 'use client'; import { cn } from '@/lib/utils'; import { AlertCircleIcon, Trash2Icon, UploadCloudIcon, XIcon, } from 'lucide-react'; import * as React from 'react'; import { useDropzone, type DropzoneOptions } from 'react-dropzone'; import { ProgressCircle } from './progress-circle'; import { formatFileSize, useUploader } from './uploader-provider'; const DROPZONE_VARIANTS = { base: 'relative rounded-md p-4 flex justify-center items-center flex-col cursor-pointer min-h-[150px] min-w-[200px] border-2 border-dashed border-gray-400 dark:border-gray-600 transition-colors duration-200 ease-in-out', image: 'border-0 p-0 min-h-0 min-w-0 relative bg-gray-100 dark:bg-gray-800 shadow-md', active: 'border-blue-500 dark:border-blue-400', disabled: 'bg-gray-100/50 dark:bg-gray-800/50 border-gray-400/50 dark:border-gray-600/50 cursor-default pointer-events-none', accept: 'border-blue-500 dark:border-blue-400 bg-blue-100 dark:bg-blue-900/30', reject: 'border-red-500 dark:border-red-400 bg-red-100 dark:bg-red-900/30', }; /** * Props for the SingleImageDropzone component. * * @interface SingleImageDropzoneProps * @extends {React.HTMLAttributes} */ export interface SingleImageDropzoneProps extends React.HTMLAttributes { /** * The width of the dropzone area in pixels. */ width: number; /** * The height of the dropzone area in pixels. */ height: number; /** * Whether the dropzone is disabled. */ disabled?: boolean; /** * Options passed to the underlying react-dropzone component. * Cannot include 'disabled', 'onDrop', 'maxFiles', or 'multiple' as they are handled internally. */ dropzoneOptions?: Omit< DropzoneOptions, 'disabled' | 'onDrop' | 'maxFiles' | 'multiple' >; } /** * A single image upload component with preview and upload status. * * This component allows users to upload a single image, shows a preview, * displays upload progress, and provides controls to remove or cancel the upload. * * @component * @example * ```tsx * * ``` */ const SingleImageDropzone = React.forwardRef< HTMLInputElement, SingleImageDropzoneProps >(({ dropzoneOptions, width, height, className, disabled, ...props }, ref) => { const { fileStates, addFiles, removeFile, cancelUpload } = useUploader(); const [error, setError] = React.useState(); const fileState = React.useMemo(() => fileStates[0], [fileStates]); const maxSize = dropzoneOptions?.maxSize; // Create temporary URL for image preview before upload is complete const tempUrl = React.useMemo(() => { if (fileState?.file) { return URL.createObjectURL(fileState.file); } return null; }, [fileState]); // Clean up temporary URL to prevent memory leaks React.useEffect(() => { return () => { if (tempUrl) { URL.revokeObjectURL(tempUrl); } }; }, [tempUrl]); const displayUrl = tempUrl ?? fileState?.url; const isDisabled = !!disabled || fileState?.status === 'UPLOADING' || fileState?.status === 'COMPLETE'; // Disable when upload complete const { getRootProps, getInputProps, isFocused, isDragAccept, isDragReject } = useDropzone({ accept: { 'image/*': [] }, // Accept only image files multiple: false, disabled: isDisabled, onDrop: (acceptedFiles, rejectedFiles) => { setError(undefined); // Handle rejections first if (rejectedFiles.length > 0) { if (rejectedFiles[0]?.errors[0]) { const error = rejectedFiles[0].errors[0]; const code = error.code; // User-friendly error messages const messages: Record = { 'file-too-large': `The file is too large. Max size is ${formatFileSize( maxSize ?? 0, )}.`, 'file-invalid-type': 'Invalid file type.', 'too-many-files': 'You can only upload one file.', default: 'The file is not supported.', }; setError(messages[code] ?? messages.default); } return; // Exit early if there are any rejections } // Handle accepted files only if there are no rejections if (acceptedFiles.length > 0) { // Remove existing file before adding a new one if (fileStates[0]) { removeFile(fileStates[0].key); } addFiles(acceptedFiles); } }, ...dropzoneOptions, }); const dropZoneClassName = React.useMemo( () => cn( DROPZONE_VARIANTS.base, isFocused && DROPZONE_VARIANTS.active, isDisabled && DROPZONE_VARIANTS.disabled, displayUrl && DROPZONE_VARIANTS.image, isDragReject && DROPZONE_VARIANTS.reject, isDragAccept && DROPZONE_VARIANTS.accept, className, ), [isFocused, isDisabled, displayUrl, isDragAccept, isDragReject, className], ); // Combined error message from dropzone or file state const errorMessage = error ?? fileState?.error; return (
{displayUrl ? ( {fileState?.file.name ) : ( // Placeholder content shown when no image is selected
drag & drop an image or click to select
{maxSize && (
Max size: {formatFileSize(maxSize)}
)}
)} {/* Upload progress overlay */} {displayUrl && fileState?.status === 'UPLOADING' && (
)} {/* Remove/Cancel button */} {displayUrl && !disabled && fileState && fileState.status !== 'COMPLETE' && ( )}
{/* Error message display */} {errorMessage && (
{errorMessage}
)}
); }); SingleImageDropzone.displayName = 'SingleImageDropzone'; export { SingleImageDropzone }; ````
## Usage ```tsx 'use client'; import { SingleImageDropzone } from '@/components/upload/single-image'; import { UploaderProvider, type UploadFn, } from '@/components/upload/uploader-provider'; import { useEdgeStore } from '@/lib/edgestore'; import * as React from 'react'; export function SingleImageDropzoneUsage() { 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 ( ); } ``` # EdgeStore Docs: Manual Installation Setup URL: /docs/components/manual-install Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/components/manual-install.mdx *** title: Manual Installation Setup description: If you're not using shadcn/ui, you can install the components manually. Follow the steps below to prepare for the components manual installation. -------------------------------------------------------------------------------------------------------------------------------------------------------------- import { OpenTabs, OpenTabsContent, OpenTabsList, OpenTabsTrigger, } from '@/components/ui/open-tabs'; import { Callout } from 'fumadocs-ui/components/callout'; If you're not using shadcn/ui, you can install the components manually. Follow the steps below to prepare for the components manual installation. Components installed manually do not use shadcn/ui specific colors. ## Install dependencies ```bash npm install tailwind-merge clsx react-dropzone lucide-react ``` ```bash pnpm add tailwind-merge clsx react-dropzone lucide-react ``` ```bash yarn add tailwind-merge clsx react-dropzone lucide-react ``` ```bash bun add tailwind-merge clsx react-dropzone lucide-react ``` ## Add the cn helper ```ts title="src/lib/utils.ts" import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } ``` ## Start adding the components Now you can follow the manual installation guide for the components you need. # EdgeStore Docs: Multi-file URL: /docs/components/multi-file Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/components/multi-file.mdx *** title: Multi-file description: A component for uploading multiple files with preview and progress indicators. ------------------------------------------------------------------------------------------- import { DemoBlock } from '@/components/demo-block'; import { LimitedCode } from '@/components/ui/limited-code'; import { OpenTabs, OpenTabsContent, OpenTabsList, OpenTabsTrigger, } from '@/components/ui/open-tabs'; import MultiFileUploaderBlock from '@/components/upload/blocks/multi-file-block'; // Assumed path import { Step, Steps } from 'fumadocs-ui/components/steps'; ## Installation CLI Manual Use the shadcn CLI to add the component to your project. ```bash npx shadcn@latest add https://edgestore.dev/r/file-uploader.json ``` ```bash pnpm dlx shadcn@latest add https://edgestore.dev/r/file-uploader.json ``` ```bash yarn dlx shadcn@latest add https://edgestore.dev/r/file-uploader.json ``` ```bash bun x shadcn@latest add https://edgestore.dev/r/file-uploader.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) * [progress-bar](./progress-bar) * [dropzone](./dropzone) ### Copy this component ````tsx 'use client'; import { cn } from '@/lib/utils'; import { AlertCircleIcon, CheckCircleIcon, FileIcon, Trash2Icon, XIcon, } from 'lucide-react'; import * as React from 'react'; import { type DropzoneOptions } from 'react-dropzone'; import { Dropzone } from './dropzone'; import { ProgressBar } from './progress-bar'; import { formatFileSize, useUploader } from './uploader-provider'; /** * Displays a list of files with their upload status, progress, and controls. * * @component * @example * ```tsx * * ``` */ const FileList = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => { const { fileStates, removeFile, cancelUpload } = useUploader(); if (!fileStates.length) return null; return (
{fileStates.map(({ file, abortController, progress, status, key }) => { return (
{file.name}
{formatFileSize(file.size)}
{status === 'ERROR' && (
)} {status === 'UPLOADING' && (
{abortController && ( )}
{Math.round(progress)}%
)} {status !== 'UPLOADING' && status !== 'COMPLETE' && ( )} {status === 'COMPLETE' && ( )}
{/* Progress Bar */} {status === 'UPLOADING' && }
); })}
); }); FileList.displayName = 'FileList'; /** * Props for the FileUploader component. * * @interface FileUploaderProps * @extends {React.HTMLAttributes} */ export interface FileUploaderProps extends React.HTMLAttributes { /** * Maximum number of files allowed. */ maxFiles?: number; /** * Maximum file size in bytes. */ maxSize?: number; /** * Accepted file types. * * @example * ```tsx * accept={{ 'image/*': ['.png', '.jpg', '.jpeg'] }} * ``` */ accept?: DropzoneOptions['accept']; /** * Whether the uploader is disabled. */ disabled?: boolean; /** * Additional className for the dropzone component. */ dropzoneClassName?: string; /** * Additional className for the file list component. */ fileListClassName?: string; /** * Ref for the input element inside the Dropzone. */ inputRef?: React.Ref; } /** * A complete file uploader component with dropzone and file list. * * @component * @example * ```tsx * * ``` */ const FileUploader = React.forwardRef( ( { maxFiles, maxSize, accept, disabled, className, dropzoneClassName, fileListClassName, inputRef, ...props }, ref, ) => { return (
); }, ); FileUploader.displayName = 'FileUploader'; export { FileList, FileUploader }; ````
## Usage ```tsx 'use client'; import { FileUploader } from '@/components/upload/multi-file'; import { UploaderProvider, type UploadFn, } from '@/components/upload/uploader-provider'; import { useEdgeStore } from '@/lib/edgestore'; import * as React from 'react'; export function MultiFileDropzoneUsage() { 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 ( ); } ``` # EdgeStore Docs: Multi-image URL: /docs/components/multi-image Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/components/multi-image.mdx *** title: Multi-image description: A component for uploading multiple images with preview grid and progress indicators. ------------------------------------------------------------------------------------------------- import { DemoBlock } from '@/components/demo-block'; import { LimitedCode } from '@/components/ui/limited-code'; import { OpenTabs, OpenTabsContent, OpenTabsList, OpenTabsTrigger, } from '@/components/ui/open-tabs'; import MultiImageUploaderBlock from '@/components/upload/blocks/multi-image-block'; import { Step, Steps } from 'fumadocs-ui/components/steps'; ## Installation CLI Manual Use the shadcn CLI to add the component to your project. ```bash npx shadcn@latest add https://edgestore.dev/r/image-uploader.json ``` ```bash pnpm dlx shadcn@latest add https://edgestore.dev/r/image-uploader.json ``` ```bash yarn dlx shadcn@latest add https://edgestore.dev/r/image-uploader.json ``` ```bash bun x shadcn@latest add https://edgestore.dev/r/image-uploader.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) * [progress-circle](./progress-circle) * [dropzone](./dropzone) ### Copy this component ````tsx 'use client'; import { cn } from '@/lib/utils'; import { Trash2Icon, XIcon } from 'lucide-react'; import * as React from 'react'; import { type DropzoneOptions } from 'react-dropzone'; import { Dropzone } from './dropzone'; import { ProgressCircle } from './progress-circle'; import { useUploader } from './uploader-provider'; /** * Props for the ImageList component. * * @interface ImageListProps * @extends {React.HTMLAttributes} */ export interface ImageListProps extends React.HTMLAttributes { /** * Whether the image deletion controls should be disabled. */ disabled?: boolean; } /** * Displays a grid of image previews with upload status and controls. * * @component * @example * ```tsx * * ``` */ const ImageList = React.forwardRef( ({ className, disabled: initialDisabled, ...props }, ref) => { const { fileStates, removeFile, cancelUpload } = useUploader(); // Create temporary URLs for image previews const tempUrls = React.useMemo(() => { const urls: Record = {}; fileStates.forEach((fileState) => { if (fileState.file) { urls[fileState.key] = URL.createObjectURL(fileState.file); } }); return urls; }, [fileStates]); // Clean up temporary URLs on unmount React.useEffect(() => { return () => { Object.values(tempUrls).forEach((url) => { URL.revokeObjectURL(url); }); }; }, [tempUrls]); if (!fileStates.length) return null; return (
{fileStates.map((fileState) => { const displayUrl = tempUrls[fileState.key] ?? fileState.url; return (
{displayUrl ? ( {fileState.file.name} ) : (
No Preview
)} {/* Upload progress indicator */} {fileState.status === 'UPLOADING' && (
)} {/* Delete/cancel button */} {displayUrl && !initialDisabled && ( )}
); })}
); }, ); ImageList.displayName = 'ImageList'; /** * Props for the ImageDropzone component. * * @interface ImageDropzoneProps * @extends {React.HTMLAttributes} */ export interface ImageDropzoneProps extends React.HTMLAttributes { /** * Whether the dropzone is disabled. */ disabled?: boolean; /** * Options passed to the underlying Dropzone component. * Cannot include 'disabled' or 'onDrop' as they are handled internally. */ dropzoneOptions?: Omit; /** * Ref for the input element inside the Dropzone. */ inputRef?: React.Ref; } /** * A dropzone component specifically for image uploads. * * @component * @example * ```tsx * * ``` */ const ImageDropzone = React.forwardRef( ({ dropzoneOptions, className, disabled, inputRef, ...props }, ref) => { return (
); }, ); ImageDropzone.displayName = 'ImageDropzone'; /** * Props for the ImageUploader component. * * @interface ImageUploaderProps * @extends {React.HTMLAttributes} */ export interface ImageUploaderProps extends React.HTMLAttributes { /** * Maximum number of images allowed. */ maxFiles?: number; /** * Maximum file size in bytes. */ maxSize?: number; /** * Whether the uploader is disabled. */ disabled?: boolean; /** * Additional className for the dropzone component. */ dropzoneClassName?: string; /** * Additional className for the image list component. */ imageListClassName?: string; /** * Ref for the input element inside the Dropzone. */ inputRef?: React.Ref; } /** * A complete image uploader component with dropzone and image grid preview. * * @component * @example * ```tsx * * ``` */ const ImageUploader = React.forwardRef( ( { maxFiles, maxSize, disabled, className, dropzoneClassName, imageListClassName, inputRef, ...props }, ref, ) => { return (
); }, ); ImageUploader.displayName = 'ImageUploader'; export { ImageList, ImageDropzone, ImageUploader }; ````
## Usage ```tsx 'use client'; import { ImageUploader } from '@/components/upload/multi-image'; import { UploaderProvider, type UploadFn, } from '@/components/upload/uploader-provider'; import { useEdgeStore } from '@/lib/edgestore'; import * as React from 'react'; export function MultiImageDropzoneUsage() { 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 ( ); } ``` # EdgeStore Docs: Progress Bar URL: /docs/components/progress-bar Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/components/progress-bar.mdx *** title: Progress Bar description: A horizontal progress bar component for visualizing completion percentage. --------------------------------------------------------------------------------------- import { DemoBlock } from '@/components/demo-block'; import { LimitedCode } from '@/components/ui/limited-code'; import { OpenTabs, OpenTabsContent, OpenTabsList, OpenTabsTrigger, } from '@/components/ui/open-tabs'; import { ProgressBarUsage } from '@/components/upload/blocks/progress-bar-block'; import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; The `ProgressBar` component displays a horizontal progress indicator, commonly used to show file upload progress. It visually represents a percentage value (0-100) with a filled bar. If you are installing the other dropzone components via the CLI, this component will be installed automatically. You can skip the following steps. ## Installation CLI Manual ```bash npx shadcn@latest add https://edgestore.dev/r/progress-bar.json ``` ```bash pnpm dlx shadcn@latest add https://edgestore.dev/r/progress-bar.json ``` ```bash yarn dlx shadcn@latest add https://edgestore.dev/r/progress-bar.json ``` ```bash bun x shadcn@latest add https://edgestore.dev/r/progress-bar.json ``` ### Setup for manual installation First you will need to add the cn helper following the [manual install setup](./manual-install) guide. ### Copy this component ````tsx import * as React from 'react'; import { cn } from '@/lib/utils'; /** * Props for the ProgressBar component. * * @interface ProgressBarProps * @extends {React.HTMLAttributes} */ export interface ProgressBarProps extends React.HTMLAttributes { /** * The progress value as a percentage (0-100). */ progress: number; } /** * A horizontal progress bar component that visualizes completion percentage. * * @component * @example * ```tsx * * ``` */ const ProgressBar = React.forwardRef( ({ progress, className, ...props }, ref) => { return (
); }, ); ProgressBar.displayName = 'ProgressBar'; export { ProgressBar }; ```` ## Usage Import the component and provide the `progress` prop. ```tsx import { ProgressBar } from '@/components/ui/progress-bar'; function ProgressDemo() { const progress = 75; return (
{/* Default usage */} {/* With custom styling */}
{/* Different progress values */}
); } ``` # EdgeStore Docs: Progress Circle URL: /docs/components/progress-circle Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/components/progress-circle.mdx *** title: Progress Circle description: A circular progress indicator component for visualizing completion percentage. ------------------------------------------------------------------------------------------- import { DemoBlock } from '@/components/demo-block'; import { LimitedCode } from '@/components/ui/limited-code'; import { OpenTabs, OpenTabsContent, OpenTabsList, OpenTabsTrigger, } from '@/components/ui/open-tabs'; import { ProgressCircleUsage } from '@/components/upload/blocks/progress-circle-block'; import { Callout } from 'fumadocs-ui/components/callout'; import { Step, Steps } from 'fumadocs-ui/components/steps'; The `ProgressCircle` component displays a circular progress indicator, commonly used to show file upload progress. It visually represents a percentage value (0-100) with a ring and a centered text label. If you are installing the other dropzone components via the CLI, this component will be installed automatically. You can skip the following steps. ## Installation CLI Manual ```bash npx shadcn@latest add https://edgestore.dev/r/progress-circle.json ``` ```bash pnpm dlx shadcn@latest add https://edgestore.dev/r/progress-circle.json ``` ```bash yarn dlx shadcn@latest add https://edgestore.dev/r/progress-circle.json ``` ```bash bun x shadcn@latest add https://edgestore.dev/r/progress-circle.json ``` ### Setup for manual installation First you will need to add the cn helper following the [manual install setup](./manual-install) guide. ### Copy this component ````tsx import { cn } from '@/lib/utils'; import * as React from 'react'; /** * Props for the ProgressCircle component. * * @interface ProgressCircleProps * @extends {React.HTMLAttributes} */ export interface ProgressCircleProps extends React.HTMLAttributes { /** * The progress value as a percentage (0-100). */ progress: number; /** * The diameter of the circle in pixels. * @default 48 */ size?: number; /** * The width of the progress stroke in pixels. * @default 4 */ strokeWidth?: number; } /** * A circular progress indicator component that visualizes completion percentage. * * @component * @example * ```tsx * * * ``` */ const ProgressCircle = React.forwardRef( ({ progress, size = 48, strokeWidth = 4, className, ...props }, ref) => { const radius = (size - strokeWidth) / 2; const circumference = 2 * Math.PI * radius; const offset = circumference - (progress / 100) * circumference; return (
{/* Background track */} {/* Progress arc */} {/* Progress Percentage Text (centered visually) */}
{Math.round(progress)}%
); }, ); ProgressCircle.displayName = 'ProgressCircle'; export { ProgressCircle }; ````
## Usage Import the component and provide the `progress` prop. You can optionally customize the `size` and `strokeWidth`. ```tsx import { ProgressCircle } from '@/components/ui/progress-circle'; function ProgressDemo() { const progress = 75; return (
{/* Default size */} {/* Larger size with thicker stroke */} {/* Smaller size */}
); } ``` # EdgeStore Docs: Uploader Provider URL: /docs/components/uploader-provider Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/components/uploader-provider.mdx *** title: Uploader Provider description: The Uploader Provider is a context provider that provides the uploader context to the components. You can use it as a base for creating your own components. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- import { LimitedCode } from '@/components/ui/limited-code'; import { OpenTabs, OpenTabsContent, OpenTabsList, OpenTabsTrigger, } from '@/components/ui/open-tabs'; import { Callout } from 'fumadocs-ui/components/callout'; If you are installing the other dropzone components via the CLI, this component will be installed automatically. You can skip the following steps. ## Installation CLI Manual ```bash npx shadcn@latest add https://edgestore.dev/r/uploader-provider.json ``` ```bash pnpm dlx shadcn@latest add https://edgestore.dev/r/uploader-provider.json ``` ```bash yarn dlx shadcn@latest add https://edgestore.dev/r/uploader-provider.json ``` ```bash bun x shadcn@latest add https://edgestore.dev/r/uploader-provider.json ``` ### Copy this component ````tsx 'use client'; import * as React from 'react'; /** * Represents the possible statuses of a file in the uploader. */ export type FileStatus = 'PENDING' | 'UPLOADING' | 'COMPLETE' | 'ERROR'; /** * Represents the state of a file in the uploader. */ export type FileState = { /** The file object being uploaded */ file: File; /** Unique identifier for the file */ key: string; /** Upload progress (0-100) */ progress: number; /** Current status of the file */ status: FileStatus; /** URL of the uploaded file (available when status is COMPLETE) */ url?: string; /** Error message if the upload failed */ error?: string; /** AbortController to cancel the upload */ abortController?: AbortController; /** Whether the file should be automatically uploaded */ autoUpload?: boolean; }; /** * Represents a file that has completed uploading. */ export type CompletedFileState = Omit & { /** Status is guaranteed to be 'COMPLETE' */ status: 'COMPLETE'; /** URL is guaranteed to be available */ url: string; }; /** * Function type for handling file uploads. */ export type UploadFn = (props: { /** The file to be uploaded */ file: File; /** AbortSignal to cancel the upload */ signal: AbortSignal; /** Callback to update progress */ onProgressChange: (progress: number) => void | Promise; /** Additional options */ options?: TOptions; }) => Promise<{ url: string }>; /** * Context type for the UploaderProvider. */ type UploaderContextType = { /** List of all files in the uploader */ fileStates: FileState[]; /** Add files to the uploader */ addFiles: (files: File[]) => void; /** Update a file's state */ updateFileState: (key: string, changes: Partial) => void; /** Remove a file from the uploader */ removeFile: (key: string) => void; /** Cancel an ongoing upload */ cancelUpload: (key: string) => void; /** Start uploading files */ uploadFiles: (keysToUpload?: string[], options?: TOptions) => Promise; /** Reset all files */ resetFiles: () => void; /** Whether any file is currently uploading */ isUploading: boolean; /** Whether files should be automatically uploaded */ autoUpload?: boolean; }; /** * Props for the UploaderProvider component. */ type ProviderProps = { /** React children or render function */ children: | React.ReactNode | ((context: UploaderContextType) => React.ReactNode); /** Callback when files change */ onChange?: (args: { allFiles: FileState[]; completedFiles: CompletedFileState[]; }) => void | Promise; /** Callback when a file is added */ onFileAdded?: (file: FileState) => void | Promise; /** Callback when a file is removed */ onFileRemoved?: (key: string) => void | Promise; /** Callback when a file upload completes */ onUploadCompleted?: (file: CompletedFileState) => void | Promise; /** Function to handle the actual upload */ uploadFn: UploadFn; /** External value to control the file states */ value?: FileState[]; /** Whether files should be automatically uploaded when added */ autoUpload?: boolean; }; // Context const UploaderContext = React.createContext | null>(null); /** * Hook to access the uploader context. * * @returns The uploader context * @throws Error if used outside of UploaderProvider * * @example * ```tsx * const { fileStates, addFiles, uploadFiles } = useUploader(); * ``` */ export function useUploader() { const context = React.useContext(UploaderContext); if (!context) { throw new Error('useUploader must be used within a UploaderProvider'); } return context as UploaderContextType; } /** * Provider component for file upload functionality. * * @component * @example * ```tsx * { * // Upload implementation * return { url: 'https://example.com/uploads/image.jpg' }; * }} * autoUpload={true} * > * * * ``` */ export function UploaderProvider({ children, onChange, onFileAdded, onFileRemoved, onUploadCompleted, uploadFn, value: externalValue, autoUpload = false, }: ProviderProps) { const [fileStates, setFileStates] = React.useState( externalValue ?? [], ); const [pendingAutoUploadKeys, setPendingAutoUploadKeys] = React.useState< string[] | null >(null); // Sync with external value if provided React.useEffect(() => { if (externalValue) { setFileStates(externalValue); } }, [externalValue]); const updateFileState = React.useCallback( (key: string, changes: Partial) => { setFileStates((prevStates) => { return prevStates.map((fileState) => { if (fileState.key === key) { return { ...fileState, ...changes }; } return fileState; }); }); }, [], ); const uploadFiles = React.useCallback( async (keysToUpload?: string[], options?: TOptions) => { const filesToUpload = fileStates.filter( (fileState) => fileState.status === 'PENDING' && (!keysToUpload || keysToUpload.includes(fileState.key)), ); if (filesToUpload.length === 0) return; await Promise.all( filesToUpload.map(async (fileState) => { try { const abortController = new AbortController(); updateFileState(fileState.key, { abortController, status: 'UPLOADING', progress: 0, }); const uploadResult = await uploadFn({ file: fileState.file, signal: abortController.signal, onProgressChange: (progress) => { updateFileState(fileState.key, { progress }); }, options, }); // Wait a bit to show the bar at 100% await new Promise((resolve) => setTimeout(resolve, 500)); const completedFile = { ...fileState, status: 'COMPLETE' as const, progress: 100, url: uploadResult?.url, }; updateFileState(fileState.key, { status: 'COMPLETE', progress: 100, url: uploadResult?.url, }); // Call onUploadCompleted when a file upload is completed if (onUploadCompleted) { void onUploadCompleted(completedFile); } } catch (err: unknown) { if ( err instanceof Error && // if using with EdgeStore, the error name is UploadAbortedError (err.name === 'AbortError' || err.name === 'UploadAbortedError') ) { updateFileState(fileState.key, { status: 'PENDING', progress: 0, error: 'Upload canceled', }); } else { if (process.env.NODE_ENV === 'development') { console.error(err); } const errorMessage = err instanceof Error ? err.message : 'Upload failed'; updateFileState(fileState.key, { status: 'ERROR', error: errorMessage, }); } } }), ); }, [fileStates, updateFileState, uploadFn, onUploadCompleted], ); const addFiles = React.useCallback( (files: File[]) => { const newFileStates = files.map((file) => ({ file, key: `${file.name}-${Date.now()}-${Math.random() .toString(36) .slice(2)}`, progress: 0, status: 'PENDING', autoUpload, })); setFileStates((prev) => [...prev, ...newFileStates]); // Call onFileAdded for each new file if (onFileAdded) { newFileStates.forEach((fileState) => { void onFileAdded(fileState); }); } if (autoUpload) { setPendingAutoUploadKeys(newFileStates.map((fs) => fs.key)); } }, [autoUpload, onFileAdded], ); const removeFile = React.useCallback( (key: string) => { setFileStates((prev) => prev.filter((fileState) => fileState.key !== key), ); // Call onFileRemoved when a file is removed if (onFileRemoved) { void onFileRemoved(key); } }, [onFileRemoved], ); const cancelUpload = React.useCallback( (key: string) => { const fileState = fileStates.find((f) => f.key === key); if (fileState?.abortController && fileState.progress < 100) { fileState.abortController.abort(); if (fileState?.autoUpload) { // Remove file if it was an auto-upload removeFile(key); } else { // If it was not an auto-upload, reset the file state updateFileState(key, { status: 'PENDING', progress: 0 }); } } }, [fileStates, updateFileState, removeFile], ); const resetFiles = React.useCallback(() => { setFileStates([]); }, []); React.useEffect(() => { const completedFileStates = fileStates.filter( (fs): fs is CompletedFileState => fs.status === 'COMPLETE' && !!fs.url, ); void onChange?.({ allFiles: fileStates, completedFiles: completedFileStates, }); }, [fileStates, onChange]); // Handle auto-uploading files added to the queue React.useEffect(() => { if (pendingAutoUploadKeys && pendingAutoUploadKeys.length > 0) { void uploadFiles(pendingAutoUploadKeys); setPendingAutoUploadKeys(null); } }, [pendingAutoUploadKeys, uploadFiles]); const isUploading = React.useMemo( () => fileStates.some((fs) => fs.status === 'UPLOADING'), [fileStates], ); const value = React.useMemo( () => ({ fileStates, addFiles, updateFileState, removeFile, cancelUpload, uploadFiles, resetFiles, isUploading, autoUpload, }), [ fileStates, addFiles, updateFileState, removeFile, cancelUpload, uploadFiles, resetFiles, isUploading, autoUpload, ], ); return ( }> {typeof children === 'function' ? children(value) : children} ); } /** * Formats a file size in bytes to a human-readable string. * * @param bytes - The file size in bytes * @returns A formatted string (e.g., "1.5 MB") * * @example * ```ts * formatFileSize(1024); // "1 KB" * formatFileSize(1024 * 1024 * 2.5); // "2.5 MB" * ``` */ export function formatFileSize(bytes?: number) { if (!bytes) return '0 B'; const k = 1024; const dm = 2; const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`; } ```` ## Usage This section provides a step-by-step guide on how to use the `UploaderProvider` and the `useUploader` hook. ### 1. Setup `` Wrap the part of your application that needs uploader functionality with `UploaderProvider`. You must provide an `uploadFn` and can optionally configure `autoUpload`. * **`uploadFn`**: An asynchronous function that handles the actual file upload. It receives the `file`, an `onProgressChange` callback, and an `AbortSignal`. It should return an object with the uploaded file's `url`. * **`autoUpload`**: (Optional, default: `false`) If `true`, files will start uploading immediately after being added. ```tsx import { UploaderProvider, UploadFn } from '@/components/ui/uploader'; // Adjust import path import { useEdgeStore } from '@/lib/edgestore'; // Adjust import path import * as React from 'react'; function MyUploaderPage() { const { edgestore } = useEdgeStore(); // Define the upload function const uploadFn: UploadFn = React.useCallback( async ({ file, onProgressChange, signal }) => { // Example using Edge Store client 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('Upload successful:', res); return res; // Must return { url: string } }, [edgestore], ); return ( // Provide the uploadFn and configure autoUpload {/* Your uploader components go here */} ); } // export default MyUploaderPage; // Assuming MyUploaderComponent is defined below ``` ### 2. Use the `useUploader` Hook Inside components nested under `UploaderProvider`, use the `useUploader` hook to access the uploader's state and control functions. ```tsx import { useUploader } from '@/components/ui/uploader'; // Adjust import path import * as React from 'react'; function MyUploaderComponent() { const { fileStates, // Array of current file states addFiles, // Function to add files removeFile, // Function to remove a file by key cancelUpload, // Function to cancel an upload by key uploadFiles, // Function to trigger uploads (all pending or specific keys) isUploading, // Boolean indicating if any upload is in progress } = useUploader(); // ... component logic using these values and functions ... return
{/* UI elements */}
; } ``` ### 3. Adding Files (`addFiles`) Typically, you'll use a standard file input. You might hide it and trigger its click event from a custom button. Get the selected `File` objects from the input's `onChange` event and pass them to `addFiles`. ```tsx function MyUploaderComponent() { const { addFiles } = useUploader(); const inputRef = React.useRef(null); // Handle file selection from the input const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files) { addFiles(Array.from(e.target.files)); // Optional: Reset input value to allow selecting the same file again e.target.value = ''; } }; // Trigger the hidden input click const handleAddClick = () => { inputRef.current?.click(); }; return (
{/* Hidden file input */} {/* Button to open file selector */} {/* ... rest of the component ... */}
); } ``` ### 4. Displaying File State (`fileStates`) The `fileStates` array contains objects representing each file. Each object includes: * `file`: The original `File` object. * `key`: A unique string identifier. * `status`: `'PENDING'`, `'UPLOADING'`, `'COMPLETE'`, or `'ERROR'`. * `progress`: Upload progress (0-100). * `url`: (Optional) The URL after successful upload (`status === 'COMPLETE'`). * `error`: (Optional) Error message if upload failed (`status === 'ERROR'`). Iterate over `fileStates` to render the UI for each file. ```tsx function MyUploaderComponent() { const { fileStates, removeFile, cancelUpload } = useUploader(); return (
{/* ... Add files button/input ... */} {/* List of files */} {fileStates.length > 0 && (
    {fileStates.map((fileState) => (
  • {fileState.file.name} ({fileState.status}) {/* Show progress during upload */} {fileState.status === 'UPLOADING' && ( {fileState.progress}% )} {/* Show cancel button during upload */} {fileState.status === 'UPLOADING' && ( )} {/* Show remove button otherwise */} {fileState.status !== 'UPLOADING' && ( )} {/* Show error message */} {fileState.status === 'ERROR' && ( {' '} Error: {fileState.error} )} {/* Show link on completion */} {fileState.status === 'COMPLETE' && fileState.url && ( View File )}
  • ))}
)}
); } ``` ### 5. Triggering Uploads (`uploadFiles`) Call `uploadFiles()` to start uploading all files with status `'PENDING'`. You can optionally pass an array of specific file keys to `uploadFiles(keysToUpload)` to upload only those files. Use the `isUploading` boolean to disable the upload button during active uploads. ```tsx function MyUploaderComponent() { const { uploadFiles, isUploading, fileStates } = useUploader(); // Check if there are any files pending upload const hasPendingFiles = fileStates.some((fs) => fs.status === 'PENDING'); return (
{/* ... Add files button/input and file list ... */} {/* Upload button */}
); } ``` ### 6. Cancelling Uploads (`cancelUpload`) Call `cancelUpload(key)` with the file's unique key to abort an ongoing upload. Your `uploadFn` must be implemented to respect the `AbortSignal` for cancellation to work correctly. ```tsx // Example within the file list rendering (see step 4) { fileState.status === 'UPLOADING' && ( ); } ``` ### 7. Removing Files (`removeFile`) Call `removeFile(key)` with the file's key to remove it from the list, regardless of its status. If the file is currently uploading, this will also attempt to cancel the upload. ```tsx // Example within the file list rendering (see step 4) { fileState.status !== 'UPLOADING' && ( ); } ``` ### 8. Callbacks You can pass callback props (`onChange`, `onFileAdded`, `onFileRemoved`, `onUploadCompleted`) to the `UploaderProvider` to execute logic when the uploader state changes. ```tsx { console.log('Files changed:', allFiles); console.log('Completed files:', completedFiles); }} onFileAdded={(fileState) => console.log('File added:', fileState.file.name)} onUploadCompleted={(completedFile) => console.log('Upload complete:', completedFile.url) } > {/* ... */} ``` ### Complete Component Example (`MyUploaderComponent`) Here is the `MyUploaderComponent` combining the steps above: ```tsx import { useUploader } from '@/components/ui/uploader'; // Adjust import path import * as React from 'react'; function MyUploaderComponent() { const { fileStates, addFiles, removeFile, cancelUpload, uploadFiles, isUploading, } = useUploader(); const inputRef = React.useRef(null); // Function to handle file selection const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files) { addFiles(Array.from(e.target.files)); e.target.value = ''; // Reset input } }; // Function to trigger the hidden file input const handleAddClick = () => { inputRef.current?.click(); }; const hasPendingFiles = fileStates.some((fs) => fs.status === 'PENDING'); return (
{/* Hidden file input */} {/* Buttons */} {/* Display file states */} {fileStates.length > 0 && (
    {fileStates.map((fileState) => (
  • {fileState.file.name} {' '} ({fileState.status}) {/* Progress and Controls */}
    {fileState.status === 'UPLOADING' && ( <> {fileState.progress}% )} {fileState.status !== 'UPLOADING' && ( )} {fileState.status === 'ERROR' && ( {' '} Error: {fileState.error} )} {fileState.status === 'COMPLETE' && fileState.url && ( View File )}
  • ))}
)}
); } ``` ### Putting It All Together (`MyUploaderPage`) Finally, use the `MyUploaderComponent` within the page component wrapped by the `UploaderProvider`. ```tsx import { UploaderProvider, UploadFn } from '@/components/ui/uploader'; // Adjust import path import { useEdgeStore } from '@/lib/edgestore'; // Adjust import path import * as React from 'react'; // Assume MyUploaderComponent is defined in the same file or imported // import { MyUploaderComponent } from './MyUploaderComponent'; function MyUploaderPage() { const { edgestore } = useEdgeStore(); // Define the upload function (same as in step 1) const uploadFn: UploadFn = React.useCallback( async ({ file, onProgressChange, signal }) => { const res = await edgestore.publicFiles.upload({ file, signal, onProgressChange, }); console.log('Upload successful:', res); return res; }, [edgestore], ); return (

My File Uploader

{ console.log( `File ${completedFile.file.name} uploaded successfully to ${completedFile.url}`, ); // Maybe trigger a database update here }} >
); } export default MyUploaderPage; ``` This provides a basic but functional file uploader using the context provider. You can style the elements and integrate them further into your application's UI. # EdgeStore Docs: AWS URL: /docs/providers/aws Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/providers/aws.mdx *** title: AWS description: Learn how to use your own AWS S3 bucket or S3-compatible services like Minio with EdgeStore using the AWS Provider. -------------------------------------------------------------------------------------------------------------------------------- You can also use the EdgeStore package with your own AWS S3 bucket. You might want to do that in case you have strict company policies that require you to have all the data in your own AWS account. You can also use the AWS Provider with other S3 compatible storage services like [Minio](https://min.io/). By using this provider, you will be able to use most of the basic features of EdgeStore. However, for some of the more advanced features like access control with protected files, you will have to create your own infrastructure and logic from scratch. ## Installation You need to install some peer dependencies to use this provider. ```bash npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner ``` ```bash pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner ``` ```bash yarn add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner ``` ```bash bun add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner ``` Then you can set the provider in the router. ```ts import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreNextHandler, type CreateContextOptions, } from '@edgestore/server/adapters/next/pages'; // [!code ++] import { AWSProvider } from '@edgestore/server/providers/aws'; import { z } from 'zod'; // ... export default createEdgeStoreNextHandler({ // [!code ++] provider: AWSProvider(), router: edgeStoreRouter, createContext, }); ``` ## Options ```ts export type AWSProviderOptions = { /** * Access key for AWS credentials. * Can also be set via the `ES_AWS_ACCESS_KEY_ID` environment variable. * * If unset, the SDK will attempt to use the default credentials provider chain. */ accessKeyId?: string; /** * Secret access key for AWS credentials. * Can also be set via the `ES_AWS_SECRET_ACCESS_KEY` environment variable. * * If unset, the SDK will attempt to use the default credentials provider chain. */ secretAccessKey?: string; /** * AWS region to use. * Can also be set via the `ES_AWS_REGION` environment variable. */ region?: string; /** * Name of the S3 bucket to use. * Can also be set via the `ES_AWS_BUCKET_NAME` environment variable. */ bucketName?: string; /** * Custom endpoint for S3-compatible storage providers (e.g., MinIO). * Can also be set via the `ES_AWS_ENDPOINT` environment variable. */ endpoint?: string; /** * Force path style for S3-compatible storage providers. * Can also be set via the `ES_AWS_FORCE_PATH_STYLE` environment variable. * Defaults to false for AWS S3, but should be true for most S3-compatible providers. */ forcePathStyle?: boolean; /** * Base URL to use for accessing files. * Only needed if you are using a custom domain or cloudfront. * * Can also be set via the `EDGE_STORE_BASE_URL` environment variable. */ baseUrl?: string; /** * Secret to use for encrypting JWT tokens. * Can be generated with `openssl rand -base64 32`. * * Can also be set via the `EDGE_STORE_JWT_SECRET` environment variable. */ jwtSecret?: string; /** * Optional function to overwrite the S3 key (object path) for uploads. * This function receives the EdgeStore bucket name, fileInfo and the default S3 key * and should return the desired S3 key string. */ overwritePath?: (args: { esBucketName: string; fileInfo: FileInfo; defaultAccessPath: string; }) => Promise | string; }; ``` ## Overwriting S3 Object Keys By default, the same logic used for generating the path for the EdgeStore Provider is used for the AWS Provider. However, sometimes you might want a cleaner path for the S3 objects that matches your use case. For that, you can use the `overwritePath` option. ```ts // ... const handler = createEdgeStoreNextHandler({ createContext, provider: AWSProvider({ // [!code ++:4] overwritePath: ({ defaultAccessPath }) => { // `publicFiles/_public/123/test.png` -> `123/test.png` return defaultAccessPath.split('/_public/')[1]; }, }), router: edgeStoreRouter, }); ``` If you are removing the `_public` folder from the path like in the example above, you might also want to disable the dev proxy in the `createEdgeStoreProvider` function. ```ts const { EdgeStoreProvider, useEdgeStore } = createEdgeStoreProvider({ disableDevProxy: true, }); ``` ## Using with Minio You can use the AWS Provider with Minio or other S3-compatible storage providers by setting the `endpoint` and `forcePathStyle` options. ```ts provider: AWSProvider({ endpoint: 'http://localhost:9000', // can be set via the `ES_AWS_ENDPOINT` environment variable forcePathStyle: true, // can be set via the `ES_AWS_FORCE_PATH_STYLE` environment variable }), ``` # EdgeStore Docs: Azure URL: /docs/providers/azure Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/providers/azure.mdx *** title: Azure description: Learn how to use your own Azure Blob Storage with EdgeStore using the Azure Provider. -------------------------------------------------------------------------------------------------- You can also use the EdgeStore package with your own Azure Blob Storage. You might want to do that in case you have strict company policies that require you to have all the data in your own Azure account. By using this provider, you will be able to use most of the basic features of EdgeStore. However, for some of the more advanced features like access control with protected files, you will have to create your own infrastructure and logic from scratch. ## Installation You need to install some peer dependencies to use this provider. ```bash npm install @azure/storage-blob ``` ```bash pnpm add @azure/storage-blob ``` ```bash yarn add @azure/storage-blob ``` ```bash bun add @azure/storage-blob ``` Then you can set the provider in the router. ```ts import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreNextHandler, type CreateContextOptions, } from '@edgestore/server/adapters/next/pages'; // [!code ++] import { AzureProvider } from '@edgestore/server/providers/azure'; import { z } from 'zod'; // ... export default createEdgeStoreNextHandler({ // [!code ++] provider: AzureProvider(), router: edgeStoreRouter, createContext, }); ``` ## Options ```ts export type AzureProviderOptions = { /** * The storage account name for Azure Blob Storage * Can also be set via the `ES_AZURE_ACCOUNT_NAME` environment variable. */ storageAccountName?: string; /** * SAS token for Azure Blob Storage * Can also be set via the `ES_AZURE_SAS_TOKEN` environment variable. */ sasToken?: string; /** * Azure Blob Storage container name * Can also be set via the `ES_AZURE_CONTAINER_NAME` environment variable. */ containerName?: string; }; ``` # EdgeStore Docs: Custom URL: /docs/providers/custom Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/providers/custom.mdx *** title: Custom description: Learn how to create your own custom storage provider for EdgeStore or contribute to the project. ------------------------------------------------------------------------------------------------------------- You can also create a custom provider for a different storage solution or just a different implementation for the existing providers. You can use the [EdgeStore Provider](https://github.com/edgestorejs/edgestore/blob/next/packages/server/src/providers/edgestore/index.ts) as a reference implementation. If you build a provider that you believe could be useful for others, please consider contributing it to the EdgeStore project by opening a pull request. # EdgeStore Docs: EdgeStore URL: /docs/providers/edgestore Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/providers/edgestore.mdx *** title: EdgeStore description: Learn about the EdgeStore provider and its configuration options for customizing your EdgeStore integration. ------------------------------------------------------------------------------------------------------------------------- You can optionally pass in a provider to the `createEdgeStoreNextHandler` function. This is useful if you want to use a different provider than the default one or if you want to pass some custom options to the provider. The EdgeStore Provider is the default provider. If you followed the documentation, you already have it configured in your app. ```ts import { initEdgeStore } from '@edgestore/server'; import { createEdgeStoreNextHandler, type CreateContextOptions, } from '@edgestore/server/adapters/next/pages'; // [!code ++] import { EdgeStoreProvider } from '@edgestore/server/providers/edgestore'; import { z } from 'zod'; // ... export default createEdgeStoreNextHandler({ // [!code ++] provider: EdgeStoreProvider(), // this is the default provider and can be omitted router: edgeStoreRouter, createContext, }); ``` ## Options ```ts export type EdgeStoreProviderOptions = { /** * Access key for your EdgeStore project. * Can be found in the EdgeStore dashboard. * * This can be omitted if the `EDGE_STORE_ACCESS_KEY` environment variable is set. */ accessKey?: string; /** * Secret key for your EdgeStore project. * Can be found in the EdgeStore dashboard. * * This can be omitted if the `EDGE_STORE_SECRET_KEY` environment variable is set. */ secretKey?: string; }; ```