A collaborative, mobile-first photo timeline app. Create shared timelines, invite friends, and upload photos that are automatically sorted by date using EXIF metadata.
- Shared Timelines - Create timelines and invite others via link or username search
- Smart Photo Upload - EXIF metadata extraction (date, GPS), auto-thumbnails, reverse geocoding
- Chronological Feed - Infinite-scroll vertical feed grouped by date
- Real-time Updates - New photos and comments appear instantly via Supabase Realtime
- Comments - Comment on any photo with real-time sync
- Metadata Editing - Correct dates, locations, and captions after upload
- Member Management - Creator controls who can access the timeline
- PWA - Installable on mobile with home screen prompt
- Dark Mode - Minimal dark theme with accent colors
| Layer | Technology | Why |
|---|---|---|
| Framework | Next.js 16 (App Router) | Server-first rendering, file-based routing, built-in image optimization |
| UI | React 19 + TypeScript | Concurrent features, type safety |
| Styling | Vanilla CSS (custom properties) | Full control, zero runtime overhead, no class-name collisions via CSS Modules |
| Database | Supabase (Postgres) | Row-Level Security, auto-generated REST API, real-time subscriptions |
| Auth | Supabase Auth | Email/password + OAuth (Google, Apple), session management via @supabase/ssr |
| Storage | Supabase Storage | Direct upload from browser, public CDN URLs, bucket-level access policies |
| Real-time | Supabase Realtime | Postgres Change Data Capture over WebSocket, zero config |
| EXIF | exifr | Lightweight client-side EXIF parser for date, GPS, dimensions |
| Geocoding | OpenStreetMap Nominatim | Free reverse geocoding — GPS coords to human-readable location |
| Keep Alive | cron-job.org | Free cron service to send read-only database requests every eight hours |
User uploads photo
→ exifr extracts EXIF (date, GPS, dimensions)
→ Canvas API generates 600px WebP thumbnail
→ Nominatim reverse-geocodes GPS → location name
→ Supabase Storage receives original + thumbnail
→ Supabase Postgres receives photo record
→ Realtime broadcasts INSERT to all connected clients
→ Other users see the photo appear in their feed instantly
Login/Signup → Supabase Auth (email or OAuth)
→ Auth callback exchanges code for session
→ Proxy refreshes session on every request
→ Auth context provides user/profile to all client components
→ Protected routes redirect unauthenticated users
All tables use Postgres Row-Level Security (RLS). A SECURITY DEFINER helper function (is_timeline_member) breaks recursive policy loops on the timeline_members table. Key rules:
- Photos: Only timeline members can read or insert; only the uploader or timeline creator can delete
- Comments: Scoped through photos → timeline_members join; only the author can delete their own
- Members: Self-referencing SELECT uses the helper function to avoid infinite recursion
The app uses a single globals.css file with CSS custom properties for theming:
- Colors: HSL-based palette with
--bg-*,--text-*,--accent,--danger,--success - Spacing: 4px-based scale (
--space-1through--space-8) - Typography: Inter via
next/font/google(self-hosted, zero CLS) - Components: Buttons (
.btn), inputs (.input), modals (.modal), avatars (.avatar), toasts - Animations:
fadeIn,slideUp,scaleInkeyframes with--transition-basetiming - Layout: Mobile-first with
--max-width: 480pxcontent area, glassmorphism bottom nav
src/
├── app/ # Next.js App Router pages
│ ├── auth/ # Login, signup, OAuth callback
│ ├── profile/ # Profile view, username setup
│ ├── timeline/ # Feed, creation, settings
│ └── invite/ # Join via invite link
├── components/
│ ├── providers/ # Auth context, Toast notifications
│ ├── layout/ # BottomNav, PWA InstallBanner
│ ├── photo/ # PhotoUploader, PhotoLightbox
│ ├── timeline/ # PhotoCard
│ └── comments/ # CommentList (realtime)
├── hooks/ # useInstallPrompt (PWA)
├── lib/
│ ├── supabase/ # Browser + server client singletons
│ ├── exif.ts # EXIF extraction + Canvas thumbnails
│ ├── geocode.ts # Rate-limited Nominatim reverse geocoding
│ └── utils.ts # Date formatting, invite codes, initials
├── proxy.ts # Session refresh + route protection
└── types/ # Shared TypeScript interfaces
- Supabase client singleton:
createClient()caches the browser client at module level and components access it viauseRefto avoid re-render loops inuseEffectdependency arrays - Infinite scroll:
IntersectionObserveron a sentinel div triggers cursor-based pagination (taken_at < cursor) - EXIF thumbnails: Client-side Canvas API generates 600px WebP blobs before upload, avoiding server-side processing
- Timeline activity: A database trigger updates each timeline's last-upload timestamp, while the feed derives it from the latest photo for legacy-data compatibility
- Real-time: Supabase
postgres_changeschannel subscriptions onphotosandcommentstables auto-update the UI - PWA install:
useSyncExternalStorereadsdisplay-mode: standalonemedia query without triggering cascading renders - Keep alive ping: App avoids being disabled on Supabase using a free cron-job.org job to send a bodyless, read-only database request every eight hours to prevent inactivity from pausing the project.