See what a roll of film looks like before you buy one.
AvoidXray.com
AvoidXray is a community archive of film photography. People upload their scans and tag each frame with the camera and film stock it was shot on, and every stock and every camera gets a page built from those frames.
Manufacturer samples are shot by professionals in good light and scanned on equipment most people don't own. They show what a stock can do. The frames here show what it did for someone else, scanned however they scan. Uploads are stored as they arrived, minus GPS, serial numbers, names and editing history.
- Upload scans in bulk and tag each with its camera and film. The original is kept, without GPS, serial numbers or names.
- Browse every film stock, camera and film-and-camera pairing, each a page of community photos with specs, notes and related gear.
- Explore by random, recent, popular (this week, month, year or all time) or the people you follow.
- Search photos by what is in them, in English or Chinese, by caption and by the gear they were shot on, with sort and film and camera filters. Profiles, albums and gear pages search their own photos the same way.
- Share with albums, follows, likes, comments, community notes and notifications.
- Export a photo as a print, filmstrip, negative, slide or instant print, at social sizes or sized for paper, singly or as a batch.
- Moderate from an admin panel with a review queue for catalog changes, reports, bulk editing and a lookup that takes any id or link. Uploads are checked for nudity before they are stored, flagged photos wait for review, and names, comments and other text refuse slurs and swearing.
Next.js 16 (App Router), React 19, TypeScript and Tailwind 4, on PostgreSQL 14 through Prisma 6. Images are processed with sharp and stored in Aliyun OSS. NextAuth handles sessions, Mailtrap sends email and a self-hosted Umami counts visits. About 50 pages, 50 API routes and 23 models, running as two processes, the site and a vision worker, on one 6-core, 8GB server that it shares with other services.
The parts worth reading about:
- Images on a memory budget. Scans run to 50MB and 60 megapixels. Limits are set in decoded pixels, renditions are made one at a time, and exports share a render semaphore sized from the machine. A 62MP print export renders in under a second, after measuring which encoder settings were worth it. docs/images.md
- A catalog that cites its sources. Every edit to a film or camera is a revision, reviewed field by field and applied in one transaction with a record of where each value came from. Research is loaded by scripts that refuse a value the cited passage does not state. docs/catalog.md
- Search by what is in the photo. "cat" or 猫 finds cats, with no outside service: an on-server model embeds every photo, and the same worker holds new uploads private until a nudity check clears them. docs/vision.md
- Feeds that agree with themselves. The first screen and every page after it share one definition, random orders are seeded so Back returns you to the same grid, and prev/next on a photo walks the order of the grid you came from. docs/architecture.md
- Privacy in one place. One module decides who can see a photo, a script calls every API route looking for leaked fields, and the build fails if the database client reaches the browser. docs/security.md
- A schema CI can prove. Every migration is replayed into an empty database on each push, checked against the schema, and the constraints Prisma cannot see are tested for existence and effect. docs/operations.md
Node 20 or later and PostgreSQL.
npm install # also runs prisma generate
cp .env.example .env # DATABASE_URL and NEXTAUTH_SECRET at minimum
npx prisma migrate deploy
npm run devUploads need Aliyun OSS credentials, or LOCAL_UPLOADS=1 to keep them in
public/local-uploads on your machine. Email needs a Mailtrap key. The rest of
the site runs without them.
| Command | |
|---|---|
npm run dev |
Development server |
npm run vision |
The vision worker. Without it, new uploads are stored as pending and search matches captions and gear only |
npm run build |
Production build, then a check that the database client stayed out of the browser |
npm test |
Tests in scripts/test that need no database |
npm run lint |
ESLint |
npx tsc --noEmit |
Type check |
src/app/ Pages and API route handlers
src/components/ Shared UI; ui/ is the design system
src/lib/ Domain logic: feeds, visibility, revisions, images, exports
src/proxy.ts Runs before rendering: real 404s, redirects, private records
prisma/ Schema, migrations and SQL constraint tests
scripts/ Tests, catalog loaders, backfills and maintenance
docs/ How it works
| Architecture | Request flow, data model, feeds, the grid, the UI system |
| Images | Upload, processing, storage, the image cache, exports |
| Catalog | Film and camera records, revisions, provenance, research loaders, slugs, SEO |
| Vision | The vision worker, screening, content search, More like this, the word filter |
| Security | Accounts, sessions, visibility, blocking, rate limits, leak checks |
| Operations | Process model, deploys, CI, tests, scheduled jobs |
| Database objects | Constraints, triggers and indexes that live outside the Prisma schema |
| Branding | Name, logo, color, type and writing |
- One change per commit, as Conventional Commits
with no scope.
style:means code formatting, not CSS. - Before building a component, look for the one that already does the job. Fix a problem everywhere it occurs, not on the one page where it was noticed.
- Catalog facts need a source before they go in. A blank field is better than a guess, because a wrong value looks authoritative and never gets corrected.
- Run
npx tsc --noEmit,npm run lint,npm testandnpm run buildbefore pushing. - A new top-level route goes in
src/lib/topLevelRoutes.ts, or the proxy reads it as a username. - Never
include: { user: true }; it returns the password hash. UsepublicUserSelectorbylineUserSelect.
© Zigao Wang. All rights reserved.
The source is public for reference only. It is not open source: no permission is given to use, copy, modify or distribute the code, or the AvoidXray name and branding.