Private File Storage in Next.js: A Server Action for Browser Direct Uploads
Short answer: for a B2B SaaS product accepting private user uploads, let a Next.js server action authorize a short-lived upload session, let the browser send bytes directly to private object storage, and issue an expiring download URL only after a separate read authorization check. The deciding constraint is not upload speed. It is the boundary between access control and delivery simplicity. The…
In the realm of B2B SaaS products that handle private user uploads, employing a Next.js server action offers an optimal approach. This method authorizes a short-lived upload session on the server side, enabling the browser to transmit the file bytes directly to private object storage. Crucially, the server generates an expiring download URL only after a separate read authorization check has been completed.
The key takeaway is that the decision-making process regarding access control and delivery simplicity takes precedence over upload speed. The application must delineate who has authorization to create, replace, download, and delete files; storage should solely handle the bytes without taking on the responsibility of managing the user database.
For a successful private upload, the server action must validate the user's identity, file size, and content type against predefined policies. Once validated, the server creates a database entry and requests a presigned write capability from the storage service. The browser then proceeds with the direct upload using this URL. Post-upload, a second server action assesses the object and marks the database row as ready.
This approach of separating the upload process from the business logic proves advantageous for audit purposes. By having a distinct "upload succeeded" flag, the critical path becomes more straightforward to review. Moreover, deterministic naming conventions and the absence of credentials transmission through the browser boundary simplify auditing procedures.
To ensure a seamless upload experience, certain precautions should be taken. The browser should not modify the Content-Type, checksum headers, or HTTP method after the signature has been applied. Ignoring these changes can lead to HTTP 403 errors, which may be mistakenly attributed to authorization issues rather than signature mismatches.
Upon completion of the file transfer, the application should examine the object against its upload policy. If necessary, an idempotent "ready" transition can be performed. It is essential to handle retries using the same upload ID to prevent the generation of duplicate records and to facilitate the cleanup process. The distinction between these states should not be obscured by a generic success response.
When it comes to downloads, the application must first authorize the database row and subsequently create a short-lived read capability. The response disposition and filename should be determined by the server, ensuring a secure delivery process. The Content-Disposition header, while important for user interface purposes, does not grant public access to the private object and should not replace the authorization check already in place.
Regarding audit records, it is important to acknowledge that an interrupted browser upload leaves behind specific traces. Firstly, treating a signed URL as a standalone permission system is a flawed approach. Anyone possessing an unexpired URL can generally utilize the capability within its defined scope. To mitigate this risk, the URL's lifetime should be kept minimal, and it should not be reused as a general upload endpoint.
Secondly, relying solely on the browser's metadata, such as the "image/png" hint, is insufficient for verifying file content. If additional steps like malware scanning or content verification are required, the object must remain in quarantine until the asynchronous check has been completed. The user-facing row should clearly distinguish between uploading, quarantined, ready, and rejected states, as failing to do so may result in accidental downloads.
Lastly, an HTTP 403 response upon an expired write capability indicates a failure in the authorization process, while a valid read capability for an authorized tenant should still grant access. Implementing a test that verifies both the access result and the application state (rather than relying on vendor-specific error strings) is crucial.
Finally, assuming a successful browser request guarantees the successful execution of the business operation is a misconception. Network failures can occur between the time storage accepts the bytes and when the browser reaches the application. A reconciliation worker should compare pending rows with the expected object key, and a retention policy should handle the removal of abandoned objects.
Implementing application-specific deletion paths and timestamps ensures that the promised "disappearance" of files occurs within the specified timeframe.
In certain scenarios, direct browser delivery to private object storage might not be the ideal boundary. This evaluation should focus on control and failure recovery aspects rather than a mere feature-count comparison. Opting for a browser-to-managed upload service boundary may be appropriate when resumability or workflow tooling is necessary, as it offloads certain responsibilities to the external service.
Conversely, implementing a proxy boundary becomes pertinent when the application needs to perform tasks such as malware scanning, content verification, or extensive file transformations before storage. This proxy approach allows the application to exercise greater control over the data lifecycle, identity mapping, and export controls.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.