In the website redesign, the product pages and news pages were easy. The trouble was 'file downloads.'
Three Tiers of Files
The client's product materials fall into three classes: public brochures anyone can download; distributor price lists and test reports for signed distributors only; and a batch of internal training materials for employees only. The old approach was a shared cloud drive with links — and once a link got forwarded, it was gone for good.
We didn't build a full permission system. The files table got a level column, values 0, 1, 2 for the three tiers; the users table got a role, written into the session at login. Download requests pass through PHP first, comparing level against role; if insufficient, the reply is 'please contact your account manager to enable access' — not a 403, so it doesn't look like the system is broken.
Real Paths Stay Hidden
The files themselves live outside the web root, under /data/files. After PHP validation, it doesn't read the file and stream it to the browser itself; it returns an X-Accel-Redirect header and lets Nginx serve from an internal path. The browser never sees where the files actually sit, and nobody can bypass validation by guessing filenames.
One small pitfall: the internal location in Nginx must be marked internal. Forget it, and the outside world can hit it directly — the whole exercise wasted. Before launch we tested with curl and confirmed direct requests return 404.
Download Logs
Every download writes one row: user_id, file_id, timestamp, IP. Four fields, no more. Marketing exports monthly, filtered by month, to see which files get downloaded most and which distributor accounts have gone quiet.
The first log version recorded only file_id, not the filename; after exporting we had to join back to the files table, and got complained at. The second version denormalized the filename into the log — faster to read, at the cost of historical rows not matching after a file is renamed. We accepted that trade.
Launch
We cut over on the morning of December 24. That afternoon a distributor called saying the price list wouldn't open; it turned out his account's role hadn't synced over from the old system — one manual row fixed it. Three more such calls followed, all the same batch of accounts; a bulk backfill the next day ended it.
At the client's year-end review, the marketing team's most-exported report wasn't 'download rankings' — it was 'which accounts have never logged in.' They took that list and called everyone on it one by one. From there, it had nothing to do with us.

