PixelSync is an image management tool built with Electron, React, Node and PostgreSQL.
It simulates an imaging pipeline where the desktop client connects to a server-sided database and storage, keeps them in sync and lets the user review and export images.
A complete demo of the application is available here: https://github.com/theaxelchoco/pixelsync/releases/tag/Demo
- Architecture overview
- Folder structure
- Core features
- Sync strategy
- Sync conflict strategy
- Data flow
- Running the project
- API reference
- Implementation notes and tradeoffs
- Future improvements
PixelSync is built as a small three tier system.
-
Electron desktop app (app/)
- React plus Vite frontend rendered inside an Electron shell
- Communicates with the API using HTTP
- Provides gallery, single viewer, crop, sync control, and export
-
Node API server (server/api/)
- Express based REST API
- Connects to PostgreSQL using
pg - Handles all reads and writes to the database
- Manages file uploads, cropping, sync logic, and corruption detection
-
PostgreSQL database (Docker container)
- Runs in a separate container managed by
docker compose - Stores image metadata and sync state
- Acts as the server side database
- Runs in a separate container managed by
-
Mounted storage (server/storage/mock/)
- Local folder used as server side image storage
- API stores uploaded files here
- Sync logic compares this folder with database contents
.
├── app/
│ ├── src/
│ │ ├── App.tsx
│ │ └── App.css
│ ├── electron/
│ ├── public/
│ └── package.json
│
├── server/
│ ├── api/
│ │ ├── src/
│ │ ├── .env
│ │ └── package.json
│ ├── db/
│ │ └── init.sql
│ └── storage/
│ └── mock/
│
├── docker-compose.yml
└── README.md
- Pure thumbnail grid with object-fit cover
- Hover zoom animation
- Multi select mode
- File type filters
- Corrupted file indicator badge
- Export selected images
- Auto-fit image to window on load
- Smooth zoom and pan
- Region selection rectangle
- Server side crop that creates a new image record
- Single and batch upload
- Image metadata extraction using sharp
- Corruption detection on decode failures
- Manual sync control
- Server always wins model
- Detects:
- Files missing from storage
- New files in storage
- Heals corrupted records when restored
- Logs sync summary in the activity panel
- Multi-select integration
- Folder chooser dialog via Electron
- Timestamped filenames to prevent conflicts
- Records events such as uploads, sync runs, selections, exports, and crop operations
PixelSync uses a simple, deterministic approach: server is the source of truth.
Rules:
- Database and storage define the correct state
- Desktop app never overwrites storage files
- Sync brings the database in line with storage
- Missing files → database rows marked corrupted
- Restored files → healed
- New files found on disk → inserted into the database
PixelSync implements the Server Always Wins synchronization model.
This means that the server side PostgreSQL database and the mounted storage folder are treated as the authoritative source of truth. Whenever a sync is triggered, the desktop client updates its state to match what the server reports without attempting to override server data.
This approach is predictable and simple to reason about.
PixelSync does not include client-side editing, offline modifications, or two-way merges. Because all meaningful operations flow through the API (upload, crop, export), the server naturally acts as the central coordinator. Using a server-priority model avoids ambiguity and ensures the state remains consistent after every sync.
While effective for a controlled demo environment, this strategy has tradeoffs:
-
Local edits or local-only files are not preserved.
If the client maintained additional metadata or modified images locally, those changes would be discarded during sync. -
No conflict resolution is performed.
In systems where multiple clients modify data independently, "Server Always Wins" can override newer client changes. -
Not suitable for collaborative, multi-device workflows.
A real production system often requires timestamps, version vectors, or application-specific merge logic to prevent silent overwrites.
For this project’s scope and constraints, Server Always Wins is the simplest and safest model while still showcasing a realistic sync mechanism.
- User selects files
- Frontend sends
multipart/form-data - API writes files to storage
- API analyzes the image with sharp
- API inserts metadata into PostgreSQL
- Gallery refreshes automatically
- User triggers sync
- API loads all DB rows
- API scans storage folder
- API:
- Inserts new rows
- Flags missing files as corrupted
- Heals previously corrupted rows
- Returns summary to client
- User draws a region
- Frontend submits normalized coordinates
- API crops with sharp
- New file is saved to storage
- Inserted as a distinct image record
- User selects multiple thumbnails
- Chooses export folder
- Main process copies files to destination
- Log panel shows export summary
- Node 18+
- npm
- Docker Desktop
- Git
Make sure Docker Desktop is running before starting the database container.
From the project root:
docker compose up -dcd server/api
npm install
npm run devAPI default URL:
http://localhost:4000
cd app
npm install
npm run dev
DB + API health check.
Returns list of image metadata.
Streams raw image file.
Single file upload.
Batch upload with summary.
Runs manual sync cycle.
Crops a region of an existing image and creates a new one.
- Server priority model simplifies conflict handling
- No destructive sync actions
- Corrupted files remain visible in gallery
- Sync is intentionally manual to prevent unexpected changes
- Automatic background sync
- Conflict-aware merge strategy
- Image tagging, searching, and sorting
- Pagination for large sets
- The system seperates metadata from the files which already scales very well for larger libraries since we don't need to locally store each image.
- To support 100k images, the gallery would have to use some form of virtualization to only render the visible thumbnails.
- Sync performance can be improved by comparing file ID and timestamps rather than the entire storage