A full-stack web application for photographers to manage events, coordinate with team members, curate photos, and securely deliver them to clients through PIN-protected galleries.
Built for simplicity, scalability, and security.
- Event Management — Create, organize, and track photography events with custom metadata
- Team Collaboration — Assign team members to specific events; view only assigned work
- Bulk Photo Upload — Direct-to-cloud uploads via Backblaze B2 with progress tracking
- Photo Curation — Admins select and approve photos for client delivery
- Secure Client Galleries — Custom URLs with 6-digit PIN protection and 1-hour signed download links
- Role-Based Access Control — Strict separation between admin (full access) and team members (event-scoped)
- Presigned URLs — Photos never pass through the server; B2 handles all binary transfers
- Rate Limiting — Throttle auth attempts and PIN guessing to prevent abuse
- JWT Authentication — Stateless, 7-day token expiry,
localStoragepersistence
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite, Tailwind CSS v4 |
| State | Zustand (auth), TanStack Query (server) |
| Routing | React Router v7 |
| Backend | Node.js, Express.js |
| Database | MongoDB + Mongoose |
| Storage | Backblaze B2 (S3-compatible) |
| Testing | Jest + Supertest (backend), Vitest + React Testing Library (frontend) |
| Security | bcrypt, JWT, helmet, CORS, express-validator, rate-limit |
- Node.js v18+ (check with
node --version) - MongoDB — local instance or MongoDB Atlas (free tier available)
- Backblaze B2 account (sign up) with a bucket and application key
git clone https://github.com/sukumar5705/pixara.git
cd pixara# Backend
cd backend
npm install
# Frontend (in a new terminal or after finishing backend install)
cd ../frontend
npm installCreate a .env file in the backend directory:
# Server
NODE_ENV=development
PORT=5000
# MongoDB
MONGO_URI=mongodb://127.0.0.1:27017/pixara
# JWT
JWT_SECRET=your_super_secret_jwt_key_here_change_this
JWT_EXPIRE=7d
# Backblaze B2
B2_KEY_ID=your_b2_application_key_id
B2_APPLICATION_KEY=your_b2_application_key
B2_BUCKET_NAME=your_b2_bucket_name
B2_ENDPOINT=s3.us-west-004.backblazeb2.com
B2_REGION=us-west-004
# Frontend URL (for gallery links)
FRONTEND_URL=http://localhost:5173How to get B2 credentials:
- Log into Backblaze B2
- Go to App Keys → Create Application Key
- Choose permissions:
readFiles,listFiles,writeFiles - Copy the Application Key (shown only once)
- Create a bucket and note its name
Create a .env file in the frontend directory:
VITE_API_URL=http://localhost:5000/apiStart the backend and frontend in separate terminals:
Terminal 1 — Backend
cd backend
npm run devExpected output:
Server running in development mode on port 5000
MongoDB Connected: 127.0.0.1
Terminal 2 — Frontend
cd frontend
npm run devExpected output:
VITE v7.3.6 ready in 123 ms
➜ Local: http://localhost:5173/
➜ Press h + enter to show help
- Frontend: http://localhost:5173
- Backend API: http://localhost:5000/api
A default admin account is seeded. Use the seed script:
cd backend
node scripts/seedAdmin.jsLogin credentials will appear in the console. From there, create additional team members via the Team page.
pixara/
├── backend/
│ ├── config/ — Database & B2 setup
│ ├── middleware/ — Auth, validation, error handling
│ ├── routes/ — API endpoints (auth, events, photos, galleries)
│ ├── controllers/ — Business logic per domain
│ ├── models/ — Mongoose schemas (User, Event, Photo, Gallery)
│ ├── services/ — B2 integration
│ ├── utils/ — JWT, PIN, slug generators
│ ├── tests/ — Jest test suite
│ ├── app.js — Express app factory
│ └── server.js — Entry point (calls app.listen)
│
├── frontend/
│ ├── src/
│ │ ├── api/ — Axios instance + domain modules
│ │ ├── hooks/ — TanStack Query hooks
│ │ ├── store/ — Zustand auth store
│ │ ├── components/
│ │ │ ├── ui/ — Reusable Button, Input, etc.
│ │ │ ├── layout/ — AppLayout, Sidebar, guards
│ │ │ ├── events/ — Event cards, modals, dialogs
│ │ │ ├── photos/ — Upload, grid, lightbox
│ │ │ └── gallery/ — PIN gate, public view
│ │ ├── pages/ — Page-level components
│ │ ├── types/ — TypeScript contracts
│ │ └── App.tsx — Router setup
│ ├── public/ — Static assets
│ ├── vite.config.ts — Vite configuration
│ └── tsconfig.json — TypeScript config
│
├── ARCHITECTURE.md — System design & API reference
├── TESTING.md — Test suite documentation
├── PROJECT_FILES.md — Detailed file manifest
└── README.md — This file
Pixara uses role-based access control (RBAC) with two roles:
| Role | Access | Capabilities |
|---|---|---|
| admin | Global | Create/edit/delete events, manage team, publish galleries, select photos |
| team | Event-scoped | Upload photos, view assigned events, see own photos only |
Key flows:
- Public Signup — Creates a
teamuser (non-admin) - Admin Invitation — Admins use
/api/auth/create-userto elevate accounts - Event Assignment — Admins assign team members to specific events
- Scope Enforcement — Every request is checked against the user's role and event assignment
Pixara uses a server-side proxy for uploads to avoid CORS complexity:
Browser → POST /api/photos/upload → Node.js Express → Backblaze B2
(raw binary)
Why? Backblaze B2 presigned PUTs require bucket-level CORS configuration. Proxying through your server sidesteps this entirely and gives you a central audit point.
Details:
- Requests to
/api/photos/uploadinclude raw binary + metadata headers - Express buffers the file in memory (100 MB max)
- B2 service generates a unique storage key and uploads
- Photo metadata is saved to MongoDB, linking the
storageKeyto the event - Download URLs are signed on-demand (1-hour expiry) when returning photos to the client
Admins curate selected photos and publish them to a client-facing URL:
Admin selects photos → POST /api/galleries/publish
→ Generates 6-digit PIN (bcrypt-hashed)
→ Returns shareable URL + plain PIN (once only)
→ Client visits URL, enters PIN
→ Receives selected photos with 1-hour signed download URLs
Security:
- PIN is bcrypt-hashed; never stored or returned after publish
- Rate limited to 10 guesses per IP per 15 minutes
- Published galleries are deactivatable at any time
- Only photos marked
isSelected: trueare visible to clients
Requires a MongoDB instance (uses a test database):
cd backend
npm test # Run all tests with coverage
npm run test:watch # Re-run on file changes
npm run test:coverage # Generate coverage reportTest suites:
auth.test.js— Login, registration, role checksevents.test.js— Event CRUD, team assignment, access controlphotos.test.js— Upload, metadata, selection, deletiongalleries.test.js— PIN verification, publish, deactivation
cd frontend
npm run test:run # Run once
npm run test # Watch mode
npm run test:ui # Open test UI dashboardTest suites:
auth.test.tsx— Login form, auth flowgallery.test.tsx— PIN gate, public gallery view
| Document | Purpose |
|---|---|
| ARCHITECTURE.md | System design, API reference, data model, known gaps |
| TESTING.md | Test philosophy, running tests, coverage targets |
| PROJECT_FILES.md | File-by-file manifest with descriptions |
Before deploying to production, address these:
- Cascade delete — Deleting an event orphans photos and galleries. Needs a
pre("deleteOne")hook. - Slug stability — Republishing a gallery rotates the slug, breaking old links. Keep slug, rotate PIN only.
- Presigned URL overload — No pagination; large events sign a URL for every photo on every load.
- CORS allowlist — Currently allows all origins. Should whitelist
FRONTEND_URLonly. - Index coverage —
Photolacks an index onevent(the hot query path).
See ARCHITECTURE.md § 10 for full details.
- Ensure MongoDB is running:
mongod --version(local) or verify Atlas connection string - Check
MONGO_URIin.envmatches your setup
- Verify bucket exists and name matches
B2_BUCKET_NAME - Confirm app key has
readFiles,writeFiles,listFilespermissions - Check endpoint matches your B2 region (e.g.,
s3.us-west-004.backblazeb2.com)
- Backend must be running on
http://localhost:5000(or whateverVITE_API_URLspecifies) - Frontend must load from
FRONTEND_URLin backend.env
- Ensure JWT is present in
Authorization: Bearer <token>header - Tokens expire after 7 days by default; re-login to refresh
- Express — HTTP server framework
- Mongoose — MongoDB object modeling
- JWT — Stateless authentication
- bcryptjs — Password and PIN hashing
- @aws-sdk/client-s3 — Backblaze B2 (S3-compatible) integration
- express-validator — Input validation
- helmet — Security headers
- express-rate-limit — Request throttling
- React — UI framework
- Zustand — Lightweight state management
- @tanstack/react-query — Server state + caching
- React Router — Client-side routing
- axios — HTTP client
- Tailwind CSS — Utility-first styling
- Lucide React — Icon library
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature - Commit changes:
git commit -m "Add your feature" - Push:
git push origin feature/your-feature - Open a Pull Request
Please ensure tests pass before submitting:
cd backend && npm test
cd ../frontend && npm run test:runThis project is provided as-is. Check the repository for license details.
For issues, questions, or feature requests, please open an issue on GitHub.
Built with ❤️ for photographers. Made to be simple, powerful, and secure.