> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Workspace storage server access

> Apply the workspaces bucket RLS rules in service-role routes, and keep the workspace_storage schema off the Data API.

## Why server routes need the same rules

The `workspaces` Storage bucket is protected by path-scoped RLS. The policies
come from migration `20261026100000_least_privilege_workspace_storage_rls.sql`
and use the database helper `workspace_storage.object_access`.

Routes that use the service role bypass RLS. Each of these routes is a second
door to the same objects: one that signs, lists or deletes on a user's behalf.
A check weaker than RLS (for example, membership only) quietly widens access.

## Shared policy helper

`@tuturuuu/storage-core/workspace-storage-access` mirrors the RLS rules on the
server:

* `classifyWorkspaceStoragePath(name)` maps an object name to its rule. Names
  without a UUID workspace folder are rejected, and so are names with empty,
  `.` or `..` segments.
* Percent-encoded aliases must classify the same as their literal form.
  Otherwise they are rejected, so transport encoding cannot select a weaker rule.
* `resolveWorkspaceStorageAccess(name, operation, context)` returns `allowed`,
  `forbidden` or `invalid`. `resolveWorkspaceStorageFolderAccess` authorizes a
  listing as reading a direct child.
* The module is framework-independent: callers inject membership, Drive
  permissions and chat ownership.
* In Web, `createWorkspaceStorageAccessContext` (`apps/web/src/lib/storage/`)
  binds those lookups:

  * request-scoped membership;
  * `has_workspace_permission` for `view_drive` and `manage_drive`;
  * `ai_chats.creator_id`.

  Lookups are memoized per request. A lookup failure answers 500 rather than
  guessing.

| Root | Read | Write |
| - | - | - |
| `chat/<chatId>/**`, `chats/ai/resources/<chatId>/**` | chat creator | none (delete: creator) |
| `chats/ai/resources/temp/<userId>/**` | that user | none (delete: that user) |
| other `chats/**`, `external-apps/**`, `AI Agent Imports/**`, `mira/images/**`, `recordings/**`, `.tuturuuu/**` | service role only | service role only |
| `project-updates/**` | member | member |
| `template-backgrounds/**` | member | `manage_drive` |
| everything else (Drive) | `view_drive` or `manage_drive` | `manage_drive` |

Every rule also requires a `MEMBER` membership. A change to a root must update
four places together:

* the SQL helper;
* this module;
* the pgTAP matrix (`supabase/tests/workspace-storage-least-privilege-rls.sql`);
* the parity test (`packages/storage-core/src/lib/workspace-storage-access.test.ts`).

The parity test reuses the pgTAP objects and expected flags.

### AI chat attachment routes

* `POST /api/ai/chat/signed-read-url` accepts only
  `{wsId}/chats/ai/resources/...` paths. Every path must pass the helper for
  `select`, and one denied path rejects the batch.
* Drive files are rejected here and go through the Drive APIs, which add
  egress controls.
* `POST /api/ai/chat/file-urls` authorizes the chat folder before listing it.
* Public chat pages (`/ai/chats/<chatId>`) do not use either route. They render
  shared messages from `ai_chat_messages` without attachment URLs, so public
  viewing does not depend on member-wide reads.

## Keep `workspace_storage` off the Data API

The `workspace_storage` schema holds the RLS helpers.
`workspace_storage.object_access(name, operation, actor)` trusts its `actor`
argument. Policies supply that argument from
`(select workspace_storage.actor_access())`, which resolves the caller once per
statement.

`authenticated` holds `USAGE` and `EXECUTE` because policy expressions run as
the caller. If the schema were exposed through PostgREST, any signed-in user
could call `object_access` over `/rest/v1/rpc` with a forged actor (another
user's id and workspace sets). They could then probe which objects that user
can reach.

So:

* Never add `workspace_storage` to **Exposed schemas** in the Supabase
  dashboard (Settings, Data API).
* Never add it to `[api].schemas` in `apps/database/supabase/config.toml`.
* Do not move the helpers into an exposed schema (`public`, `private`,
  `storage`, `graphql_public`) either. `private` is exposed in this project.

When reviewing an API settings or `config.toml` change, check that the
exposed-schema list still omits `workspace_storage`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.