Access Files
Understand the shipped public image route and how to add private file authorization.
The kit stores image object keys rather than permanent provider URLs. useStorage converts those keys into a URL handled by the application.
Shipped behavior
The current route is app/storage/[...path]/route.ts:
import { NextResponse } from 'next/server';
import { storageConfig } from '@/config/storage.config';
import { getSignedUrl } from '@/lib/storage';
export const GET = async (
_req: Request,
{ params }: { params: Promise<{ path: string[] }> }
) => {
const { path } = await params;
const [bucket, filePath] = path;
if (!(bucket && filePath)) {
return new Response('Invalid path', { status: 400 });
}
if (bucket === storageConfig.bucketNames.images) {
const signedUrl = await getSignedUrl(filePath, bucket, 60 * 60);
return NextResponse.redirect(signedUrl, {
headers: { 'Cache-Control': 'max-age=3600' }
});
}
return new Response('Not found', { status: 404 });
};This route:
- Is public and does not read a session
- Allows only the configured images bucket
- Generates a signed
GetObjectURL that expires after one hour - Caches the redirect for up to one hour
- Does not query a file record or verify user or organization ownership
Anyone who knows a valid key can request a signed download URL through this route. Keeping the bucket itself private prevents direct anonymous bucket access, but it does not make this application route private.
Using useStorage
import { useStorage } from '@/hooks/use-storage';
export function Image({ imageKey }: { imageKey: string }) {
const src = useStorage(imageKey);
return (
<img
src={src}
alt=""
/>
);
}For a local key, the hook returns:
/storage/{NEXT_PUBLIC_IMAGES_BUCKET_NAME}/{imageKey}If the value starts with http, the hook returns it unchanged. If the value is empty, it returns the optional fallback.
Flat keys only
Although the route uses a catch-all segment, the shipped handler reads only the first two segments:
const [bucket, filePath] = path;The included avatar and logo components therefore use flat keys such as user-id-uuid.png. A nested key such as users/user-id/avatar.png will not be reconstructed by the current route. To support nested keys, change the handler to read [bucket, ...filePath] and join the remaining segments after validation.
Signed URLs are not authorization
A signed URL is a temporary bearer credential. Anyone who receives it can use it until it expires. Signing a URL proves that your server authorized the storage operation, but the shipped public route does not decide whether the requester owns the object.
The included route is suitable for avatars and logos that are expected to be visible. Do not use it for private documents or tenant-confidential exports.
Adding private file access
The following work is a customization. It is not included in either Pro repository.
- Add a file metadata table with the object key, bucket, owner or organization ID, content type, byte size and lifecycle status.
- Create object keys on the server from the authenticated user or active organization. Do not accept an unrestricted owner prefix from the client.
- Replace the public image route for private files with a protected tRPC procedure or route handler.
- Load the file record and verify current organization membership and resource permission before signing a short-lived download URL.
- Use private cache headers or
no-storefor protected redirects. - Add rate limits, access logs and deletion cleanup for your requirements.
Keep public display images and private documents in separate buckets or separate route policies. This makes it harder to expose a private object through the convenience image route.
Listing and deleting
The storage module exports only getSignedUploadUrl and getSignedUrl. It does not export the S3 client and there are no shipped list or delete procedures. Removing an avatar or organization logo clears the database reference but does not delete the object from storage.
Implement listing, deletion and orphan cleanup only after adding file metadata and ownership checks. Examples that refer to a File model, filesTable, verifyFileAccess or storageService.getS3Client() are custom designs rather than repository APIs.