2.0 beta — new releases can include breaking changes. Stable 1.x docs
dimah-s3v2.0.0-beta.0

Delete Hooks

Authorize deletion and run cleanup after S3 removes the object.

Enable object deletion with delete: true or a DeleteConfig object on a named route.

Unlike upload and download, deletes run on your server with DeleteObject after the guards pass. The client never deletes from S3 directly.

delete: true turns the endpoint on. It does not check who owns the object. The key must still sit under this route's keyPrefix (the route name by default) — that check always runs, with or without db(). Ownership is delete.guard or the db plugin. See Security.

lib/s3.ts
import { dimahS3, route, s3Error } from "@dimah-s3/server";
import { awsS3 } from "./aws-s3";

export const s3 = dimahS3({
  client: awsS3,
  bucket: process.env.S3_BUCKET!,
  routes: {
    avatar: route({
      delete: {
        guard: async ({ key, request }) => {
          const session = await getSession(request);
          if (!session) throw s3Error("UNAUTHORIZED");

          const isOwner = await checkAvatarOwnership(session.userId, key);
          if (!isOwner) throw s3Error("FORBIDDEN");
        },
        onDeleted: async ({ key, request }) => {
          const session = await getSession(request); 
          await db.user.update({
            where: { id: session.userId }, 
            data: { avatarKey: null }, 
          }); 
        }, 
      },
    }),
  },
});

Delete lifecycle flow

Before delete.guard, the server resolves the named route and rejects keys outside its keyPrefix (KEY_INVALID).

Delete flow
  1. route.guardhook
  2. delete.guardhook
  3. DeleteObjectS3
  4. onDeletedDatabase cleanup

Delete is idempotent. A key that never landed, or one that is already gone, succeeds and still runs onDeleted, so the bookkeeping for a pending or expired upload is cleaned up. The response is { key, bucket }.

The S3 call does not stop when the client disconnects, so onDeleted runs for every delete that happened.


Deleting many keys

deleteMany (POST /delete/batch) deletes up to 100 keys of one route (DELETE_MANY_MAX_KEYS) in one DeleteObjects request:

  • The route guard runs once for the call.
  • Each key is checked against the route namespace and delete.guard.
  • onDeleted runs for each deleted key.
  • A per-key failure (an invalid key, a guard, an S3 error, or an onDeleted throw) goes into failed and does not fail the other keys.
const { deleted, failed } = await s3.api.deleteMany(
  { route: "avatar", keys: [key1, key2] },
  { headers: await headers() },
);
// failed: [{ key, error: { code, message, params? } }]

A route error, such as UNKNOWN_ROUTE or a route guard rejection, fails the whole call. So does a DeleteObjects request that fails as a whole.


Type reference

import type {
  DeleteGuardContext,
  DeleteOnDeletedContext,
} from "@dimah-s3/server";

DeleteGuardContext

Prop

Type


DeleteOnDeletedContext

Prop

Type

Frequently asked questions

On this page