# 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
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
Configure the router and hosted provider once. The resulting `configuredEdgeStore`
instance is used by the HTTP handler and exposes the type-safe 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 { createEdgeStore, initEdgeStore } from '@edgestore/server';
import { createEdgeStoreNextHandler } from '@edgestore/server/adapters/next/app';
import { edgestore } from '@edgestore/server/providers/edgestore';
const es = initEdgeStore.create();
export const router = es.router({
publicFiles: es.fileBucket(),
});
export const configuredEdgeStore = createEdgeStore({
router,
provider: edgestore(),
});
export const handler = createEdgeStoreNextHandler({
edgestore: configuredEdgeStore,
});
export const backendClient = configuredEdgeStore.client;
```
Then you will need to update your api route to use the exported handler.
```ts title="src/app/api/edgestore/[...edgestore]/route.ts"
import { handler } from '@/lib/edgestore-server';
export { handler as GET, handler as POST };
```
You can find an example of the backend client usage in the
[next-advanced](https://github.com/edgestorejs/edgestore/tree/main/examples/next-advanced)
example.
The backend client is privileged. It validates router input and applies file
type, size, transform, path, and metadata rules, but it does not run
`accessControl`, `beforeUpload`, or `beforeDelete`. Perform authorization in
the server code that calls it.
The hosted `edgestore()` provider supports the complete backend client. S3
currently exposes `get`, `delete`, and `deleteMany`. Azure Blob Storage also
exposes `createSignedUrl` and `createSignedUrls` for private reads. Methods such
as backend upload and listing remain absent from those clients until the
provider implements them.
## Backend Upload
You can use the `upload` function to upload files from your backend.
### Upload a text file
The simplest use case would be to just upload a `txt` file:
```ts
const res = await backendClient.publicFiles.upload({
content: 'some text content',
});
```
### Upload a blob
You can also upload a more complex file using the Blob object. And there are also all the other options available in the normal upload.
```ts
const res = await backendClient.publicFiles.upload({
content: {
blob: new Blob(['col1,col2,col2'], { type: 'text/csv' }),
extension: 'csv',
},
options: {
temporary: true,
},
ctx: {
userId: '123',
userRole: 'admin',
},
input: {
type: 'post',
},
signal,
onProgress: ({ percentage, phase }) => {
console.log(phase, `${percentage}%`);
},
});
console.log(res.id, res.key, res.sizeBytes);
```
### Copy an existing file
You can use an existing file's URL to copy it into the EdgeStore bucket. This can be an external file (from outside of EdgeStore) or an existing EdgeStore file.
```ts
const res = await backendClient.publicFiles.upload({
content: {
url: 'https://some-url.com/file.txt',
extension: 'txt',
},
});
```
### Transform a file before upload
You can transform backend uploads before EdgeStore validates and uploads them.
The transform receives the resolved `Blob` and extension, and returns the new
`Blob` and extension.
For example, you can use `sharp` to convert an image to WebP before upload:
npm
pnpm
yarn
bun
```bash
npm install sharp
```
```bash
pnpm add sharp
```
```bash
yarn add sharp
```
```bash
bun add sharp
```
```ts
import sharp from 'sharp';
const res = await backendClient.publicImages.upload({
content: {
url: 'https://some-url.com/image.jpg',
extension: 'jpg',
},
options: {
transform: async ({ blob }) => {
const input = Buffer.from(await blob.arrayBuffer());
const output = await sharp(input).webp({ quality: 80 }).toBuffer();
return {
blob: new Blob([output], { type: 'image/webp' }),
extension: 'webp',
};
},
},
});
```
### Confirm a temporary file upload
If you upload a temporary file, you can confirm it by using the `confirm` function.
```ts
const res = await backendClient.publicFiles.confirm({
id: file.id,
});
```
File operations accept a stable file ID, storage key, or URL. Singular
operations throw `EdgeStoreFileMutationError` when that file fails. Use the
plural form when partial success should be preserved:
```ts
const result = await backendClient.publicFiles.confirmMany({
refs: [{ id: first.id }, { key: second.key }],
});
for (const failure of result.failed) {
console.error(failure.ref, failure.error.code);
}
```
## Backend Delete
You can use the `delete` function to delete files from your backend.
```ts
const res = await backendClient.publicFiles.delete({
id: file.id,
});
```
`deleteMany`, `restore`, and `restoreMany` use the same singular and
partial-batch semantics.
## Backend List Files (search)
You can use the `list` function to list files from your backend. It's also possible to filter the results by path, metadata or upload timing.
```ts
// simple usage
// get the first 20 files in the bucket
const res = await backendClient.publicFiles.list();
// with filter and pagination
const res = await backendClient.publicFiles.list({
filter: {
metadata: {
role: 'admin',
},
path: {
type: 'post',
},
uploadedAt: {
gt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7), // past 7 days
},
},
cursor: 'cursor-from-previous-response',
limit: 50, // default: 20 (max: 100)
});
for (const file of res.items) {
console.log(file.id, file.url);
}
if (res.hasMore) {
console.log('Next cursor:', res.nextCursor);
}
```
## Private read URLs
Providers with signed-read support expose `createSignedUrl` on protected
buckets:
```ts
const access = await backendClient.privateFiles.createSignedUrl({
url: { id: file.id },
expiresIn: 15 * 60,
});
console.log(access.signedUrl, access.expiresAt);
```
Use `createSignedUrls` to sign several references in one provider call.
## Reusing backend client types
Derive a single method's exact input or output directly from the configured
client:
```ts
type UploadInput = Parameters<
typeof configuredEdgeStore.client.publicFiles.upload
>[0];
type UploadOutput = Awaited<
ReturnType
>;
```
This includes the router's context, input, path, and metadata configuration and
the provider's file and reference types.
Use `InferClientInputs` and `InferClientOutputs` when you need a reusable map of
every bucket and method:
```ts title="src/lib/edgestore-server.ts"
import type {
InferClientInputs,
InferClientOutputs,
} from '@edgestore/server';
export type BackendInputs = InferClientInputs;
export type BackendOutputs = InferClientOutputs;
type UploadInput = BackendInputs['publicFiles']['upload'];
type UploadOutput = BackendOutputs['publicFiles']['upload'];
```
The default represents the hosted provider. Pass a custom provider as the
second generic when its capabilities or associated types differ:
```ts
type BackendOutputs = InferClientOutputs<
typeof router,
typeof customProvider
>;
```
`InferClientResponse` remains as a deprecated alias of `InferClientOutputs`.
# EdgeStore Docs: Configuration
URL: /docs/configuration
Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/configuration.mdx
## Bucket Types
There are two types of file buckets: `IMAGE` and `FILE`. Both types of buckets work basically the same way, but the `IMAGE` bucket only accepts [certain mime types](#image-bucket-accepted-mime-types).
IMAGE buckets automatically generate a thumbnail version of the image file if the file is bigger than 200px in width or height. In case a thumbnail was generated, the url will be included in the response of the upload request.
```ts
const router = es.router({
publicFiles: es.fileBucket(),
publicImages: es.imageBucket(),
});
```
## Basic File Validation
You can set the maximum file size and the accepted mime types for every file bucket.
```ts
const router = es.router({
publicFiles: es.fileBucket({
maxSize: 1024 * 1024 * 10, // 10MB
accept: ['image/jpeg', 'image/png'], // wildcard also works: ['image/*']
}),
});
```
## Context
Many of the functions that you can use to configure your file buckets receive a `context` object as an argument. This object is generated by the `createContext` function that you pass to your router configuration.
```ts
import { createEdgeStore, initEdgeStore } from '@edgestore/server';
import {
type CreateContextOptions,
createEdgeStoreNextHandler,
} from '@edgestore/server/adapters/next/app';
import { edgestore } from '@edgestore/server/providers/edgestore';
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 configuredEdgeStore = createEdgeStore({
router,
provider: edgestore(),
});
export default createEdgeStoreNextHandler({
edgestore: configuredEdgeStore,
/**
* 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 router = es.router({
publicFiles: es
.fileBucket()
// this input will be required for every upload request
.input(
z.object({
category: z.string(),
}),
)
// e.g. /publicFiles/{category}/{author}
.path(({ ctx, input }) => [
{ category: input.category },
{ author: ctx.userId },
])
// this metadata will be added to every file in this bucket
.metadata(({ ctx, input }) => ({
userRole: ctx.userRole,
})),
});
```
## Lifecycle Hooks
You can use the `beforeUpload` and `beforeDelete` hooks to allow or deny file uploads and deletions. The `beforeDelete` hook must be defined if you want to delete files directly from the client.
```ts
import { initEdgeStore } from '@edgestore/server';
import {
type CreateContextOptions,
createEdgeStoreNextHandler,
} from '@edgestore/server/adapters/next/app';
import { z } from 'zod';
type Context = {
userId: string;
userRole: 'admin' | 'user';
};
async function createContext({ req }: CreateContextOptions): Promise {
const { id, role } = await getUserSession(req); // replace with your own session logic
return {
userId: id,
userRole: role,
};
}
const es = initEdgeStore.context().create();
const router = es.router({
publicFiles: es
.fileBucket()
/**
* return `true` to allow upload
* By default every upload from your app is allowed.
*/
.beforeUpload(({ ctx, input, fileInfo }) => {
console.log('beforeUpload', ctx, input, fileInfo);
return true; // allow upload
})
/**
* return `true` to allow delete
* This function must be defined if you want to delete files directly from the client.
*/
.beforeDelete(({ ctx, fileInfo }) => {
console.log('beforeDelete', ctx, fileInfo);
return true; // allow delete
}),
});
```
## Access Control (Experimental)
You can use the `accessControl` function to add bucket level logic to allow or deny access to files. If you have ever used Prisma, you will probably notice that the structure of the `accessControl` function is similar to how you would write a Prisma query.
If you set the `accessControl` function, your bucket will automatically be configured as a **protected bucket**. You cannot change a protected bucket to a public bucket after it has been created. The opposite is also true, you cannot change a public bucket to a protected bucket.
To access files from a **protected bucket** the user will need a specific encrypted cookie generated in your server by the EdgeStore package. Which means that they will only be able to access the files from within your app. Sharing the url of a protected file will not work.
The access control check is performed on an edge function without running any database queries, so you won't need to worry about bad performance on your protected files.
```ts
const filesBucket = es
.fileBucket()
.path(({ ctx }) => [{ author: ctx.userId }])
.accessControl({
OR: [
{
// this will make sure that only the author of the file can access it
userId: { path: 'author' },
},
{
// or if the user is an admin
userRole: {
eq: 'admin',
}, // same as { userRole: 'admin' }
},
],
});
```
Other available operators are: `eq`, `not`, `gt`, `gte`, `lt`, `lte`, `in`, `contains`
The access control functionality uses 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
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
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:
npm
pnpm
yarn
bun
```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
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
edgestore: configuredEdgeStore,
});
```
## Log Levels
* `debug`
* `info`
* `warn`
* `error`
* `none`
# EdgeStore Docs: Migrate to v1
URL: /docs/migrate-to-v1
Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/migrate-to-v1.mdx
EdgeStore v1 is a deliberate major-version redesign. The browser router DX is
largely unchanged, while direct API and privileged backend code now use API v2
semantics without a runtime v1 fallback.
## Configure EdgeStore once
The router, provider, HTTP handler, and backend client now share one configured
instance. This is a semantic migration, so there is no rename codemod.
```ts title="Before"
import { initEdgeStore } from '@edgestore/server';
import { createEdgeStoreNextHandler } from '@edgestore/server/adapters/next/app';
import { initEdgeStoreClient } from '@edgestore/server/core';
const es = initEdgeStore.create();
const router = es.router({
documents: es.fileBucket(),
});
export const handler = createEdgeStoreNextHandler({
router,
});
export const backendClient = initEdgeStoreClient({
router,
});
```
```ts title="After"
import { createEdgeStore, initEdgeStore } from '@edgestore/server';
import { createEdgeStoreNextHandler } from '@edgestore/server/adapters/next/app';
import { edgestore } from '@edgestore/server/providers/edgestore';
const es = initEdgeStore.create();
const router = es.router({
documents: es.fileBucket(),
});
export const configuredEdgeStore = createEdgeStore({
router,
provider: edgestore(),
});
export const handler = createEdgeStoreNextHandler({
edgestore: configuredEdgeStore,
});
export const backendClient = configuredEdgeStore.client;
```
## Breaking changes
| v0.7 | v1 | Required change |
| ------------------------------------------- | --------------------------------- | ---------------------------------------------------------- |
| Separate handler and client config | `createEdgeStore` | Configure the router and provider once. |
| `initEdgeStoreClient` | `configuredEdgeStore.client` | Read the client from the configured hosted provider. |
| URL-only identity | `{ id }`, `{ key }`, or `{ url }` | Prefer stable IDs in new code. |
| Predicted upload result | Canonical processed file | Use `sizeBytes`, `id`, `key`, and the returned timestamps. |
| `{ pagination: { currentPage, pageSize } }` | `{ cursor, limit }` | Replace page numbers with explicit cursor continuation. |
| `result.data` | `result.items` | Read canonical file records from `items`. |
| `{ success: boolean }` | Singular result or partial batch | Catch singular errors or inspect `failed`. |
| React `confirmUpload` | React `confirm` | Use the resource-scoped lifecycle name. |
| React `confirmUploads` | React `confirmMany` | Use the `Many` suffix for batches. |
| Backend `getFile` | Backend `get` | Use the bucket-scoped read name. |
| Backend `listFiles` | Backend `list` | Use the bucket-scoped list name. |
| Backend `confirmUpload` | Backend `confirm` | Use the singular lifecycle name. |
| Backend `confirmUploads` | Backend `confirmMany` | Use the `Many` suffix for batches. |
| Backend `deleteFile` | Backend `delete` | Use the singular lifecycle name. |
| Backend `deleteFiles` | Backend `deleteMany` | Use the `Many` suffix for batches. |
| Backend `restoreFile(s)` | Backend `restore` / `restoreMany` | Use singular and `Many` lifecycle names. |
| Backend `getSignedUrl` | Backend `createSignedUrl` | Make signed-URL creation explicit. |
| Backend `getSignedUrls` | Backend `createSignedUrls` | Make batch signed-URL creation explicit. |
| Nullable metadata values | Nullish values omitted | Treat those inferred keys as optional strings. |
| Handcrafted server raw client | `@edgestore/sdk` | Migrate direct API consumers to the public SDK. |
| `ES_AZURE_SAS_TOKEN` | `ES_AZURE_ACCOUNT_KEY` | Give Azure signing authority only to the server. |
Pagination is no longer page-number based. Continue only when your application
intentionally needs another page:
```ts title="Before (v0.7)"
const page = await backendClient.documents.listFiles({
pagination: {
currentPage: 1,
pageSize: 50,
},
});
```
```ts title="After (v1)"
const firstPage = await backendClient.documents.list({ limit: 50 });
const secondPage = firstPage.hasMore
? await backendClient.documents.list({
cursor: firstPage.nextCursor ?? undefined,
limit: 50,
})
: undefined;
```
```ts
const page = await backendClient.documents.list({ limit: 50 });
const deleted = await backendClient.documents.deleteMany({
refs: page.items.map((file) => ({ id: file.id })),
});
for (const failure of deleted.failed) {
console.error(failure.ref, failure.error.code);
}
```
Frontend route bodies and bucket input are now parsed before any router hook or
provider method runs. Invalid values return `BAD_REQUEST`. The encrypted
`edgestore-ctx` payload is also namespaced in v1; there is no decoder fallback
for a v0.7 context cookie, so clients must complete the normal `/init` request
after deployment.
Azure users must replace the reusable SAS token with the storage account key.
The provider now derives short-lived create/write upload URLs and independent
read-only URLs for private files. Canonical file URLs never include the
credential query string.
## Provider capabilities
Every provider is defined through the same resource-oriented contract. The
router backend client exposes exactly the methods present on that provider:
| Provider | Router backend client | Adapter upload/delete | API v2 calls |
| -------------------- | ---------------------------------------- | --------------------- | ------------------------ |
| Hosted `edgestore()` | Full support | Full support | Through `@edgestore/sdk` |
| `s3()` | Get and delete | Full support | Never |
| `azureBlob()` | Get, delete, and signed private reads | Full support | Never |
| Custom provider | Inferred from its `defineProvider` shape | Provider-defined | Provider-defined |
Omitted capabilities are absent from TypeScript and are not replaced by a
runtime fallback to the hosted API.
## Release ordering
API v2 must be deployed with the pinned OpenAPI contract before stable v1
packages are published. Use prereleases against staging until that gate is met;
v0.7 remains installable for applications that cannot migrate yet.
# 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
## Next.js Setup
### Install
Let's start by installing the required packages.
npm
pnpm
yarn
bun
```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 title="src/app/api/edgestore/[...edgestore]/route.ts"
import { createEdgeStore, initEdgeStore } from '@edgestore/server';
import { createEdgeStoreNextHandler } from '@edgestore/server/adapters/next/app';
import { edgestore } from '@edgestore/server/providers/edgestore';
const es = initEdgeStore.create();
/**
* This is the main router for the EdgeStore buckets.
*/
const router = es.router({
publicFiles: es.fileBucket(),
});
const configuredEdgeStore = createEdgeStore({
router,
provider: edgestore(),
});
const handler = createEdgeStoreNextHandler({
edgestore: configuredEdgeStore,
});
export { handler as GET, handler as POST };
/**
* This type is used to create the type-safe client for the frontend.
*/
export type EdgeStoreRouter = typeof router;
```
```ts title="src/pages/api/edgestore/[...edgestore].ts"
import { createEdgeStore, initEdgeStore } from '@edgestore/server';
import { createEdgeStoreNextHandler } from '@edgestore/server/adapters/next/pages';
import { edgestore } from '@edgestore/server/providers/edgestore';
const es = initEdgeStore.create();
/**
* This is the main router for the edgestore buckets.
*/
const router = es.router({
publicFiles: es.fileBucket(),
});
const configuredEdgeStore = createEdgeStore({
router,
provider: edgestore(),
});
export default createEdgeStoreNextHandler({
edgestore: configuredEdgeStore,
});
/**
* This type is used to create the type-safe client for the frontend.
*/
export type EdgeStoreRouter = typeof router;
```
>
### Frontend
Now let's initiate our context provider.
<>
```ts title="src/lib/edgestore.ts"
'use client';
import { createEdgeStoreProvider } from '@edgestore/react';
import { type EdgeStoreRouter } from '../app/api/edgestore/[...edgestore]/route';
const { EdgeStoreProvider, useEdgeStore } =
createEdgeStoreProvider();
export { EdgeStoreProvider, useEdgeStore };
```
```ts title="src/lib/edgestore.ts"
'use client';
import { createEdgeStoreProvider } from '@edgestore/react';
import { type EdgeStoreRouter } from '../pages/api/edgestore/[...edgestore]';
const { EdgeStoreProvider, useEdgeStore } =
createEdgeStoreProvider();
export { EdgeStoreProvider, useEdgeStore };
```
>
And then wrap our app with the provider.
<>
```tsx title="src/app/layout.tsx"
// [!code ++]
import { EdgeStoreProvider } from '../lib/edgestore';
import './globals.css';
// ...
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{/* [!code ++] */}
{children}
);
}
```
```tsx title="src/pages/_app.tsx"
import '../styles/globals.css';
import type { AppProps } from 'next/app';
// [!code ++]
import { EdgeStoreProvider } from '../lib/edgestore';
export default function App({ Component, pageProps }: AppProps) {
return (
{/* [!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,
});
```
Use `deleteMany` to send one request for multiple files. Storage failures are
reported per URL:
```tsx
const result = await edgestore.publicFiles.deleteMany({
urls: selectedFiles.map((file) => file.url),
});
for (const failure of result.failed) {
console.error(failure.url, failure.error.code);
}
```
EdgeStore runs `beforeDelete` for every file before deleting any of them. If
one file is unauthorized, the entire request is rejected without calling the
storage provider. Once authorization succeeds, the provider may still return
partial storage failures in `result.failed`.
### Cancel upload
To cancel an ongoing file upload, you can use an AbortController the same way you would use it to cancel a fetch request.
```tsx
// prepare a state for the AbortController
const [abortController, setAbortController] = useState();
// ...
// instantiate the AbortController and add the signal to the upload method
const abortController = new AbortController();
setAbortController(abortController);
const res = await edgestore.publicFiles.upload({
file,
signal: abortController.signal,
});
// ...
// to cancel the upload, call the controller's abort method
abortController?.abort();
```
When you cancel an upload, an `UploadAbortedError` will be thrown.
You can catch this error and handle it as needed.
For more information, check the [Error Handling](/docs/error-handling) page.
### Transform files before upload
You can transform a file before EdgeStore validates and uploads it by passing
the `transform` option. If the transform keeps the same extension, you can
return the transformed `File` or `Blob` directly. If the transform changes the
file type, return the transformed file with its new extension.
For example, you can convert JPEG and PNG images to WebP before upload:
npm
pnpm
yarn
bun
```bash
npm install browser-image-compression
```
```bash
pnpm add browser-image-compression
```
```bash
yarn add browser-image-compression
```
```bash
bun add browser-image-compression
```
```tsx
import imageCompression from 'browser-image-compression';
const res = await edgestore.publicImages.upload({
file,
options: {
transform: async ({ file, extension, signal }) => {
if (!['image/jpeg', 'image/png'].includes(file.type)) {
return { file, extension };
}
const compressedFile = await imageCompression(file, {
fileType: 'image/webp',
initialQuality: 0.8,
useWebWorker: true,
signal,
});
return {
file: compressedFile,
extension: 'webp',
};
},
},
});
```
If you provide `manualFileName`, EdgeStore will use that exact file name. Make
sure the file name extension matches the transformed file type.
### Temporary files
You can upload temporary files by passing the `temporary` option to the `upload` method.
Temporary files will be automatically deleted after 24 hours if they are not confirmed.
```tsx
await edgestore.publicFiles.upload({
file: fileToUpload,
// [!code ++:3]
options: {
temporary: true,
},
});
```
To confirm a temporary file, you can use the `confirm` method.
```tsx
await edgestore.publicFiles.confirm({
url: urlToConfirm,
});
```
To confirm several temporary files in one request, use `confirmMany`:
```tsx
const result = await edgestore.publicFiles.confirmMany({
urls: temporaryFiles.map((file) => file.url),
});
```
You can check if a file is temporary in the dashboard.
Temporary files are marked with a clock icon.
## Troubleshooting
If you have any problems using EdgeStore, please check the [Troubleshooting](./troubleshooting) page.
## FAQ
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
EdgeStore is a type-safe file upload solution for React applications. It
provides an easy-to-use API for uploading, managing, and serving files with
features like progress tracking, file validation, and automatic cleanup of
temporary files.
Yes! The EdgeStore Provider has a free plan with generous limits, so you can
get started without any cost. You can also use EdgeStore with your own
infrastructure (like AWS S3 or Azure Blob Storage) if you prefer to manage
your own storage.
The EdgeStore packages (`@edgestore/server`, `@edgestore/react`, etc.) are
open source and released under the MIT license. However, the EdgeStore
Provider (cloud service) is not open source.
EdgeStore supports multiple frameworks including Next.js (App Router and Pages
Router), Astro, Express, Fastify, Hono, Remix, and TanStack Start. Check the
[Adapters](/docs/adapters/next) section for setup guides.
By default, EdgeStore accepts most common file types. You can customize
allowed file types and maximum file sizes per bucket using the `accept` and
`maxSize` options. See the [Configuration](/docs/configuration) page for
details.
Yes! EdgeStore supports AWS S3, Azure Blob Storage, and custom providers in
addition to EdgeStore Cloud. Check the [Providers](/docs/providers/edgestore)
section for setup instructions.
EdgeStore is not recommended for real-time streaming of large files like
videos and audio. It's optimized for file uploads and serving static files,
not for streaming use cases. For video/audio streaming, consider using a
dedicated streaming service or CDN.
# EdgeStore Docs: Low-level SDK
URL: /docs/sdk
Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/sdk.mdx
`@edgestore/sdk` is the public, low-level client for EdgeStore API v2. Use it
when you need API operations that are not tied to an EdgeStore router. For
router-derived bucket names, input, path, and metadata types, use the
[backend client](/docs/backend-client) instead.
The SDK is server-only. Project secrets and management tokens must never be
included in browser bundles.
## Project client
Project credentials expose runtime operations for the credential's current
project. The reserved project selector is handled internally.
```ts
import { createEdgeStoreSdk } from '@edgestore/sdk';
const sdk = createEdgeStoreSdk({
credentials: {
accessKey: process.env.EDGE_STORE_ACCESS_KEY!,
secretKey: process.env.EDGE_STORE_SECRET_KEY!,
},
});
const file = await sdk.runtime.uploads.upload({
bucket: 'documents',
source: pdfBlob,
fileName: 'invoice.pdf',
metadata: { invoiceId: invoice.id },
signal,
onProgress: ({ percentage, phase }) => {
console.log(phase, percentage);
},
});
```
The upload helper selects single or multipart upload automatically, reports
progress, supports cancellation, and waits for upload processing to finish.
Upload creation is never retried because it is not idempotent. Signed storage
transfers may be retried safely, and processing polls follow `Retry-After`. You
can also use `runtime.uploads.request`, `createParts`, and `completeMultipart`
when you need to manage transfer details yourself.
Sources can be text, a `Blob`, an `ArrayBuffer` or typed-array view, or a
known-size Web `ReadableStream`:
```ts
await sdk.runtime.uploads.upload({
bucket: 'archives',
source: { stream, sizeBytes },
fileName: 'archive.tar',
});
await sdk.runtime.uploads.uploadFromUrl({
bucket: 'imports',
url: 'https://example.com/report.csv',
});
```
The defaults are a 100 MiB multipart threshold, 16 MiB parts, concurrency 4, a
30-second control timeout, no transfer timeout, and a 60-second processing
timeout. Configure upload defaults with the `upload` option on
`createEdgeStoreSdk`; pass an `AbortSignal` for per-operation cancellation or
deadlines.
## Runtime resources
```ts
const page = await sdk.runtime.files.search({
bucket: 'documents',
filter: { metadata: { ownerId: user.id } },
pagination: { limit: 50 },
});
if (page.pagination.hasMore) {
const nextPage = await sdk.runtime.files.search({
bucket: 'documents',
pagination: {
cursor: page.pagination.nextCursor ?? undefined,
limit: 50,
},
});
}
const { signedUrls } = await sdk.runtime.files.generateSignedReadUrls({
bucket: 'documents',
urls: page.files.map((file) => file.url),
expiresIn: 15 * 60,
});
await sdk.runtime.files.confirm({ file: { id: file.file.id } });
await sdk.runtime.files.delete({ file: { key: file.file.key } });
const batch = await sdk.runtime.files.deleteMany({
files: [{ id: file.file.id }, { url: legacyFileUrl }],
});
for (const result of batch.results) {
if (!result.success) console.error(result.fileRef, result.error.code);
}
```
Runtime resources also include projects, buckets, file lookup, signed read
URLs, access tokens, upload inspection, cancellation, and singular or plural
restore operations. Singular mutations throw `EdgeStoreFileMutationError` for
an item failure; plural mutations return the complete partial result.
## Management client
A management token uses Bearer authentication and exposes account, project,
credential, token, and membership resources. Runtime calls can select a
project per operation or create an eagerly scoped runtime client once.
```ts
const management = createEdgeStoreSdk({
credentials: {
token: process.env.EDGE_STORE_MANAGEMENT_TOKEN!,
},
});
const projects = await management.management.projects.list({
account: 'account-id',
});
const project = management.runtime.forProject(projects.projects[0]!.id);
const buckets = await project.buckets.list();
// An explicit selector remains useful for one-off calls.
const anotherProject = await management.runtime.projects.get({
project: 'another-project-id',
});
const { accessUrls } =
await management.management.files.generateAccessUrls({
project: projects.projects[0]!.id,
files: [{ id: 'file-id' }],
expiresIn: 15 * 60,
});
```
## Reusing SDK types
Common runtime workflows have named public input and result types:
```ts
import type {
RuntimeFileLookupInput,
RuntimeFileLookupResult,
RuntimeSignedReadUrlsGenerateInput,
RuntimeSignedReadUrlsGenerateResult,
RuntimeUploadInput,
RuntimeUploadResult,
} from '@edgestore/sdk';
```
For any other operation, derive its friendly input and resolved output from the
public SDK interface. This includes SDK selectors such as `account`, `project`,
and `bucket`, plus `signal`.
```ts
import type { ManagementEdgeStoreSdk } from '@edgestore/sdk';
type CreateProjectMethod =
ManagementEdgeStoreSdk['management']['projects']['create'];
type CreateProjectInput = Parameters[0];
type CreateProjectOutput = Awaited>;
```
You can use the same pattern with a configured SDK value:
```ts
type LookupInput = Parameters[0];
type LookupOutput = Awaited<
ReturnType
>;
```
The generated OpenAPI operation types remain internal. Deriving from the public
interface keeps application types aligned with the supported SDK surface.
## Errors
```ts
import { EdgeStoreApiError, EdgeStoreNetworkError } from '@edgestore/sdk';
try {
await sdk.runtime.files.lookup({ file: { url } });
} catch (error) {
if (error instanceof EdgeStoreApiError) {
console.error(error.status, error.code, error.requestId);
} else if (error instanceof EdgeStoreNetworkError) {
console.error('The API could not be reached', error.cause);
}
}
```
API errors preserve the HTTP status, machine-readable code, details, request
ID, and retry guidance. Abort, network, upload, cancellation, and processing
timeout errors have dedicated classes.
## Custom environments
Use `apiUrl` for a compatible API v2 deployment and `fetch` to provide a
custom server-side transport implementation.
```ts
const sdk = createEdgeStoreSdk({
credentials: { accessKey, secretKey },
apiUrl: 'https://api.example.com/v2',
fetch: instrumentedFetch,
});
```
`apiUrl` is the complete v2 URL. The SDK does not append `/v2` to an explicit
value.
# EdgeStore Docs: Troubleshooting
URL: /docs/troubleshooting
Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/(getting-started)/troubleshooting.mdx
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
## 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
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.
npm
pnpm
yarn
bun
```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 { createEdgeStore, initEdgeStore } from '@edgestore/server';
import { createEdgeStoreAstroHandler } from '@edgestore/server/adapters/astro';
import { edgestore } from '@edgestore/server/providers/edgestore';
export const prerender = false;
const es = initEdgeStore.create();
const router = es.router({
publicFiles: es.fileBucket(),
});
const configuredEdgeStore = createEdgeStore({
router,
provider: edgestore(),
});
const handler = createEdgeStoreAstroHandler({
edgestore: configuredEdgeStore,
});
export { handler as GET, handler as POST };
export type EdgeStoreRouter = typeof router;
```
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
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.
npm
pnpm
yarn
bun
```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 { createEdgeStore, initEdgeStore } from '@edgestore/server';
import { createEdgeStoreExpressHandler } from '@edgestore/server/adapters/express';
import { edgestore } from '@edgestore/server/providers/edgestore';
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 router = es.router({
publicFiles: es.fileBucket(),
});
export type EdgeStoreRouter = typeof router;
const configuredEdgeStore = createEdgeStore({
router,
provider: edgestore(),
});
const handler = createEdgeStoreExpressHandler({
edgestore: configuredEdgeStore,
});
// --- 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
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.
npm
pnpm
yarn
bun
```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 { createEdgeStore, initEdgeStore } from '@edgestore/server';
import { createEdgeStoreFastifyHandler } from '@edgestore/server/adapters/fastify';
import { edgestore } from '@edgestore/server/providers/edgestore';
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 router = es.router({
publicFiles: es.fileBucket(),
});
export type EdgeStoreRouter = typeof router;
const configuredEdgeStore = createEdgeStore({
router,
provider: edgestore(),
});
const handler = createEdgeStoreFastifyHandler({
edgestore: configuredEdgeStore,
});
// --- 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
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.
npm
pnpm
yarn
bun
```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 { createEdgeStore, initEdgeStore } from '@edgestore/server';
import { createEdgeStoreHonoHandler } from '@edgestore/server/adapters/hono';
import { edgestore } from '@edgestore/server/providers/edgestore';
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 router = es.router({
publicFiles: es.fileBucket(),
});
export type EdgeStoreRouter = typeof router;
const configuredEdgeStore = createEdgeStore({
router,
provider: edgestore(),
});
const handler = createEdgeStoreHonoHandler({
edgestore: configuredEdgeStore,
});
// --- 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
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
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.
npm
pnpm
yarn
bun
```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 { createEdgeStore, initEdgeStore } from '@edgestore/server';
import { createEdgeStoreRemixHandler } from '@edgestore/server/adapters/remix';
import { edgestore } from '@edgestore/server/providers/edgestore';
const es = initEdgeStore.create();
const router = es.router({
publicFiles: es.fileBucket(),
});
export type EdgeStoreRouter = typeof router;
const configuredEdgeStore = createEdgeStore({
router,
provider: edgestore(),
});
const handler = createEdgeStoreRemixHandler({
edgestore: configuredEdgeStore,
});
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
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.
npm
pnpm
yarn
bun
```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 { createEdgeStore, initEdgeStore } from '@edgestore/server';
import { createEdgeStoreStartHandler } from '@edgestore/server/adapters/start';
import { edgestore } from '@edgestore/server/providers/edgestore';
import { createAPIFileRoute } from '@tanstack/start/api';
const es = initEdgeStore.create();
const router = es.router({
publicFiles: es.fileBucket(),
});
export type EdgeStoreRouter = typeof router;
const configuredEdgeStore = createEdgeStore({
router,
provider: edgestore(),
});
const handler = createEdgeStoreStartHandler({
edgestore: configuredEdgeStore,
});
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
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
npm
pnpm
yarn
bun
```bash
npx shadcn@latest add https://edgestore.dev/r/dropzone.json
```
```bash
pnpm dlx shadcn@latest add https://edgestore.dev/r/dropzone.json
```
```bash
yarn dlx shadcn@latest add https://edgestore.dev/r/dropzone.json
```
```bash
bun x shadcn@latest add https://edgestore.dev/r/dropzone.json
```
### 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 (
);
},
);
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
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
npm
pnpm
yarn
bun
```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
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
npm
pnpm
yarn
bun
```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 (
{/* 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 (
);
}
```
# 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
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
npm
pnpm
yarn
bun
```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 (
);
}
```
### 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: Azure Blob Storage
URL: /docs/providers/azure-blob
Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/providers/azure-blob.mdx
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.
The provider creates short-lived, blob-scoped SAS URLs for browser uploads and
private reads. Permanent file URLs never contain credentials. Advanced hosted
features such as image processing still require separate infrastructure.
## Installation
You need to install some peer dependencies to use this provider.
npm
pnpm
yarn
bun
```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 configure the provider together with the router and pass the resulting
`configuredEdgeStore` instance to the adapter.
```ts
import { createEdgeStore, initEdgeStore } from '@edgestore/server';
import {
createEdgeStoreNextHandler,
type CreateContextOptions,
} from '@edgestore/server/adapters/next/pages';
// [!code ++]
import { azureBlob } from '@edgestore/server/providers/azure-blob';
import { z } from 'zod';
// ...
const configuredEdgeStore = createEdgeStore({
router,
// [!code ++]
provider: azureBlob(),
});
export default createEdgeStoreNextHandler({
edgestore: configuredEdgeStore,
createContext,
});
```
## Options
```ts
export type AzureBlobProviderOptions = {
/**
* The storage account name for Azure Blob Storage
* Can also be set via the `ES_AZURE_ACCOUNT_NAME` environment variable.
*/
storageAccountName?: string;
/**
* Account key used for server-side requests and signing short-lived URLs.
* Can also be set via the `ES_AZURE_ACCOUNT_KEY` environment variable.
*/
storageAccountKey?: string;
/**
* Azure Blob Storage container name
* Can also be set via the `ES_AZURE_CONTAINER_NAME` environment variable.
*/
containerName?: string;
/**
* Optional base URL for Azurite or another compatible endpoint.
* Can also be set via the `ES_AZURE_BASE_URL` environment variable.
*/
customBaseUrl?: string;
/**
* Lifetime of browser upload URLs in seconds.
* @default 3600
*/
uploadUrlExpiresIn?: number;
/**
* Default lifetime of private read URLs in seconds.
* @default 3600
*/
signedUrlExpiresIn?: number;
};
```
The account key stays on the server. Upload URLs receive only create/write
permissions, while private read URLs receive only read permission and expire
independently.
# EdgeStore Docs: Custom
URL: /docs/providers/custom
Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/providers/custom.mdx
Use `defineProvider` to integrate another storage service or a custom storage
implementation. The same provider instance powers the EdgeStore HTTP handler
and the router-derived backend client:
```ts
const configuredEdgeStore = createEdgeStore({
router,
provider: myProvider,
});
createEdgeStoreNextHandler({ edgestore: configuredEdgeStore });
configuredEdgeStore.client.files.get({ key: 'files/report.pdf' });
```
Method inputs are contextually typed. File references, cursors, returned file
fields, and provider-specific errors are inferred from the definition, so you
do not need to maintain a parallel provider interface or backend-operation
map.
## Minimal provider
A provider must implement the upload-planning operations used by browser
uploads and `files.get`, which EdgeStore uses to resolve a file before running
router lifecycle hooks. Other operations are optional and only appear on the
backend client when implemented.
This single-part example delegates storage-specific work to a small driver:
```ts title="src/lib/my-provider.ts"
import { defineProvider } from '@edgestore/server';
import { z } from 'zod';
import { storage } from './storage';
const baseUrl = 'https://files.example.com';
export const myProvider = defineProvider({
name: 'my-storage',
baseUrl,
async init() {
return {};
},
reference: {
schema: z.object({ key: z.string().min(1) }),
fromUrl(url) {
return { key: new URL(url).pathname.slice(1) };
},
},
uploads: {
async request({ bucketName, fileInfo }) {
const key = storage.createKey({ bucketName, fileInfo });
return {
uploadUrl: await storage.signUpload(key),
accessUrl: `${baseUrl}/${key}`,
};
},
},
files: {
async get({ bucketName, file }) {
const object = await storage.head({
bucketName,
key: file.key,
});
return {
url: `${baseUrl}/${file.key}`,
sizeBytes: object.size,
path: object.path,
metadata: object.metadata,
uploadedAt: object.createdAt,
updatedAt: object.updatedAt,
};
},
},
});
```
The resulting backend client exposes `get`, but not `upload`, `list`, or
mutations, because those optional capabilities were not defined.
## Complete provider
A complete provider can add direct backend uploads, pagination, mutations, and
signed read URLs. Standard Schema transformations let callers use convenient
input references while provider methods receive one normalized output shape:
```ts title="src/lib/my-provider.ts"
import { defineProvider } from '@edgestore/server';
import { z } from 'zod';
import { storage } from './storage';
const baseUrl = 'https://files.example.com';
const referenceSchema = z
.union([z.string().url(), z.object({ key: z.string().min(1) })])
.transform((reference) =>
typeof reference === 'string'
? { key: new URL(reference).pathname.slice(1) }
: reference,
);
const toFile = (object: Awaited>) => ({
url: `${baseUrl}/${object.key}`,
sizeBytes: object.size,
path: object.path,
metadata: object.metadata,
uploadedAt: object.createdAt,
updatedAt: object.updatedAt,
etag: object.etag,
});
export const myProvider = defineProvider({
name: 'my-storage',
baseUrl,
async init() {
return {};
},
reference: {
schema: referenceSchema,
fromUrl: (url) => url,
},
uploads: {
async request({ bucketName, fileInfo }) {
return storage.createUploadPlan({ bucketName, fileInfo });
},
multipart: {
async requestParts({ multipart, path }) {
return storage.createUploadParts({ multipart, path });
},
async complete({ uploadId, key, parts }) {
await storage.completeMultipart({ uploadId, key, parts });
},
},
async upload({ bucketName, fileInfo, source, signal, onProgress }) {
const object = await storage.upload({
bucketName,
fileInfo,
source,
signal,
onProgress,
});
return { file: toFile(object) };
},
},
files: {
cursorSchema: z.string().min(1),
async get({ bucketName, file }) {
return toFile(
await storage.head({
bucketName,
key: file.key,
}),
);
},
async list({ bucketName, cursor, filter, limit = 20 }) {
const page = await storage.list({ bucketName, cursor, filter, limit });
return {
items: page.objects.map(toFile),
limit,
nextCursor: page.nextCursor,
hasMore: page.nextCursor !== null,
};
},
async confirm({ bucketName, files }) {
return {
results: await storage.confirm({
bucketName,
files,
}),
};
},
async delete({ bucketName, files }) {
return {
results: await storage.delete({
bucketName,
files,
}),
};
},
async restore({ bucketName, files }) {
return {
results: await storage.restore({
bucketName,
files,
}),
};
},
async getSignedUrls({ bucketName, files, expiresIn = 3600 }) {
return Promise.all(
files.map(async (file) => ({
url: `${baseUrl}/${file.key}`,
signedUrl: await storage.signRead({
bucketName,
key: file.key,
expiresIn,
}),
expiresAt: new Date(Date.now() + expiresIn * 1000),
expiresIn,
})),
);
},
},
});
```
Here, callers may pass either a URL or `{ key }`, but every provider operation
receives `{ key: string }` after runtime validation. The backend client also
inherits the string cursor, the extra `etag` file field, and any literal
mutation error codes returned by the storage driver.
The backend client uses bucket-scoped names: `get`, `list`, `confirm`,
`confirmMany`, `delete`, `deleteMany`, `restore`, `restoreMany`,
`createSignedUrl`, and `createSignedUrls`. Provider method names remain
resource operations (`files.get`, `files.list`, `files.getSignedUrls`) and do
not need to mirror that public client surface.
`get` and `list` preserve the exact file fields inferred from the provider,
including its `path` and `metadata` shapes. An `upload` result instead exposes
the path and metadata EdgeStore computed from the router, even if the storage
driver returns different placeholders.
Every file operation must enforce the logical EdgeStore `bucketName`. Treat
`bucketName` and the normalized file reference as the complete storage
identity, and reject a reference that belongs to another logical bucket. This
keeps a frontend request authorized through bucket A from loading or mutating a
file in bucket B. The official providers enforce the same ownership invariant.
Mutation providers return exactly one status for each input file, in the same
order. EdgeStore attaches the original file references and derives success and
failure counts for the public client. A provider result therefore contains
only `{ success: true }` or `{ success: false, error }`; it does not repeat the
file reference or calculate counts.
Multipart support is optional. A single-part provider defines only
`uploads.request`. When `uploads.multipart` is present, `uploads.request` may
return a multipart plan and EdgeStore exposes the matching part-request and
completion routes.
`uploads.request` and `uploads.upload` remain separate because they perform
different work: the former creates signed instructions for a browser transfer,
while the latter receives bytes and performs a privileged server-side upload.
## Operation groups
The provider is organized by storage resource, not by caller:
* `uploads` contains browser upload planning and optional direct backend
upload.
* `files` contains canonical file operations shared by HTTP adapters and the
backend client.
* `reference` defines how frontend URLs and backend inputs become the
provider's normalized file reference.
Frontend deletion still obeys the router: EdgeStore loads every file and runs
`beforeDelete` for all of them before calling `files.delete`. The privileged
backend client calls `files.delete` directly and is responsible for its own
authorization.
The official EdgeStore, S3, and Azure Blob providers use `defineProvider`
themselves and are useful reference implementations. If your provider could be
useful to others, consider contributing it to EdgeStore.
# EdgeStore Docs: EdgeStore
URL: /docs/providers/edgestore
Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/providers/edgestore.mdx
The hosted `edgestore()` provider uses EdgeStore API v2. Configure it together
with your router, then pass that configured instance to the HTTP adapter.
```ts
import { createEdgeStore, initEdgeStore } from '@edgestore/server';
import {
createEdgeStoreNextHandler,
type CreateContextOptions,
} from '@edgestore/server/adapters/next/pages';
// [!code ++]
import { edgestore } from '@edgestore/server/providers/edgestore';
import { z } from 'zod';
const configuredEdgeStore = createEdgeStore({
router,
// [!code ++]
provider: edgestore(),
});
export default createEdgeStoreNextHandler({
edgestore: configuredEdgeStore,
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;
/**
* Override the API v2 base URL.
*/
apiUrl?: string;
};
```
# EdgeStore Docs: S3
URL: /docs/providers/s3
Source: https://raw.githubusercontent.com/edgestorejs/edgestore/refs/heads/main/docs/content/docs/providers/s3.mdx
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 S3 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.
npm
pnpm
yarn
bun
```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 configure the provider together with the router and pass the resulting
`configuredEdgeStore` instance to the adapter.
```ts
import { createEdgeStore, initEdgeStore } from '@edgestore/server';
import {
createEdgeStoreNextHandler,
type CreateContextOptions,
} from '@edgestore/server/adapters/next/pages';
// [!code ++]
import { s3 } from '@edgestore/server/providers/s3';
import { z } from 'zod';
// ...
const configuredEdgeStore = createEdgeStore({
router,
// [!code ++]
provider: s3(),
});
export default createEdgeStoreNextHandler({
edgestore: configuredEdgeStore,
createContext,
});
```
## Options
```ts
export type S3ProviderOptions = {
/**
* AWS SDK credentials (or credentials provider) to use for S3 requests.
*
* If unset, the AWS SDK will use its default credential provider chain
* (environment variables, shared config files, instance/task roles, etc).
*/
credentials?: S3ClientConfig['credentials'];
/**
* 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;
/**
* Customizes the object path beneath the logical EdgeStore bucket prefix.
*
* The logical bucket prefix is always preserved so router authorization for
* one bucket cannot access objects from another.
*/
path?: (args: {
edgestoreBucketName: string;
fileInfo: FileInfo;
defaultPath: string;
}) => Promise | string;
};
```
## Customizing S3 Object Paths
By default, the S3 provider uses the same path-generation logic as the hosted
provider. The first object-key segment is always the logical EdgeStore router
bucket. This lets multiple logical buckets safely share one physical S3 bucket:
```text
documents/_public/acme/invoice.pdf
avatars/_public/user-123/profile.png
```
Use `path` to customize everything beneath that protected prefix. For example,
this removes `_public` while keeping the remaining generated path:
```ts title="src/server/edgestore.ts"
const configuredEdgeStore = createEdgeStore({
router,
provider: s3({
path: ({ defaultPath }) => {
// `documents/_public/acme/invoice.pdf`
// becomes `documents/acme/invoice.pdf`
return defaultPath.replace(/^_public\//, '');
},
}),
});
const handler = createEdgeStoreNextHandler({
edgestore: configuredEdgeStore,
createContext,
});
```
The callback returns a path relative to the logical bucket and cannot escape
that boundary. It also receives `edgestoreBucketName` and `fileInfo` when you
need provider-specific naming logic.
If you remove `_public`, you might also want to disable the development proxy
in `createEdgeStoreProvider`.
```ts
const { EdgeStoreProvider, useEdgeStore } =
createEdgeStoreProvider({
disableDevProxy: true,
});
```
## Using with Minio
You can use the S3 provider with MinIO or other S3-compatible storage providers
by setting the `endpoint` and `forcePathStyle` options.
```ts
provider: s3({
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
}),
```