Temporary, secure photo sharing. Upload photos, create a short link, share it with someone, and it expires on your schedule. No accounts needed for viewers. No cloud photo services snooping on your images. Just a simple, private way to share photos with the people you choose.
Glimpse is a self-hosted photo sharing app designed for one thing: letting you share photos privately and temporarily.
Here's how it works:
- You upload photos to your personal Glimpse server through the admin panel.
- You create a share link by selecting which photos to include and setting an expiry date (tomorrow, next week, whenever you want).
- You send the link to whoever you want. They get a unique, unguessable URL.
- They view the photos by visiting the link. No sign-up, no app download, no account needed.
- The link expires on the date you set. After that, the photos are no longer accessible. You can also revoke a link at any time if you change your mind.
That's it. You stay in control of what's shared, who sees it, and for how long.
Unlike sharing photos through social media, messaging apps, or cloud storage services, Glimpse keeps you in control:
- Your photos stay on your server. They aren't uploaded to Google, Apple, Meta, or any third party. They live on a server that you own and control.
- Links are unguessable. Each share link uses a cryptographically random 22-character code with over 128 bits of entropy — the same level of security used by GitHub and other major platforms. Brute-force guessing is computationally infeasible.
- Links expire automatically. Every share link has an expiry date. Once it passes, the photos can't be accessed anymore. No "shared with link" that lives forever.
- You can revoke access instantly. Changed your mind? Revoke a link and it stops working immediately.
- No viewer accounts. The people you share with don't need to create accounts, hand over their email address, or download an app. Less data floating around means less risk.
- Photos are cleaned up automatically. Photos that aren't part of any active share link are automatically deleted after a configurable number of days (default 30). Cleanup runs on a built-in scheduler — no cron job required. Nothing lingers on the server forgotten.
- Download protection. The gallery viewer prevents casual right-click saving and image dragging. (This won't stop a determined technical user, but it discourages casual copying.)
- Email-gated downloads. When configured with a Resend API key, Glimpse requires recipients to provide their email address before downloading photos. A time-limited download token is sent to their inbox, creating an accountability trail. The admin can see which email addresses downloaded which photos.
- Per-link download control. The admin can enable or disable downloads on each share link individually. When downloads are disabled, the gallery is view-only.
- Invisible watermarking. When photos are downloaded through a share link, Glimpse embeds three layers of invisible watermark: EXIF metadata (survives lossless operations), QIM pixel-domain (survives format conversion), and DCT frequency-domain (survives JPEG re-compression and mild resizing). Each watermark encodes the download ID, so leaked photos can be traced back to the specific download event. The admin panel includes a watermark extraction tool to check any image.
- No tracking by third parties. Glimpse doesn't embed analytics scripts, social media pixels, or advertising trackers. The built-in analytics use IP hashing so even the server admin can't see the real IP addresses of viewers.
- Password-protected admin. Only someone with the admin password can upload photos, create links, or view analytics. The login is rate-limited to prevent brute-force attacks.
The admin panel lets you:
- Upload and manage a pool of photos — see view counts, download counts, and link usage at a glance, and drill into per-photo detail pages showing which links include each photo and download history
- Create share links with selected photos, an expiry date, optional captions, and per-link download permissions. See all active, expired, and revoked links at a glance. Bulk select to revoke or delete multiple links at once.
- View analytics: how many times a link was viewed, from which countries, what devices, and how long people spent looking
- Check watermarks: upload any photo to the watermark checker to extract the embedded download ID and trace it back to the original download event
- Configure settings: manage runtime settings like cleanup retention period from the admin panel
One-click deploy to Railway with a pre-configured app, PostgreSQL database, and persistent volume. Admin password and session secret are auto-generated — just click and go.
Deploy to Vercel with a Neon PostgreSQL database and Vercel Blob for photo storage. Everything runs on Vercel's free tier.
- Fork or import this repo into Vercel
- Create a Neon PostgreSQL database and add
DATABASE_URLto your Vercel environment variables - In your Vercel project, go to Storage → Create → Blob to add a blob store (
BLOB_READ_WRITE_TOKENis auto-configured) - Set these environment variables in your Vercel project settings:
| Variable | Description |
|---|---|
DATABASE_URL |
Neon PostgreSQL connection string |
ADMIN_PASSWORD |
Your admin password |
SESSION_SECRET |
Random string, 32+ characters |
SITE_URL |
Your Vercel app URL (e.g. https://glimpse.vercel.app) |
Note: Vercel has a 4.5 MB request body limit on the free tier, so upload roughly one photo at a time. Photo cleanup on Vercel runs via a daily cron job (
/api/cleanup) instead of the built-in scheduler.
The quickest way to self-host Glimpse on your own machine. Docker Compose handles everything — the app, PostgreSQL, and persistent storage — in a single command. All you need is a machine with Docker installed.
git clone https://github.com/james-langridge/glimpse.git
cd glimpse
# Copy the example env file
cp .env.example .envEdit .env and set these three values:
ADMIN_PASSWORD=your-strong-password
SESSION_SECRET=at-least-32-characters-of-random-text
POSTGRES_PASSWORD=a-strong-database-passwordThen start everything:
docker compose up -dGlimpse will be available at http://localhost:3000. The database and photo storage use Docker named volumes, so your data persists across container restarts.
To update after pulling new changes:
git pull
docker compose up -d --buildOptional settings you can add to .env:
| Variable | Description | Default |
|---|---|---|
SITE_URL |
Public URL for share links | http://localhost:3000 |
CLEANUP_DAYS |
Days before unlinked photos are deleted (0 to disable) | 30 |
RESEND_API_KEY |
Resend API key for email-gated downloads | |
EMAIL_FROM |
Sender address for download emails | onboarding@resend.dev |
DISPLAY_TIMEZONE |
IANA timezone for displayed times | Server timezone |
BLOB_READ_WRITE_TOKEN |
Vercel Blob token (uses Blob storage instead of filesystem) |
If you prefer to run Glimpse directly with Node.js, you'll need to provide your own PostgreSQL instance and manage the process yourself.
Prerequisites: Node.js 18+, PostgreSQL 14+, a persistent filesystem path for photo storage (or set BLOB_READ_WRITE_TOKEN to use Vercel Blob instead).
Create a .env.local file:
ADMIN_PASSWORD=your-strong-password
SESSION_SECRET=at-least-32-characters-of-random-text
DATABASE_URL=postgresql://user:password@localhost:5432/glimpse
PHOTO_STORAGE_PATH=/data/photos
SITE_URL=https://photos.example.comgit clone https://github.com/james-langridge/glimpse.git
cd glimpse
npm install
mkdir -p /data/photos
npm run build
npm startThe database tables are created automatically on first startup. The production server runs on port 3000 by default. Use a reverse proxy (Nginx, Caddy) to add HTTPS.
Glimpse automatically cleans up photos that are older than CLEANUP_DAYS days (default 30) and aren't part of any active share link. Set CLEANUP_DAYS=0 to disable cleanup and store photos indefinitely.
For Docker and Railway deployments, cleanup runs automatically — 60 seconds after server startup, then every 24 hours. No configuration needed. On Vercel, cleanup runs via a daily cron job (/api/cleanup, secured by CRON_SECRET). The last cleanup time is shown on the admin photos page.
If you're running Glimpse behind a reverse proxy (common with both Docker Compose and manual setups), here's an Nginx example:
server {
listen 443 ssl;
server_name photos.example.com;
ssl_certificate /etc/letsencrypt/live/photos.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/photos.example.com/privkey.pem;
client_max_body_size 50M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}The X-Forwarded-For header is important -- Glimpse uses it for rate limiting and analytics geolocation.
Security note: Glimpse trusts the first IP in the
X-Forwarded-Forheader for rate limiting. A reverse proxy that sets this header is required in production -- without one, clients can spoof the header to bypass rate limits. Railway and Vercel set this header automatically. For Docker or manual deployments, always run behind a reverse proxy (Nginx, Caddy, Traefik) that overwritesX-Forwarded-Forwith the real client IP.
Glimpse is a full-stack Next.js application with a PostgreSQL database and filesystem or Vercel Blob photo storage.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript |
| UI | React 19, Tailwind CSS 4 |
| Database | PostgreSQL |
| Auth | iron-session (encrypted cookies) |
| Image Processing | Sharp |
| Analytics | geoip-lite, ua-parser-js, Recharts |
app/ # Next.js App Router (pages + API routes)
admin/ # Admin panel pages (dashboard, photos, links, analytics)
api/ # REST API endpoints
[code]/ # Public share gallery (dynamic route)
src/
components/ # React client components
db/ # Database queries (schema, photos, links, analytics, downloads, settings)
lib/ # Business logic (auth, storage, rate-limiting, cleanup)
Key design decisions:
- Database queries are isolated in
src/db/with asqltagged template for parameterized queries - Side effects (file I/O, auth) live in
src/lib/ - React Server Components handle data fetching; Client Components handle interactivity
- Photos are stored on the filesystem or Vercel Blob (selected by
BLOB_READ_WRITE_TOKEN), metadata in PostgreSQL - The database schema auto-initializes on server startup via
instrumentation.ts, which also schedules automatic photo cleanup
Seven tables:
- photos -- uploaded photo metadata (dimensions, blur placeholder, file size, captions)
- share_links -- share codes, expiry dates, revocation status, download permissions
- share_link_photos -- junction table linking photos to share links with display order and per-link captions
- link_views -- analytics records (hashed IP, geo, device, browser, session duration)
- settings -- runtime configuration (e.g. cleanup retention period)
- photo_downloads -- download analytics with optional email and token linkage
- download_tokens -- email-gated download tokens with expiry and consumption tracking
All foreign keys use ON DELETE CASCADE.
Public (no auth required):
| Method | Path | Purpose |
|---|---|---|
| GET | /api/shared-image/[code]/[filename] |
Serve a photo from an active link |
| GET | /api/download/[code]/[filename] |
Download a watermarked photo from an active link |
| POST | /api/download-request |
Request a download token via email |
| GET | /api/download-token/[token] |
Validate and consume a download token |
| POST | /api/analytics/duration |
Record session duration (beacon) |
| GET | /api/cleanup |
Run photo cleanup (Vercel cron, requires CRON_SECRET) |
Admin (session required):
| Method | Path | Purpose |
|---|---|---|
| POST | /api/auth/login |
Authenticate with admin password |
| POST | /api/auth/logout |
End session |
| GET | /api/photos |
List all photos (with view/link counts) |
| POST | /api/photos/upload |
Upload photos (multipart form data) |
| GET | /api/photos/[id]/detail |
Photo detail (metadata, links, view stats) |
| PATCH | /api/photos/[id] |
Update photo metadata |
| DELETE | /api/photos/[id] |
Delete a photo |
| GET | /api/photos/[id]/analytics |
Photo analytics (views, downloads) |
| GET | /api/photos/[id]/image |
Serve photo image for admin preview |
| POST | /api/photos/bulk-delete |
Bulk delete photos |
| POST | /api/photos/watermark/extract |
Extract watermark from an uploaded image |
| GET/POST | /api/links |
List links / Create a link |
| GET | /api/links/[id] |
Link detail (metadata, photos) |
| PUT | /api/links/[id] |
Update link (expiry, title, photos, downloads) |
| DELETE | /api/links/[id] |
Delete a link |
| PATCH | /api/links/[id]/revoke |
Revoke a link |
| GET | /api/links/[id]/analytics |
Link analytics (views, devices, geo) |
| POST | /api/links/[id]/duplicate |
Duplicate a link |
| PUT | /api/links/[id]/photos/[photoId]/caption |
Set per-link photo caption |
| POST | /api/links/bulk-delete |
Bulk delete links |
| POST | /api/links/bulk-revoke |
Bulk revoke links |
| GET | /api/analytics |
Fetch analytics data |
| GET/PUT | /api/settings |
Read/update admin settings |
- Password auth with constant-time HMAC comparison (timing-safe)
- Rate limiting on login (5/min) and code lookup (10/min) via in-memory sliding window
- Security headers:
X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy,Permissions-Policy - Session encryption via iron-session (httpOnly, sameSite=lax, secure in production)
- IP hashing with SHA-256 and a secret salt for analytics (raw IPs are never stored)
- Parameterized queries throughout (no string concatenation in SQL)
On upload, Sharp extracts image dimensions and generates a 20x20px blur placeholder (base64 JPEG). The blur data is stored in the database and rendered as a CSS background while the full-resolution image loads. Photos are served with 1-hour cache headers.
On download, photos are watermarked with three layers of increasing robustness:
- EXIF metadata —
ImageDescriptiontag with the download ID. Instant to read, stripped by re-saving. - QIM pixel-domain — Quantization Index Modulation on the blue channel of 120 PRNG-selected pixels. Survives lossless operations, destroyed by JPEG re-compression.
- DCT frequency-domain — 2D Discrete Cosine Transform on 120 random 8×8 luminance blocks, modifying a mid-frequency coefficient. Survives JPEG re-compression at quality 70+ and mild resizing.
Extraction tries each layer in order (cheapest first) and returns the first successful match. All watermark logic is in src/lib/watermark.ts with no external dependencies beyond Sharp.
# Start the dev server (with hot reload)
npm run dev
# Lint
npm run lint
# Build for production
npm run build- Database queries go in
src/db/using thesqltagged template fromsrc/lib/db.ts - Business logic (pure functions, utilities) goes in
src/lib/ - React components go in
src/components/with the"use client"directive for interactive components - API routes go in
app/api/following Next.js App Router conventions - Path alias
@/*maps to the project root
- If it needs new tables, add them to
src/db/schema.ts(they auto-create on startup) - Add query functions in a new or existing
src/db/*.tsfile - Add API routes under
app/api/ - Add UI components in
src/components/and pages inapp/
MIT — see LICENSE for details.





