Something went wrong. Try again.
experiments in a post-browser web
Something went wrong. Try again.
Peek Extensibility Model #
Balance minimal install/development/distribution barriers with web-level safety at runtime.
Current Implementation #
Extension Structure #
Extensions live in ./features/{name}/ with:
features/
├── groups/
│ ├── manifest.json # Extension metadata
│ ├── background.js # Main logic (ES module)
│ ├── config.js # Configuration
│ ├── home.html # UI pages
│ └── ...
├── peeks/
│ └── ...
└── slides/
└── ...
Manifest Format #
{
"id": "groups",
"shortname": "groups",
"name": "Groups",
"description": "Tag-based grouping of addresses",
"version": "1.0.0",
"background": "background.js",
"builtin": true
}
Required fields:
id- Unique extension identifiershortname- URL path segment (lowercase alphanumeric + hyphens)name- Display namebackground- Background script filename
Optional fields:
description- Extension descriptionversion- Semver version stringbuiltin- If true, has full API access (default: false)
Extension URLs #
Extensions are accessed via the peek://ext/ protocol:
peek://ext/{shortname}/{file}
Examples:
peek://ext/groups/home.htmlpeek://ext/peeks/settings.htmlpeek://ext/slides/background.js
Reserved shortnames (cannot be used):
app,ext,extensions,settings,system
Extension Loader #
Located at app/extensions/loader.js, handles:
- Loading: Fetches manifest, validates, registers shortname, imports background script
- Unloading: Calls uninit(), unregisters shortname, removes from registry
- Reloading: Unload + load cycle
- Conflict detection: Rejects extensions with duplicate shortnames
Built-in extensions are registered in the loader:
export const builtinExtensions = [
{ id: 'groups', path: 'peek://ext/groups', backgroundScript: 'background.js' },
{ id: 'peeks', path: 'peek://ext/peeks', backgroundScript: 'background.js' },
{ id: 'slides', path: 'peek://ext/slides', backgroundScript: 'background.js' }
];
Permissions Model #
Coarse permission levels:
builtin: true→ Full API access including extension managementbuiltin: false→ Restricted fromapi.extensionsmanagement APIs
Permission check: Only peek://app/... addresses can manage extensions.
Extension Management API #
// List running extensions (no permission required)
const result = await api.extensions.list();
// { success: true, data: [{ id, shortname, manifest, ... }] }
// Get extension manifest (no permission required)
const result = await api.extensions.getManifest('groups');
// { success: true, data: { id, shortname, name, ... } }
// Load/unload/reload (permission required - core app only)
await api.extensions.load('groups');
await api.extensions.unload('groups');
await api.extensions.reload('groups');
Background Script Pattern #
// features/myext/background.js
import { openStore } from 'peek://app/utils.js';
import appConfig from './config.js';
const api = window.app;
const { id, defaults, storageKeys } = appConfig;
const store = openStore(id, defaults);
const init = () => {
// Register shortcuts, commands, subscriptions
api.shortcuts.register('Option+x', handleShortcut, { global: true });
api.commands.register({ name: 'my command', execute: handler });
};
const uninit = () => {
// Clean up
api.shortcuts.unregister('Option+x', { global: true });
api.commands.unregister('my command');
};
export default { id, init, uninit };
Design Goals #
- Extensions are a folder with a manifest and web content files
- They're opened under the peek:// protocol, each in a web content process
- Their main window is hidden, like the Peek background content process
- Extensions are run directly from their local folder
- Extensions are managed in the settings app (add/remove, enable/disable)
Note: The implementation shifts logic into the background web app vs node.js space, enabling future non-Electron backends.
Capabilities via Injected API #
- Window management
- Datastore access
- Command registration
- Hotkey registration
- Pubsub messaging
Command API #
Extensions can register commands that appear in the cmd palette.
Registration #
// Register a command
api.commands.register({
name: 'my command', // Searchable name (required)
description: 'Description', // Shown in palette (optional)
execute: async (ctx) => { // Handler function (required)
// ctx contains: typed, name, params, search
console.log('Command executed:', ctx);
}
});
// Unregister when extension unloads
api.commands.unregister('my command');
Context Object #
The execute function receives a context object:
{
typed: 'my command foo bar', // Full typed string
name: 'my command', // Matched command name
params: ['foo', 'bar'], // Parameters after command
search: 'foo bar' // Text after command (for search-style commands)
}
Implementation Details #
- Commands are registered via pubsub with GLOBAL scope (cross-window)
- Execute handlers are stored locally (functions can't cross IPC)
- The cmd background process maintains a registry of registered commands
- The cmd panel queries the registry when opened
- Execution requests are published back to the registering extension
Example: Groups Extension #
const init = () => {
api.commands.register({
name: 'groups',
description: 'Open the groups manager',
execute: async (ctx) => {
api.window.open('peek://ext/groups/home.html', { ... });
}
});
};
const uninit = () => {
api.commands.unregister('groups');
};
Dev workflow:
- User can open/close devtools for a given extension (via a cmd)
- User can reload a given extension (via a cmd)
- Hot reloading using node fs watcher to watch folder for changes, and reload
Open questions, for later:
- Dirty writes - add ext or sys as source. also ensure no direct writes, only api adds
- Sharded space, in/outbox style too maybe
- Trade off exfiltration-proof-ness for sensitive access, eg history?
- How to provide authorship verification? regular website + sigs
- How to provide tamper detection?
- How to do extension-specific settings? Manifest link to bundled settings UI? Or a api for placement into Settings app?
- How to allow for maximal unloading vs always persistent
- How to do remixes, eg take verified extension X, copy and hack
- figure out in-extension settings vs jamming in global settings
- syncable settings (extension decides)
Mobile, for later:
- open web pages
- if calls a registration api, user can choose to add
- permissions
- expose peek api upon approval
- also, preverification via sync