SavedFlow documentation
A browser extension that organizes your saved Instagram posts into collections, with save dates, search, multi-select and drag and drop. Manifest V3, plain JavaScript, no build step.
Overview
Instagram has had a Saved button for years. It has one folder. SavedFlow adds a "Save to..." button next to Instagram's bookmark, keeps everything in local storage, and can import the saves you already have. There is no account, no server, and no analytics.
The repository is at github.com/jacksonfdam/SavedFlow. The extension source lives in the savedflow/ folder.
Install (unpacked)
Chrome, Edge or Brave
- Open
chrome://extensions. - Turn on Developer mode, top right.
- Click Load unpacked and select the
savedflow/folder. - Pin the icon, open Instagram, save something.
Firefox
- Open
about:debugging#/runtime/this-firefox. - Load Temporary Add-on and pick
savedflow/manifest.json.
side_panel key only, because Chrome warns about the Firefox-only sidebar_action. The popup and content script work in Firefox as they are; for the sidebar there, add a sidebar_action block pointing at panel/index.html. Loaded temporarily, Firefox forgets the add-on on restart.Using it
Saving
On any post, reel or the single-post view, a "Save to..." button appears next to Instagram's own bookmark, which is dimmed but left wired up so its state never breaks. Click it to pick a collection, toggle the post in or out of collections, or create a new collection inline.
Importing your existing saves
Open your own Saved pages and SavedFlow reads them from the page. The control at the bottom of the page shows live progress and has Pause, Resume and Cancel. It scrolls to the bottom and harvests links as it goes, because Instagram virtualises the grid and removes items from the DOM once they scroll out of view. The all-posts and audio pseudo-collections are skipped.
/<user>/saved/imports your collection list./<user>/saved/all-posts/imports every saved post./<user>/saved/<name>/<id>/imports one collection's posts, filed under it.
The side panel
Collections in a sidebar, a masonry grid or list, live search, a refresh button, multi-select with bulk move and remove, and drag and drop to file a post or reorder collections. Removing a post can also unsave it on Instagram, after confirmation; this needs a logged-in Instagram tab open.
Architecture
Everything attaches to one global SF object and loads as ordinary scripts in the order the manifest lists them. No bundler, no framework, no dependencies.
| File | Role |
|---|---|
shared/constants.js | Storage keys, the cross-browser API handle, bookmark aria-label strings across locales, message names, the Instagram web app id. |
shared/utils.js | UUIDs, the relative date formatter, shortcode extraction, HTML escaping. |
shared/storage.js | Promise wrapper over chrome.storage.local. Counts derived on read; idempotent import; dedupePosts(). |
content/observer.js | MutationObserver that finds posts and re-scans on SPA navigation. Keys off semantic attributes, never class names. |
content/injector.js | Dims the native bookmark and inserts the custom button. Guards against duplicates on a shared bookmark. |
content/overlay.js | The floating collection picker anchored to the button. |
content/importer.js | Reads the Saved pages, scrolls and harvests, with the pause/resume/progress control. |
content/igapi.js | Converts a shortcode to a media id and calls Instagram's private unsave endpoint from the logged-in tab. |
content/index.js | Wires observer to injector, starts the importer, injects through a small queue. |
content/styles.css | All injected styles. No inline styles at runtime, because Instagram's CSP blocks them. |
background/service-worker.js | Opens the side panel, initialises empty storage on install, relays save events to the panel. |
popup/, panel/ | The toolbar popup and the side panel UI, vanilla JS reusing the shared/ modules. |
Thumbnails get special handling. Instagram's CDN sends a Cross-Origin-Resource-Policy header, so a plain <img> is blocked from this extension's origin. The panel fetches each image through the CDN host permission, turns it into a blob URL, and caches it for the session, six requests at a time.
Data model
Stored in chrome.storage.local under three keys: collections, savedPosts, settings.
collection = { id, name, emoji, color, createdAt, order, igRef? }
post = { id, instagramPostId, postUrl, imageUrl, caption,
username, userAvatarUrl, savedAt, collectionIds[] }
Collection counts are computed from the posts on read, not stored, so they cannot drift. Imports match collections by name and posts by shortcode, so re-running never duplicates.
Permissions
| Permission | Why |
|---|---|
storage | Stores collections and saved-post records locally. |
sidePanel | Shows the organizer as a Chrome side panel. |
https://www.instagram.com/* | Runs the content script: the Save button, import, and unsave. |
https://*.cdninstagram.com/*, https://*.fbcdn.net/* | Lets the panel fetch post thumbnails, which the CDN blocks cross-origin otherwise. |
No tabs, no cookies, no remote code.
Build & publish
Run ./build.sh from the repo root. It reads the version from manifest.json and writes dist/savedflow-<version>.zip with manifest.json at the archive root, which is what the stores want.
A GitHub Actions workflow at .github/workflows/release.yml builds the zip on a version tag (v1.0.0), attaches it to a GitHub Release, and, if credentials are set, publishes to both stores.
Chrome Web Store
Via chrome-webstore-upload-cli. Needs the repository variable CHROME_EXTENSION_ID and the secrets CHROME_CLIENT_ID, CHROME_CLIENT_SECRET, CHROME_REFRESH_TOKEN. There is a one-time 5 dollar developer fee and every submission is reviewed, so "automatic" means "queued for review".
Firefox Add-ons
Via web-ext sign --channel=listed. Needs the secrets AMO_JWT_ISSUER and AMO_JWT_SECRET. The first listed version also needs metadata that AMO asks for.
Limitations
- Instagram's DOM moves. Selectors lean on
aria-label,roleandhref, not class names. If Instagram relabels the bookmark or restructures a view, the button can stop appearing until the selectors are updated. - Import only sees your own Saved pages. It has to scroll the whole grid, which on a large account takes a while; pause and resume exist for that.
- Imported posts carry an approximate date. Instagram does not expose the original save time, so imports are stamped with the import time.
- Image URLs expire. The CDN links are short-lived, so a thumbnail only loads while its URL is valid. The post link always works.
- Unsaving uses an undocumented endpoint. If Instagram changes it, unsaving fails quietly while local removal still works. It acts irreversibly on your real account and needs a logged-in Instagram tab.
- Storage is local. One browser profile, no sync, no backup, no export yet.
- No automated test suite in the repository. Logic was checked with ad-hoc Node and jsdom scripts that were not committed.
Privacy
No analytics, no account, no server. Your data stays in chrome.storage.local on your machine. The only network requests SavedFlow makes go to Instagram and its CDN, on your behalf: reading your saves, fetching thumbnails, and calling the unsave endpoint when you remove a post. Nothing is sent anywhere else.
SavedFlow is not affiliated with, endorsed by, or connected to Instagram or Meta. MIT licensed.