---
title: "Supabase storage"
description: "Use a private Supabase bucket for user-namespaced attachments and short-lived signed URLs."
---

`createSupabaseStorage()` returns a user-bound `StorageAdapterFactory`.

```ts
import { createSupabaseStorage }
  from '@mordn/chat-widget/server/supabase';
```

```env
NEXT_PUBLIC_SUPABASE_URL="https://your-project.supabase.co"
SUPABASE_SERVICE_ROLE_KEY="..."
```

> **Keep the bucket and key private**
>
> Create a private bucket named `chat-attachments`. Never prefix the service-role
> key with `NEXT_PUBLIC_`; it must not enter the client bundle.

```ts
createChatHandler({
  getUserId: getChatUserId,
  model,
  store,
  storage: createSupabaseStorage(),
});
```


- **`supabaseUrl`** · `string` · default `NEXT_PUBLIC_SUPABASE_URL`

  Project URL.

- **`serviceRoleKey`** · `string` · default `SUPABASE_SERVICE_ROLE_KEY`

  Server-only service key.

- **`bucket`** · `string` · default `chat-attachments`

  Private bucket name.

- **`signedUrlTtlSeconds`** · `number` · default `3600`

  Signed URL lifetime.


Objects use a bound-user namespace and random path segment. The adapter never
calls `getPublicUrl`; history reload creates fresh signed URLs.

Omitting `storage` disables uploads and the upload route returns `503`. The
widget's `features.fileUpload` flag should therefore be enabled only when a
storage adapter is mounted.
