Uploads Without a Filesystem: Storing Files in PostgreSQL


The classic problem when deploying to serverless: there is no writable filesystem. On Vercel, the public directory is read-only at runtime — so the common "save to public/uploads" strategy fails in production.
My solution: store files as BYTEA in PostgreSQL. The uploads table has just three columns: name, mime, and data. The /api/upload endpoint accepts multipart/form-data, validates the file type and magic bytes, then INSERTs into the database. Files are served back through /api/files/[name], which reads the BYTEA and returns it with the correct Content-Type.
-- the uploads table: this simple CREATE TABLE uploads ( name TEXT PRIMARY KEY, mime TEXT NOT NULL, data BYTEA NOT NULL );
Layered validation: first the allowed MIME types (JPG, PNG, GIF, WebP), then the 5MB limit, then magic bytes — because file.type from the browser can be faked. JPEG must start with FF D8 FF, PNG with 89 50 4E 47. Only files passing every check get stored.
The downside is obvious: the database grows and every image request becomes a DB query. At the scale of a personal portfolio blog, the tradeoff is worth it — zero external storage dependencies, backups ride along with the database, and no orphaned files when data is deleted.
