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

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

<Callout>
  Unlike upload and download, object deletion is executed directly on your
  server using `DeleteObjectCommand` after passing `guard` checks. 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://v1.s3.dimah.dev/docs/db).
  Ownership is `delete.guard` or the db plugin. See
  [Security](https://v1.s3.dimah.dev/docs/server/security#route-namespace-vs-ownership).
</Callout>

```ts title="lib/s3.ts"
import { S3Client } from "@aws-sdk/client-s3";
import { dimahS3, errors, route } from "@dimah-s3/server";

export const awsS3 = new S3Client({/* env */});

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 errors.unauthorized();

          const isOwner = await checkAvatarOwnership(session.userId, key);
          if (!isOwner) throw errors.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` (`INVALID_KEY`).

<Flow
  label="Delete flow"
  steps={[
    { name: "delete.guard", kind: "hook" },
    { name: "HeadObject in S3", kind: "s3" },
    { name: "DeleteObject in S3", kind: "s3", note: "Only if it exists" },
    { name: "onDeleted", kind: "hook", note: "Database cleanup" },
  ]}
/>

Delete is idempotent. When HeadObject finds nothing — an upload that was
presigned but never sent, an aborted multipart upload, or an object a bucket
lifecycle rule already removed — the server skips `DeleteObject`, still runs
`onDeleted` with `existed: false`, and responds with
`{ success: true, existed: false }`. A database row for that key is cleaned up
the same way as for a stored object.

---

## 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://v1.s3.dimah.dev/docs/db). See
[Security](https://v1.s3.dimah.dev/docs/server/security#route-namespace-vs-ownership).

</Accordion>

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

Use `deleteMany` on the server API or `removeMany` in the React client. Batch
deletion runs the same flow for each key — `delete.guard`, S3 `DeleteObject`
when the object exists, then `onDeleted` — and each result reports `existed`:

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

</Accordion>

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

`onDeleted` does not run for a failed S3 deletion. The endpoint returns an
error response such as `502 S3_NETWORK_ERROR` or `INTERNAL_ERROR`.

</Accordion>

</Accordions>
