Skip to content

About

A film photography community platform for sharing and discovering analog photography.

Resources

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

AvoidXray: a grid of community film photos behind the logo and live counts of photos, films and cameras

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.

The Kodak UltraMax 400 page The Canon AE-1 Program page

What it does

  • 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.

How it's built

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

Running locally

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 dev

Uploads 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

Documentation

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

Working on it

  • 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 test and npm run build before 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. Use publicUserSelect or bylineUserSelect.

License

© 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.

About

A film photography community platform for sharing and discovering analog photography.

Resources

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages