Glidey
Private 31 May 2026 · Kotlin · Go · Jetpack Compose · PostgreSQL
Overview
Glidey is a self-hosted photo and video gallery for Android, designed as a replacement for Google Photos or the stock Samsung Gallery where the underlying storage and infrastructure remain under the user’s control. The system is split into three components: the Android client, a Go backend serving metadata over Postgres and Redis, and a Cloudflare Worker that proxies and caches media stored in Backblaze B2.
Architecture
The client is built with Jetpack Compose throughout, with one exception: the timeline grid uses a traditional grid layout rather than Compose’s own lazy grid, which did not hold up at the scroll speeds a large photo library produced.
The Go backend exposes a metadata API in front of Postgres, with Redis handling caching and rate limiting. Media is stored in Backblaze B2, fronted by a Cloudflare Worker that authorizes against B2’s API, caches the resulting auth token for most of a day, and proxies only read requests under a strict path pattern, forwarding range headers so that video scrubbing continues to work through the CDN layer.
The timeline is loaded through a single batched metadata call per month rather than one request per item, paired with a scroll-state machine that determines which network and decode operations are permitted at a given moment. During fast scrolling, network fetches are disabled entirely, and only already-cached thumbnails are rendered.

Design Decisions
Image loading is split across two separate loader instances, one for the grid and one for the fullscreen viewer, each with its own disk cache budget and decode/fetch concurrency limits. This split was made after profiling showed decode times in the double-digit seconds and lock contention compounding to over ten seconds when a single loader handled both cases. Each loader is responsible for rendering different views to the user, which was why the natural split occurred.
The photo viewer’s transition into and out of fullscreen is built manually rather than through a built-in shared-element animation API. The transition computes scale, translation, and corner radius from the tapped thumbnail’s on-screen position, blends this with a swipe-to-dismiss gesture, and crossfades in a matching thumbnail during the final portion of the exit animation. All of this is to ensure the user-facing experience is as clean and native-like as possible.
As with the transition, the upload pipeline is handled client-side end to end: a thumbnail and preview are generated locally, the original file is streamed directly rather than loaded into memory, and all three assets are uploaded to presigned B2 URLs (generated by the backend) in parallel.
Challenges
The fullscreen transition required several iterations before the swipe gesture, scale calculations, and thumbnail crossfade behaved consistently together. A particular pain point was the difference in aspect ratios between thumbnails (square) and preview images (usually either portrait or landscape rectangles).
Sharing a single memory cache between the grid and viewer loaders required explicit cache-key namespacing, since without it a downsampled grid thumbnail could be returned in place of a full-resolution image in the viewer. Early prototypes had issues with the caching system purging a picture a user was trying to view as the photo gallery grid loaded in the background.
Album reordering required concurrency handling once drag-to-reorder was implemented. The server tracks an atomic version number per user, the client stores this version locally, and a version conflict on save triggers a sync with the server rather than an automatic retry.
Lessons Learned
Cold start cost is easy to underestimate. The gallery’s slow initial load appeared early in development, but the root cause was only identified through profiling rather than code inspection. Removing unused initializers and manually starting only the required components, including pre-warming fetches and caches, reduced launch time measurably.
A recurring lesson was that Compose’s default patterns were a good starting point but required targeted optimization for production. The timeline grid, image pipeline, and caching strategy were each adapted to handle larger datasets and real network latency.
What’s Next
-
HTTP/3 support and a proper interceptor stack on the shared network client
-
Further work on decode contention during fast scrolling
-
Resumable uploads for large videos and unstable networks
-
Incremental state refreshes of collections