---
name: object-storage setup quirks
description: Durable rules for wiring Replit object-storage into this auth-gated pnpm monorepo app.
---

# Auth-gated uploads: don't use the generic web upload hook
The object-storage skill's `@workspace/object-storage-web` `useUpload`/`ObjectUploader` client does its request-url POST with a **raw `fetch`** that carries no `Authorization` header. This app authenticates with a Bearer JWT via the generated client's `customFetch`. So the generic hook can't talk to an auth-protected upload endpoint.

**Rule:** when the upload endpoint requires auth, skip the generic hook. Use the generated authenticated client (e.g. `useRequestUploadUrl` from `@workspace/api-client-react`) for the request-url step, then do the direct `PUT` to the returned presigned URL with plain `fetch` (the presigned URL needs no app auth). This let the unused `object-storage-web` lib be removed entirely.

# Upload endpoint must be authorized + validated server-side
**Rule:** `POST /storage/uploads/request-url` must run `requireAuth` AND enforce a server-side MIME allowlist (image/jpeg|png|webp|gif) and max size (~5MB) before signing. Client-side `file.type` checks are UX only, not security.
**Why:** an unauthenticated, unbounded request-url endpoint is an anonymous write/DoS/cost-abuse vector flagged in review. Public GET on `/storage/objects/*` is fine for public profile photos.

# objectStorage.ts type cast
`response.json()` is `unknown` under this repo's strict TS; the signed-url helper must cast: `(await response.json()) as { signed_url: string }`. Type annotation only — does not touch the GCS sidecar auth logic the skill says not to modify.

# Serving uploaded photos
api-server mounts all routes under `/api` (`app.use("/api", router)`), and the proxy routes browser-origin `/api/*` to it. Serve an upload's `objectPath` as `/api/storage${objectPath}` (root-relative) — renders directly in `<img src>` and matches the generated client's `/api` baseUrl.
