Developer Guide
Architecture, internals, and everything you need to build on or contribute to My SPACE — the Chrome MV3 extension with three execution contexts, PGlite, AES-GCM crypto, and Google Drive sync.
Architecture
OverviewMy SPACE is a Chrome MV3 extension with three execution contexts:
┌─────────────────────────────────────────────────────────┐
│ Side Panel (React UI) │
│ src/sidepanel/ │
│ Renders views, sends messages to service worker │
└────────────────────────┬────────────────────────────────┘
│ chrome.runtime.sendMessage
┌────────────────────────▼────────────────────────────────┐
│ Service Worker (message router + OAuth) │
│ src/service-worker/index.ts │
│ Routes DB messages to offscreen, handles sync/OAuth │
└────────────────────────┬────────────────────────────────┘
│ chrome.runtime.sendMessage
┌────────────────────────▼────────────────────────────────┐
│ Offscreen Document (database host) │
│ src/offscreen/ │
│ Runs PGlite (PostgreSQL WASM), handles crypto │
└─────────────────────────────────────────────────────────┘
The offscreen document is needed because Chrome service workers cannot run WebAssembly (PGlite). The service worker creates the offscreen document on demand and proxies database messages to it.
Project Structure
Source treesrc/ ├── service-worker/ │ └── index.ts # Message router, OAuth flows, Drive sync ├── offscreen/ │ ├── main.ts # Offscreen entry — wires chrome.runtime.onMessage │ ├── handler.ts # Dispatches messages to db.ts / crypto.ts │ ├── db.ts # PGlite schema + CRUD for all 8 tables │ ├── crypto.ts # AES-GCM encrypt/decrypt via Web Crypto API │ └── polyfill.ts # WASM/PGlite environment polyfills ├── sidepanel/ │ ├── App.tsx # Root — icon rail, view routing, idle lock, setup/lock screens │ ├── views/ │ │ ├── NotesView.tsx │ │ ├── KeyvaultView.tsx │ │ ├── GeneratorView.tsx │ │ ├── SubscriptionsView.tsx │ │ ├── ReportsView.tsx │ │ ├── TodoView.tsx │ │ ├── MapPinsView.tsx │ │ ├── SyncView.tsx │ │ └── SettingsView.tsx │ ├── components/ │ │ ├── IconRail.tsx │ │ ├── NoteCard.tsx │ │ ├── SecretCard.tsx │ │ ├── TagInput.tsx │ │ ├── IconPicker.tsx │ │ └── icons/index.tsx │ └── index.css ├── content/ │ ├── mapExtractor.ts │ └── savePrompt.ts ├── lib/ │ ├── renderMarkdown.ts │ ├── generatePassword.ts │ ├── parseImport.ts │ ├── nextBilling.ts │ ├── currency.ts │ └── shareLink.ts └── shared/ └── messages.ts
Database
PGlite · WASMPGlite runs inside the offscreen document. Schema is initialised in src/offscreen/db.ts → initDb().
| Table | Purpose |
|---|---|
| notes | Markdown notes with tags and embedded images (base64 data URLs) |
| secrets | AES-GCM encrypted credentials (ciphertext + IV stored, value never plaintext) plus url and description columns |
| subscriptions | Recurring billing tracker with currency, cycle, active toggle, logo |
| bills | Actual paid bills per subscription per month (composite PK: sub_id, year, month) |
| todo_lists | Colour-coded, icon-tagged task lists |
| todo_tasks | Tasks with priority, due date, recurrence (FK to todo_lists, CASCADE delete) |
| map_stacks | Colour-coded, icon-tagged pin collections |
| map_pins | Location pins with lat/lng, category, priority, rating, review (FK to map_stacks, CASCADE delete) |
All CRUD functions are in db.ts and called from handler.ts. The vault (secrets table) is only accessible after VAULT_UNLOCK sets the encryption key in memory.
The exportAllRows() function serialises all 8 tables for sync, and importRows() performs upsert-by-updated_at conflict resolution — last-write-wins for notes/secrets/subs/tasks, first-write-wins for stacks/pins.
Content Scripts
Browser integrationMy SPACE ships two content scripts that live in src/content/ and are registered in manifest.json.
mapExtractor.ts — floating Pin button on map pages
Matches: google.com/maps, maps.google.com, openstreetmap.org, bing.com/maps, maps.apple.com.
/@lat,lng, ll=lat,lng, map=zoom/lat/lng, cp=lat~lng, !3dlat!4dlng, lat=…&lng=…).bottom: 220px right: 16px.{ type: 'MAP_PIN_CAPTURE', payload: ExtractedPin } to the service worker.{ type: 'MAP_PIN_FROM_PAGE', payload } to the side panel.MapPinsView listens for MAP_PIN_FROM_PAGE — when received, it sets pendingPin and addMode='page' so the confirm form is pre-filled.A MutationObserver re-scans the URL on SPA navigation.
savePrompt.ts — Save Password badge on login forms
The script wants to run on every login form on the web, which would normally trigger Chrome Web Store's "Broad Host Permissions" review. We avoid that by registering the script dynamically with chrome.scripting.registerContentScripts only after the user opts in.
The bundle path savePrompt.js is generated by Vite with a non-hashed filename and listed in web_accessible_resources. <all_urls> is declared under optional_host_permissions, not host_permissions. The Settings view shows a "Save Password Prompt" card to let the user opt in.
<input type="password"> fields.input events on both fields (debounced 500 ms).{ url, username, password, formAction? } and sends SAVE_PASSWORD_OFFER.SAVE_PASSWORD_OFFER_FROM_PAGE to the side panel.KeyvaultView listens and renders a confirm card.Crypto
AES-GCM · PBKDF2src/offscreen/crypto.ts wraps the Web Crypto API with four capabilities:
The vault key is held in memory in the offscreen document only. It is never written to storage. Locking the vault clears it. Cross-device pull with a mismatched salt uses SYNC_DECRYPT_WITH_SALT to re-derive a temporary key — this derived key is never cached.
Google Drive Sync
OAuth · appDataHandled entirely in src/service-worker/index.ts.
OAuth uses chrome.identity.getAuthToken with three scopes: drive.appdata, userinfo.email, userinfo.profile.
DB_EXPORT → encrypt → upload to Drive appDataFolder (file: keyvault-backup.json)DB_IMPORTSYNC_DECRYPT_WITH_SALT re-derives the key using the backup's salt, decrypts, then imports.Design System
Glass-card dark UIGlass-card dark UI. Key CSS variables in src/sidepanel/index.css. Tailwind v4 is available for layout and spacing. Use the glass-card class for cards.
/* Base */ --bg-base: #0d1117; --glass-bg: rgba(255,255,255,0.06); --glass-border: rgba(255,255,255,0.08); /* Feature accents */ --accent-notes: #6366f1; /* indigo */ --accent-vault: #f59e0b; /* amber */ --accent-sync: #3b82f6; /* blue */ --accent-gen: #a78bfa; /* violet */ --accent-subs: #34d399; /* emerald */ --accent-reports: #f472b6; /* pink */ --accent-todo: #38bdf8; /* sky */ --accent-maps: #fb923c; /* orange */
Android App
Kotlin · ComposeThe android/ directory contains a Kotlin + Jetpack Compose app that mirrors the Chrome extension's feature set.
| Layer | Technology |
|---|---|
| UI | Jetpack Compose + Material3, HorizontalPager navigation |
| Database | Room (SQLite), 8 entities, 6 migrations (v1→v7) |
| Crypto | Android Keystore AES-GCM (hardware-backed) |
| Sync | Google Drive REST API via OkHttp + OAuth implicit flow |
| Images | Coil async image loading |
Key files
- crypto/CryptoManager.kt Keystore-backed AES-GCM encrypt/decrypt
- data/AppDatabase.kt Room database with all entities, DAOs, and migrations
- sync/DriveRepository.kt Drive push/pull with multipart upload
- ui/MySpaceApp.kt Root composable with pager navigation and accent glows
- ui/screens/ 8 screen composables — one per feature
- ui/theme/Theme.kt Dark colour scheme with per-feature accent colours
- util/BillingCalc.kt Monthly equivalent USD conversion and chart data builder
Testing
Vitest · jsdomnpm test
Tests live in tests/. Currently covers:
renderMarkdownparseImportgeneratePasswordnextBillingcrypto
The test environment is Vitest with jsdom.
Adding a New View
Extension guidesrc/sidepanel/views/MyView.tsx — accept { sendMsg } as props.src/sidepanel/components/icons/index.tsx.View type in App.tsx and add it to the icon rail in IconRail.tsx.App.tsx (add to the GATED_VIEWS set if it requires vault unlock).src/shared/messages.ts and handle in handler.ts.Versioning & Release
DeploymentVersion is kept in sync between package.json and manifest.json via scripts/bump.js.
npm run release:patch # bump + build + zip
The zip file is named my-space-<version>.zip and is ready for Chrome Web Store upload.
- Run
npm test— all tests green - Bump version with
npm run release:patch(or:minor/:major) - Verify the zip contains the correct
manifest.jsonversion - Upload to Chrome Web Store Developer Dashboard