experiments in a post-browser web

Peek Datastore#

The Peek Personal Datastore stores URLs, navigation history, tags, notes, and other user data. It uses SQLite with a unified schema across desktop backends (Electron, Tauri). The browser extension uses IndexedDB with the same logical schema.

Architecture#

All data operations are handled in the main/backend process, with renderer processes accessing via IPC through window.app.datastore.

Renderer Process                    Main/Backend Process
┌─────────────────┐                ┌─────────────────────┐
│  window.app.    │  IPC invoke()  │  IPC Handlers       │
│  datastore.*    │───────────────▶│  ├─ add-item        │
│                 │                │  ├─ query-items     │
│                 │                │  ├─ add-visit       │
│                 │                │  └─ ...             │
└─────────────────┘                │         │           │
                                   │         ▼           │
                                   │  ┌─────────────┐    │
                                   │  │   SQLite    │    │
                                   │  │  Database   │    │
                                   │  └─────────────┘    │
                                   └─────────────────────┘

Why IPC-Based?#

  1. Backend Portability - Same renderer code works with Electron, Tauri, or mobile
  2. Storage Flexibility - Can swap SQLite for other backends without renderer changes
  3. Security - Datastore logic in trusted main process
  4. Sync Readiness - Same API can route to local or remote storage

Database Schema#

Location: {app_data}/{profile}/datastore.sqlite

Core Tables#

items - Unified Item Storage#

Stores all user content types: URLs, text notes, tagsets, images.

Column Type Description
id TEXT Primary key (UUID)
type TEXT url, text, tagset, image
content TEXT The actual content (URL, note text, etc.)
mimeType TEXT MIME type (e.g., text/html)
metadata TEXT JSON for flexible extra data
syncId TEXT Server-assigned ID for sync
syncSource TEXT Origin of sync (server, history, etc.)
syncedAt INTEGER Last sync timestamp (ms)
createdAt INTEGER Creation timestamp (ms)
updatedAt INTEGER Last update timestamp (ms)
deletedAt INTEGER Soft-delete timestamp (0 if active)
starred INTEGER 1 if starred
archived INTEGER 1 if archived
visitCount INTEGER Total visit count
lastVisitAt INTEGER Most recent visit timestamp
frecencyScore INTEGER Calculated frecency for ranking
title TEXT Display title (denormalized for URLs)
domain TEXT Domain (denormalized for URLs)
favicon TEXT Favicon URL (denormalized for URLs)

item_visits - Navigation History#

Tracks page visits with timing, context, and navigation chaining. Local-only (not synced).

Column Type Description
id TEXT Primary key
itemId TEXT FK to items
timestamp INTEGER Unix timestamp (ms)
duration INTEGER Time spent (ms)
source TEXT direct, link, bookmark, reload, etc.
sourceId TEXT ID of referring visit/item
windowType TEXT main, modal, panel
metadata TEXT JSON for extra context
scrollDepth INTEGER How far user scrolled (0-100)
interacted INTEGER 1 if user interacted (clicked, typed)
prevId TEXT Previous visit in chain
nextId TEXT Next visit in chain

Frecency Scoring: Each device calculates frecency from its local visits using time-decay weighting. Recent visits and interactions score higher.

tags - Tag Definitions#

Column Type Description
id TEXT Primary key
name TEXT Tag name (unique)
frecency REAL Usage frequency score

item_tags - Tag Associations#

Column Type Description
item_id TEXT FK to items
tag_id TEXT FK to tags

extension_settings - Extension Config#

Key-value storage for extension and core settings.

Column Type Description
key TEXT Setting key (e.g., sync.serverUrl)
value TEXT JSON-encoded value

API Reference#

Access via window.app.datastore in any peek:// page.

Items#

// Add item
const result = await api.datastore.addItem({
  type: 'url',
  content: 'https://example.com',
  metadata: { title: 'Example' }
});

// Query items
const urls = await api.datastore.queryItems({ type: 'url' });
const recent = await api.datastore.queryItems({ type: 'url', sortBy: 'lastVisit', limit: 20 });
const popular = await api.datastore.queryItems({ type: 'url', sortBy: 'frecency', limit: 20 });
const searched = await api.datastore.queryItems({ type: 'url', search: 'github' });

// Update item
await api.datastore.updateItem(id, { metadata: { title: 'New Title' } });

// Delete item (soft delete)
await api.datastore.deleteItem(id);

Query filters: type, starred, archived, domain, search, limit, includeDeleted Sort options: created, updated, frecency, lastVisit, visitCount

Tags#

// Get or create tag
const tag = await api.datastore.getOrCreateTag('bookmark');

// Tag an item
await api.datastore.tagItem(itemId, tagId);

// Untag
await api.datastore.untagItem(itemId, tagId);

// Get item's tags
const tags = await api.datastore.getItemTags(itemId);

// Get items by tag
const items = await api.datastore.getItemsByTag(tagId);

Visits & Navigation#

// Track a navigation (finds/creates item, records visit, updates frecency)
const result = await api.datastore.trackNavigation('https://example.com', {
  source: 'link',
  title: 'Example Page',
  favicon: 'https://example.com/favicon.ico'
});
// Returns: { visitId, itemId, created: true/false }

// Record visit to existing item
await api.datastore.recordItemVisit(itemId, {
  source: 'direct',
  interacted: 1
});

// Get visits for an item
const visits = await api.datastore.getItemVisits(itemId, { limit: 50 });

// Query by frecency (optimized for omnibox/history)
const topUrls = await api.datastore.queryItemsByFrecency({
  search: 'github',
  limit: 10
});

Visit sources: direct, link, bookmark, reload, form, generated, frame, other

Settings#

// Get setting
const value = await api.datastore.getSetting('sync.serverUrl');

// Set setting
await api.datastore.setSetting('sync.serverUrl', 'https://...');

Stats#

const stats = await api.datastore.getStats();
// Returns: { items: 150, visits: 1200, tags: 25 }

Sync#

The datastore syncs between backends via the server API:

  1. Desktop ↔ Server: Bidirectional sync in backend/electron/sync.ts
  2. Mobile → Server: Push sync from mobile app
  3. Conflict Resolution: Last-write-wins based on updatedAt

See docs/sync.md for sync architecture details.

Files#

  • backend/electron/datastore.ts - Electron SQLite implementation
  • backend/tauri/src-tauri/src/datastore.rs - Tauri SQLite implementation
  • backend/server/db.js - Server SQLite implementation
  • app/datastore/ - Shared schema and helpers