What it is
AnyManyJobs is a job marketplace with three user types — job seekers, recruiters, and an administrative panel. It is deliberately not a single-page application: server-rendered templates with Tailwind and a light sprinkling of Alpine, which for a content-heavy marketplace means pages that are fast and indexable without a hydration budget.
The object that deserved the most thought
A job marketplace holds one category of file that is far more sensitive than the rest. Company logos and page images are public by design. A CV is a named individual's employment history, contact details and often their address — uploaded on the understanding that recruiters see it and nobody else does.
The common failure is to put both in the same bucket with the same access model, because it is one line of configuration less. Once that happens, a CV is one guessed or leaked URL away from being public, permanently, with no way to revoke it.
How access actually works
Two storage disks share a single bucket, and application code never names either one directly — it asks for the private disk or the public disk by configuration, so the production and development mappings can differ without any code knowing.
The private disk is deliberately configured with no public URL at all. There is no permanent address for a CV to leak, which means the question stops being "who might find the link" and becomes "who is allowed, right now". Downloads go through an authenticated route that authorises the requester and then issues a signed URL valid for a configured handful of minutes.
Verified against the live bucket, not assumed
Storage permissions are exactly the kind of thing that looks correct in configuration and is wrong in production, so the behaviour was checked against the real bucket rather than trusted:
- A CV written through the private disk lands with a private ACL
- An anonymous request to the direct origin URL returns 403
- An anonymous request via the CDN also returns 403 — the CDN is a separate path to the same object and has to be checked separately
- A freshly signed URL returns 200 and carries the expected signature and expiry parameters
- The same signed URL after expiry returns 403
Two things that are easy to get wrong
Both were found by testing against the live bucket, and both produce confusing failures rather than obvious ones.
The storage endpoint must be the region endpoint, not the bucket URL — the S3 driver composes the bucket into the address itself, so supplying the bucket URL produces a doubled hostname. And the CDN endpoint must not include the disk's root path, because the adapter prefixes that root before concatenating, producing a duplicated path segment and a 403 that looks like a permissions problem when it is really a string-building one.
The local development fallback deserves a warning too. It preserves the private/public split so development works without cloud credentials, but its temporary URLs are not cryptographically signed the way the real ones are. It exists so the app runs on a laptop, not as a deployment option — which is stated plainly in the README, because a fallback that is merely undocumented eventually becomes a production configuration.
This is an OffSetup product, so there is no client to anonymise and the figures above are drawn from the project's own engineering documentation.