A cross-browser extension that enables users to share snapshots of selected tabs via URL-encoded links.
Stash consists of two parts:
- Browser Extension - Captures tabs and generates share links
- Viewer Site (Astro) - Renders shared tabs in a centered card UI
- Share multiple tabs with a single URL
- URL budget enforcement (8000 character limit)
- Automatic expiry after 24 hours
- Cross-browser support (Chrome, Firefox, Edge)
- Compressed payload using pako
- Clean, modern UI with purple gradient theme
stash/
├── apps/
│ ├── extension/ # WXT browser extension
│ │ ├── entrypoints/
│ │ │ └── background.ts # Service worker (context menu, clipboard)
│ │ ├── lib/
│ │ │ ├── encoder.ts # Payload encoding + budget enforcement
│ │ │ ├── types.ts # Shared TypeScript interfaces
│ │ │ └── constants.ts # BUDGET_CHARS, VIEWER_ORIGIN, etc.
│ │ ├── public/
│ │ │ ├── icon-16.svg
│ │ │ ├── icon-48.svg
│ │ │ └── icon-128.svg
│ │ ├── wxt.config.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ └── viewer/ # Astro static site
│ ├── src/
│ │ ├── pages/
│ │ │ └── s.astro # Viewer page at /s/
│ │ ├── lib/
│ │ │ ├── decoder.ts # Payload decoding
│ │ │ ├── types.ts # Same as extension
│ │ │ └── constants.ts # Same as extension
│ │ └── layouts/
│ │ └── Layout.astro
│ ├── astro.config.mjs
│ ├── package.json
│ └── tsconfig.json
│
├── package.json # Root workspace config
└── README.md
- Node.js 18+
- pnpm
- Install dependencies:
pnpm install- Build the extension:
pnpm run build- Build the viewer:
pnpm run build# Terminal 1: Run extension in development mode
pnpm run dev:ext
# Terminal 2: Run viewer in development mode
pnpm run dev:viewThe extension will be available at chrome://extensions/ (or Firefox equivalent) and the viewer will run at http://localhost:4321.
- Install the extension in Chrome (dev mode)
- Open 5 tabs (e.g., GitHub, Stack Overflow, MDN, etc.)
- Multi-select all tabs (Cmd+Click or Shift+Click)
- Right-click on selected tab
- Click "Share selected tabs…"
- Verify notification: "Link copied! 5 tabs shared"
- Open new tab and paste URL
- Verify viewer loads with the shared tabs
- Update
VIEWER_ORIGINinapps/extension/lib/constants.ts - Update
siteinapps/viewer/astro.config.mjs - Deploy
apps/viewer/dist/to static host (Cloudflare Pages, Vercel, Netlify) - Rebuild both packages with production URL:
pnpm run build(Turborepo builds extension and viewer together) - Create zip:
pnpm --filter stash-extension run zip:chrome(orzip:firefox)
- Build:
pnpm run build - Zip:
pnpm --filter stash-extension run zip:chrome - Upload
apps/extension/.output/stash-extension-{version}-chrome.zipto Chrome Web Store - Provide: Description, icons, screenshots, privacy policy
- Build:
pnpm --filter stash-extension run build:firefox - Zip:
pnpm --filter stash-extension run zip:firefox - Upload
apps/extension/.output/stash-extension-{version}-firefox.zipto Firefox Add-ons - Source code required: Run
./scripts/create-sources-zip.shand upload the generated sources zip - Provide: Description, icons, screenshots, privacy policy
See apps/extension/SOURCES.md for detailed build instructions required by Mozilla.
Edit apps/extension/lib/constants.ts:
export const PAYLOAD_VERSION = 1;
export const EXPIRY_HOURS = 24;
export const BUDGET_CHARS = 8000;
export const MAX_TITLE_CHARS = 30;
export const VIEWER_ORIGIN = "http://localhost:4321"; // Update before production
export const VIEWER_PATH = "/s/";Edit apps/viewer/astro.config.mjs:
export default defineConfig({
site: "http://localhost:4321", // Update before production
output: "static",
build: {
format: "file",
},
});- Normalize titles (30 char max)
- Create payload
{v: 1, e: timestamp, i: [[url, title], ...]} - JSON.stringify (no whitespace)
- TextEncoder → UTF-8 bytes
- pako.deflate → compressed bytes
- btoa + URL-safe replacements → base64url
- Build URL:
${VIEWER_ORIGIN}${VIEWER_PATH}#p=${base64url} - If length > 8000, binary search for max subset
- Extract
#p=...from URL fragment - Convert base64url → base64 (add padding)
- atob → binary string → Uint8Array
- pako.inflate → decompressed bytes
- TextDecoder → UTF-8 string
- JSON.parse → SharePayload
- Validate schema version and structure
- Check expiry (compare timestamp to now)
- Return
{version, expiry, items, isExpired}
This fork is licensed under the GNU Affero General Public License v3.0 (AGPL‑3.0‑only).
See the LICENSE file for the full license text, or refer to the LICENSE file.