Chrome extension (Manifest V3) for saving your own bookmarks, synced with the
app's private folder in your Google Drive (appDataFolder): it doesn't show
up in your regular Drive, and only this app can read or write it.
For now the extension has only three screens, on purpose:
- New tab (
newtab.tsx): the central piece of the extension. A dark dashboard listing your bookmarks. - Popup (
popup.tsx): opens when you click the extension icon. It's just a quick form to save the active tab, with visual confirmation on save — it doesn't list or manage bookmarks. - Settings (
options.tsx): manual sync, sign out, export/import JSON.
(There was a side panel at one point; it was removed to keep the scope small. If needed later, it can be added back with Chrome's Side Panel API.)
- UI: Next.js (Pages Router,
output: 'export') + TypeScript + Tailwind. Eachsrc/pages/*.tsxcompiles to its corresponding static.html. - Background: MV3 service worker in TypeScript (
src/background), bundled separately with esbuild (Next doesn't compile service workers). - Data: Google Drive API v3, a single
bookmarks.jsonfile insideappDataFolder. Local cache inchrome.storage.localwith "last write wins" sync (byupdatedAt) to allow offline use. - Auth:
chrome.identity.getAuthToken(uses the Chrome session, no need to handle redirect URIs manually).
src/
pages/ newtab.tsx, popup.tsx, options.tsx, _app.tsx, _document.tsx
components/ UI (bookmarks dashboard, form, favicon)
lib/ auth.ts, drive.ts, storage.ts, sync.ts, types.ts, useBookmarks.ts
background/ service worker ("Save to TabSpace" context menu)
manifest.template.json manifest template (client_id is injected at build time)
scripts/build.mjs orchestrates next build + esbuild + copies everything to dist/
scripts/generate-extension-key.mjs pins the extension ID for dev
npm install
- Go to Google Cloud Console and create (or reuse) a project.
- APIs & Services -> Library: enable Google Drive API.
- APIs & Services -> OAuth consent screen: set it up in "External" mode (or "Internal" if you use Google Workspace), with your email as a test user. No verification is needed for personal use.
- Pin the extension ID before creating the client, so it doesn't change
every time you reload the extension during development:
Copy the
npm run gen-key"key": "..."it prints intomanifest.template.json(next to"manifest_version"), and save the extension ID it shows you. - Credentials -> Create credentials -> OAuth client ID -> type Chrome Extension. Paste the extension ID from the previous step as the "Item ID".
- Copy the generated Client ID (
....apps.googleusercontent.com).
cp .env.example .env
Then set the Client ID in GOOGLE_OAUTH_CLIENT_ID.
npm run build
This generates dist/ with manifest.json, newtab.html, popup.html,
options.html, background.js, and icons.
In Chrome: chrome://extensions -> enable "Developer mode" -> "Load
unpacked" -> select the dist/ folder.
Since you pinned the key in the manifest (step 2.4), the ID stays the same between reloads, so Google sign-in keeps working every time you rebuild and reload.
To iterate quickly on the popup/options design in a regular browser (without reloading the extension every time):
npm run dev
Open http://localhost:3000/newtab, /popup, or /options. Note: chrome.*
doesn't exist there, so Drive/identity calls will fail in that mode; it's
mainly useful for laying out the UI. To test the real flow (login, save,
sync) you need to build (npm run build) and reload the extension.
- The only scope requested is
drive.appdata: the extension never sees or touches your regular Drive files. - The icons in
public/icons/are a script-generated placeholder; replace them once you have a final design. - Deleted bookmarks are marked with
deleted: trueinstead of being fully removed, so the deletion propagates across devices on the next sync instead of "reviving" the bookmark.