EdgeStore

Quick Start

Implement file uploads in your app with EdgeStore.

Set up with your agent

Read https://edgestore.dev/SKILL.md and add file uploads to this application.

Install the skill or connect MCP.

Step-by-step setup

Next.js Setup

Install

npm install @edgestore/server@rc @edgestore/react@rc

Environment Variables

Copy your project's keys from the dashboard to the backend env file:

.env.local
EDGESTORE_ACCESS_KEY=your-access-key
EDGESTORE_SECRET_KEY=your-secret-key

Use the app's existing env file, or .env.local for a new app. Keep it gitignored.

Backend

Choose your router below. This bucket's files are accessible to anyone with the link.

src/app/api/edgestore/[...edgestore]/route.ts
import { initEdgeStore } from '@edgestore/server';
import { createEdgeStoreNextHandler } from '@edgestore/server/adapters/next/app';

const es = initEdgeStore.create();

/**
 * This is the main router for the EdgeStore buckets.
 */
const router = es.router({
  publicFiles: es.fileBucket(),
});

const handler = createEdgeStoreNextHandler({
  router,
});

export { handler as GET, handler as POST };

/**
 * This type is used to create the type-safe client for the frontend.
 */
export type EdgeStoreRouter = typeof router;
src/pages/api/edgestore/[...edgestore].ts
import { initEdgeStore } from '@edgestore/server';
import { createEdgeStoreNextHandler } from '@edgestore/server/adapters/next/pages';

const es = initEdgeStore.create();

/**
 * This is the main router for the edgestore buckets.
 */
const router = es.router({
  publicFiles: es.fileBucket(),
});

export default createEdgeStoreNextHandler({
  router,
});

/**
 * This type is used to create the type-safe client for the frontend.
 */
export type EdgeStoreRouter = typeof router;

Frontend

Create the React provider:

src/lib/edgestore.ts
'use client';

import { createEdgeStoreProvider } from '@edgestore/react';
import { type EdgeStoreRouter } from '../app/api/edgestore/[...edgestore]/route';

const { EdgeStoreProvider, useEdgeStore } =
  createEdgeStoreProvider<EdgeStoreRouter>();

export { EdgeStoreProvider, useEdgeStore };
src/lib/edgestore.ts
'use client';

import { createEdgeStoreProvider } from '@edgestore/react';
import { type EdgeStoreRouter } from '../pages/api/edgestore/[...edgestore]';

const { EdgeStoreProvider, useEdgeStore } =
  createEdgeStoreProvider<EdgeStoreRouter>();

export { EdgeStoreProvider, useEdgeStore };

Wrap your app with the provider:

src/app/layout.tsx
import { EdgeStoreProvider } from '../lib/edgestore';
import './globals.css';

// ...

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <EdgeStoreProvider>{children}</EdgeStoreProvider>
      </body>
    </html>
  );
}
src/pages/_app.tsx
import '../styles/globals.css';
import type { AppProps } from 'next/app';
import { EdgeStoreProvider } from '../lib/edgestore';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <EdgeStoreProvider>
      <Component {...pageProps} />
    </EdgeStoreProvider>
  );
}

Upload file

You can use the useEdgeStore hook to access type-safe frontend client and use it to upload files.

'use client';

import * as React from 'react';
import { useEdgeStore } from '../lib/edgestore';

export default function Page() {
  const [file, setFile] = React.useState<File>();
  const { edgestore } = useEdgeStore();

  return (
    <div>
      <input
        type="file"
        onChange={(e) => {
          setFile(e.target.files?.[0]);
        }}
      />
      <button
        onClick={async () => {
          if (file) {
            const res = await edgestore.publicFiles.upload({
              file,
              onProgressChange: (progress) => {
                // you can use this to show a progress bar
                console.log(progress);
              },
            });
            // you can run some server action or api here
            // to save `res.id` and `res.url` in your database
            console.log(res);
          }
        }}
      >
        Upload
      </button>
    </div>
  );
}

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.

const res = await edgestore.publicFiles.upload({
  file,
  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 on the bucket.

await edgestore.publicFiles.delete({
  url: urlToDelete,
});

Use deleteMany to send one request for multiple files. Storage failures are reported per URL:

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.

// prepare a state for the AbortController
const [abortController, setAbortController] = useState<AbortController>();

// ...

// 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 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 install browser-image-compression
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.

await edgestore.publicFiles.upload({
  file: fileToUpload,
  options: {
    temporary: true,
  },
});

For forms, upload files as temporary and save their returned IDs/keys and URLs with your record before confirming them. If the database save fails, leave the files temporary. Confirmation does not require waiting for hosted processing.

Use the confirm method after saving:

await edgestore.publicFiles.confirm({
  url: urlToConfirm,
});

To confirm several temporary files in one request, use confirmMany:

const result = await edgestore.publicFiles.confirmMany({
  urls: temporaryFiles.map((file) => file.url),
});

You can check if a file is temporary in the dashboard.
Temporary files are marked with a clock icon.

Wait for processing

upload resolves as soon as the file is transferred. EdgeStore then processes it in the background, for example to generate an image thumbnail. The file ID and URL are final right away, so most apps can save them without waiting.

When you need processed details immediately, such as whether the image got a thumbnail, pass waitForProcessing. The upload then resolves with the processed file, including freshly signed URLs for buckets with autoSignedUrls, and onPhaseChange tells you when the transfer finishes and processing begins.

const res = await edgestore.publicImages.upload({
  file,
  onProgressChange: (progress) => setProgress(progress),
  onPhaseChange: (phase) => {
    if (phase === 'processing') setStatus('Processing…');
  },
  options: {
    waitForProcessing: true, // or { timeoutMs: 120_000 }
  },
});

Processing failures reject with UploadCanceledError. If processing takes longer than timeoutMs (60 seconds by default), the upload rejects with UploadProcessingTimeoutError; the file may still finish processing. Both errors include the file id. See Error Handling.

Waiting for processing does not confirm a temporary file. Confirm it separately once your app decides to keep it.

Providers without background processing, such as S3 and Azure Blob, resolve once the transfer completes.

Troubleshooting

If you have any problems using EdgeStore, please check the Troubleshooting page.

FAQ

On this page