# Delete Hooks (https://v2.s3.dimah.dev/docs/server/hooks/delete)

Enable object deletion with `delete: true` or a `DeleteConfig` object on a [named route](https://v2.s3.dimah.dev/docs/server/routes).

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

<Callout type="warn">
  `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()`](https://v2.s3.dimah.dev/docs/db).
  Ownership is `delete.guard` or the db plugin. See
  [Security](https://v2.s3.dimah.dev/docs/server/security#route-namespace-vs-ownership).
</Callout>

```ts title="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 }) => {
          // [!code ++]
          const session = await getSession(request); // [!code ++]
          await db.user.update({
            // [!code ++]
            where: { id: session.userId }, // [!code ++]
            data: { avatarKey: null }, // [!code ++]
          }); // [!code ++]
        }, // [!code ++]
      },
    }),
  },
});
```

---

## Delete lifecycle flow [#delete-lifecycle-flow]

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

<Flow
  label="Delete flow"
  steps={[
    { name: "route.guard", kind: "hook" },
    { name: "delete.guard", kind: "hook" },
    { name: "DeleteObject", kind: "s3" },
    { name: "onDeleted", kind: "hook", note: "Database 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 [#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.

```ts
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 [#type-reference]

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

### DeleteGuardContext [#deleteguardcontext]

<AutoTypeTable
  path="packages/server/src/types/hook-contexts.ts"
  name="DeleteGuardContext"
/>

---

### DeleteOnDeletedContext [#deleteondeletedcontext]

<AutoTypeTable
  path="packages/server/src/types/hook-contexts.ts"
  name="DeleteOnDeletedContext"
/>

## Frequently asked questions [#frequently-asked-questions]

<Accordions>

<Accordion title="Can anyone delete any object if I enable delete without a guard?">

Anyone who can call the endpoint can delete any **in-namespace** key they
know — not the whole bucket, and not keys on other routes. A UUID in the path
is not an access check. Ownership is `delete.guard` or [`db()`](https://v2.s3.dimah.dev/docs/db). See
[Security](https://v2.s3.dimah.dev/docs/server/security#route-namespace-vs-ownership).

</Accordion>

<Accordion title="How do I delete multiple files at once?">

Use `s3.api.deleteMany` on the server, or `removeMany` from `useDelete` in
React:

```ts title="Client usage"
const { removeMany } = useDelete({ route: "avatar" });
const { deleted, failed } = await removeMany([key1, key2]);
```

</Accordion>

<Accordion title="What happens if S3 deletion fails?">

`onDeleted` does not run for a failed S3 deletion. A single delete returns an
error such as `502 S3_NETWORK_ERROR` or `500 INTERNAL_ERROR`. In
`deleteMany`, a key that S3 refused is reported in `failed` with
`INTERNAL_ERROR`; the S3 error itself is logged and passed to `onError`.

</Accordion>

</Accordions>
