# API (https://s3.dimah.dev/docs/db/api)



## Server [#server]

With the plugin registered, `s3.db.objects` is available in routes, quota
checks, and jobs:

```ts
const scope = "user:123";
const ref = { bucket: "my-bucket", key: "avatar/photo.jpg" };

const objects = await s3.db.objects.listByScope({
  scope,
  status: "active",
  route: "avatar",
  limit: 50,
  cursor: previous.nextCursor ?? undefined,
});

const count = await s3.db.objects.countByScope(scope, "active");
const usage = await s3.db.objects.getScopeUsage(scope);

const object = await s3.db.objects.find({ bucket: ref.bucket, key: ref.key });
const active = await s3.db.objects.findActive({
  bucket: ref.bucket,
  key: ref.key,
});

const pending = await s3.db.objects.findPendingMultipart({
  bucket: ref.bucket,
  key: ref.key,
  fileSize: 1024000,
});
```

### StorageObjectStore [#storageobjectstore]

```ts
import type { StorageObjectStore, StorageObject } from "@dimah-s3/db";
```

<AutoTypeTable path="packages/db/src/store/storage-object-store.ts" name="StorageObjectStore" />

To remove a file, use [`api.delete`](https://s3.dimah.dev/docs/react) — not store helpers. See
[delete behavior](https://s3.dimah.dev/docs/db/setup#delete-behavior).

`GET /db/objects` (browser `listObjects`) defaults to `status: active` and
`limit: 50`, and rejects a `limit` above `100`. It returns `nextCursor`
when another page exists. `GET /db/object` (`getObject`) loads one row by
key. Pass `limit` on `listByScope` yourself for server listings.

## Browser [#browser]

Register `dbClient()` once. In a client component, `s3Client.useApi()` gives
you `api.db.listObjects`. Call it and render the list yourself — there is no
list hook or list component.

```ts title="lib/s3-client.ts"
"use client";

import { createS3Client } from "@dimah-s3/react";
import { dbClient } from "@dimah-s3/db/client";

export const s3Client = createS3Client({
  basePath: "/api/s3",
  plugins: [dbClient()],
});
```

```tsx title="components/file-list.tsx"
"use client";

import { useCallback, useEffect, useState } from "react";
import type { DbClientObject } from "@dimah-s3/db/client";
import { s3Client } from "@/lib/s3-client";

export function Files() {
  const api = s3Client.useApi();
  const [objects, setObjects] = useState<DbClientObject[]>([]);
  const [nextCursor, setNextCursor] = useState<string | null>(null);

  const load = useCallback(
    async (cursor?: string) => {
      const page = await api.db.listObjects({
        route: "avatar",
        ...(cursor ? { cursor } : {}),
      });
      setObjects((prev) =>
        cursor ? [...prev, ...page.objects] : page.objects,
      );
      setNextCursor(page.nextCursor ?? null);
    },
    [api],
  );

  useEffect(() => {
    void load();
  }, [load]);

  return (
    <>
      <ul>
        {objects.map((file) => (
          <li key={file.id}>{file.filename ?? file.key}</li>
        ))}
      </ul>
      {nextCursor ? (
        <button type="button" onClick={() => void load(nextCursor)}>
          More
        </button>
      ) : null}
    </>
  );
}
```

<Callout title="Scope is resolved on the server">
  `listObjects` is the browser wrapper for `GET /db/objects`. It does not take a
  scope. The plugin uses `resolveScope` on the request. To list another scope,
  call `listByScope` on the server.
</Callout>

## Quota and extra guards [#quota-and-extra-guards]

Quotas are app-owned numbers. Pass them to `db({ quota })` or
`createQuotaGuard` — the plugin only compares usage.

```ts
plugins: [
  db({
    client: dimahS3Db,
    resolveScope,
    quota: { maxBytes: 100 * 1024 * 1024, maxFiles: 50 },
  }),
],
```

Ownership still runs first. Stack more user guards with `chainHooks`:

<Accordions>
  <Accordion title="getScopeUsage on large scopes">
    `getScopeUsage` scans every row for the scope on each call. That is fine
    for demos and light use. For frequent presign checks or large scopes, keep
    a separate quota table (or counter) updated in your hooks instead of
    recounting each time.
  </Accordion>
</Accordions>

## Custom access [#custom-access]

For rules beyond same-scope ownership, add a guard on the feature config
(it runs after the plugin) or use `createObjectAccessGuard` on your own
routes:

```ts
download: {
  guard: async ({ request, bucket, key }) => {
    // ownership already checked by the db plugin
  },
},
```

```ts
createObjectAccessGuard({ client: dimahS3Db, resolveScope, authorize: myRule });
```

`authorize` replaces the default `object.scope === scope` check. Use it
where you own the full access policy — custom routes, or a guard you wire
yourself.
