A self-hostable, S3-native photo album manager. Store your photos in your own S3-compatible bucket (Backblaze B2, Wasabi, Cloudflare R2, Hetzner, MinIO, …), keep them encrypted at rest, and share a private album server with family and friends — without handing your library to a third-party cloud.
See CLAUDE.md for the product rationale and TECHNICAL.md for architecture details.
- Auth — username + password (NextAuth, bcrypt), with
ADMINandUSERroles. Admins manage user accounts and can reset passwords or change roles. - Upload + thumbnails — server-side upload generates two thumbnails (small ~300px, medium ~800px) plus a re-encoded original, all via Sharp.
- Encrypted at rest — every derivative is AES-256-GCM encrypted before it reaches the
bucket; the private bucket only ever holds
.encblobs. - Albums — private, public, and password-protected shared albums.
- Per-photo public CDN publishing — publish individual photos to a separate public bucket and get stable, immutable, cacheable CDN URLs to embed on external sites.
- Trash — soft delete with configurable retention.
- De-duplication — identical uploads are detected by content hash.
- Provider-agnostic keys — object keys encode no provider details, so you can switch S3 providers without re-architecting.
- Setup wizard — first-run wizard for storage/CDN configuration.
Next.js 16 (App Router) · Prisma 7 + Postgres · Sharp · AWS SDK v3 · NextAuth · Ant Design · Docker
Requires Bun and a Postgres database.
- Install dependencies:
bun install - Copy env and fill it in:
cp .env.example .envDATABASE_URL— your Postgres connection stringNEXTAUTH_SECRET— generate withopenssl rand -base64 32
- Create the schema:
bun run db:push(orbun run db:migrateto use migration history) - Seed the admin user:
bun run db:seed— createsadmin/changeme(change the password after first login) - Start the dev server:
bun run dev→ http://localhost:3000 - On first visit you'll be redirected to
/setupto enter your S3 and CDN configuration. After that, log in asadmin.
bun run dev— start the dev serverbun run build/bun run start— production build / servebun run lint— ESLintbun run db:generate— regenerate the Prisma clientbun run db:push— push the schema to the database (no migration files)bun run db:migrate— create and apply a dev migrationbun run db:seed— seed the admin user
The app ships as a container image. Postgres can be external or run alongside the app.
- External database — set
DATABASE_URL(andIMAGE,PORT,NEXTAUTH_SECRET,NEXTAUTH_URL) in.env, then:docker compose up -d
- Bundled Postgres — leave
DATABASE_URLblank, setPOSTGRES_*in.env, then bring up both compose files:docker compose -f docker-compose.yml -f docker-compose.postgres.yml up -d
Put a reverse proxy (Caddy for automatic HTTPS) or a Cloudflare Tunnel in front of the app —
see the deployment targets in CLAUDE.md. Runtime configuration lives in a single
Instance row created by the /setup wizard on first run; beyond secrets and the database
URL there is nothing else to set in the environment.