instaGallery
A private, self-hosted archive for organizing Instagram saved posts
- Type
- Self-hosted web app
- Role
- Solo project
- Runs on
- My Mac, reached over Tailscale
- Scale
- 1,000+ posts, ~30 categories
- Timeline
- 3 months, 53 commits
- Built with
- Python, FastAPI, SQLite, instaloader, pytest, JavaScript, Tailwind, DaisyUI, Tailscale
I'd spend hours on Instagram, scrolling and saving posts that I wanted to come back to later: recipes, craft ideas, DIY projects, etc... Unfortunately the "saved" tab on the Instagram app leads to one long list of disorganized posts, so I might as well have been tossing the saved posts into the void.
I wanted a way to organize them into categories, and to be able to search through them quickly. So I built instaGallery, a private, self-hosted web app that copies my saved posts onto my own computer and gives me a fast, searchable gallery for sorting them.
It runs entirely on my Mac with no cloud service. I set up a private network with Tailscale, so I can also access it from my iPad for easy use in the kitchen.
Post images and captions are blurred to respect the original creators. Click a video to pause it.
The gallery at a glance
Getting the actual content
Instagram's official data export only includes links, not the posts themselves. I built an ingest pipeline on instaloader that downloads the real media and captions.
Instagram's bot detection blocks normal scripted logins, so the pipeline signs in with the session from my regular browser instead. New saves are merged into a SQLite database, and a one-click "Refresh" in the app runs the whole pipeline in the background.
Organizing
Posts go into two-level categories, like food → pasta. Categories are assigned from a picker rather than typed, so a typo can't quietly create a near-duplicate category.
"Uncategorized" views (for the whole archive or within a single category) show me which posts still need sorting.
Browsing by category
Jumping to a category from any post
Organizing a post
Bulk tagging
Notes
Each post has a notes section that opens with a recipe template, since the vast majority of my saved posts are recipe-related. Notes are saved to the database and also written to plain Markdown files, so they're still readable without the app.
Recipe notes
Everyday features
- Search across captions and notes
- Pinned posts
- A "made it" marker
- Categorizing many posts at once
Managing categories
Light and dark themes
Challenges
"Load more" showed duplicate posts
The grid shuffles posts in a random but repeatable order on every change in filter. I did this to make sure I was surfacing as many new posts as possible, instead of the same date-time order.
Early on, categorizing a post while scrolling made the "Load more" button bring back posts I'd already scrolled past earlier on the same filter. This was a consequence of my "reshuffle post order at any opportunity." Any change in the number of matching posts (such as changing the category of one post) reshuffled the whole list.
I switched to giving each post a fixed position from a hash of the shuffle seed and its ID, and to loading the next page after the last post shown rather than by page number. Now posts can come and go mid-scroll without anything repeating or being skipped.
Making it work on the iPad
On the iPad, scrolling inside an open post also scrolled the grid behind it, and Safari kept showing old versions of the app after I'd changed it. I fixed the first by locking the page in place while a post is open, and the second by telling the browser to always check for updated files.
What's next
Next I'd like to sync new saves automatically on a schedule instead of with a button, and improve the layout on phones; it's built mainly for my Mac and iPad.
How it's built
The filter logic, the subtlest part of the app, is covered by an automated test suite. The front end is split into small JavaScript modules that all share one source of truth for the current filter, so the sidebar, filter chips and grid can't get out of sync.
The front end is plain JavaScript with Tailwind and DaisyUI, with no build step.