* feat(plugins): keyed persistence API for per-context LWW (Stage A Phase 1+3) Add an optional `key` argument to `persistDataSynced` / `loadSyncedData`, composed at the bridge transport boundary into `pluginId:key` entity ids. Distinct keys now produce distinct ops that LWW-resolve per-entity, enabling document-mode-style plugins to avoid cross-context blob overwrites without changing existing keyless callers. Phase 3: `removePluginUserData(pluginId)` now sweeps the full prefix (legacy entry + every keyed entry), dispatching one delete per match with the rule-6 setTimeout(0) trailer so remote replicas don't keep keyed entries after uninstall. The reducer-only "smart prefix match" shortcut is wrong (one op for the prefix only, remote keyed entries leak) — see docs/plans/2026-05-23-stage-a-keyed-plugin-persistence.md Phase 3. Phase 4 (document-mode plugin-side migration of the legacy single-blob entry) is left as a separate follow-up so the host change can be reviewed in isolation. Issue #7749 * feat(document-mode): migrate to keyed persistence (Stage A Phase 4) Move from one synced blob under the bare plugin id to per-entity keyed entries: - meta — { enabledCtxIds: string[] }, owned by background.ts - doc:${ctxId} — one entry per context, owned by the editor iframe - __meta__ — migration stamp Each entry has its own LWW timestamp on the host, so a concurrent edit in project A on Device 1 and project B on Device 2 no longer whole-blob-collide. The migration runs idempotently from both background.ts and editor.ts (stamp-guarded), splits the legacy single blob into keyed entries, then tombstones the legacy entry with an empty payload — giving LWW a winning side against any offline device that still writes the old shape. flushSave / flushSaveSync no longer need to read+merge sibling state, since each context's entry stands alone. The future-version blob guard (isStorageUnreadable) is dropped — it referenced the wrapping blob's version, which no longer exists; per-doc corruption still falls back via isDocCorrupt. Issue #7749 * fix(plugins): tighten Stage A keyspace at the boundaries Multi-review surfaced three small gaps in the keyed-persistence rollout: - The synchronous composeId throw covers the bridge's iframe and direct-API entry points, but three in-process callers (plugin-config.service, plugin.service, plugin-config-dialog) bypass the bridge and route directly into the persistence service. A user-installed plugin with `id: "evil:plugin"` passed manifest validation and would have collided with the legitimate `evil` plugin's keyed namespace — `removePluginUserData('evil')` would have over-matched the sweep. Reject the colon at install time in `validatePluginManifest`; keep the bridge throw as defense-in-depth. - The new `key` arg at the bridge was typia-asserted on `data` but unchecked itself. A compromised iframe could pass a multi-megabyte string or a non-string value via postMessage. `data` is capped at 1 MB, but the entity id composed from `key` would be stored verbatim in NgRx state, IndexedDB, the op-log, and on the sync wire — bypassing the data cap. Add `assertPluginPersistenceKey` with a 256-char cap. - `_loadPersistedData` silently returned `null` when composeId threw, while `_persistDataSynced` rethrew. The asymmetry made a malformed pluginId look like "no data yet" on the load side, indistinguishable from a fresh install. Hoist composeId + key validation out of the load try/catch so it throws symmetrically. Issue #7749 * fix(plugins): lower per-write cap to 256 KB The pre-Stage-A 1 MB cap was sized for the old single-blob shape, where one entry held every context's data. With the keyed split, each entity gets its own write budget — 1 MB per write is wildly over-provisioned for the realistic upper bound of plugin payloads (heavy document-mode docs ~30–100 KB, configs and automations KB-scale). 256 KB keeps 2–5× headroom over realistic payloads while bounding the per-plugin storage growth more tightly. Document-mode's migration loop now skips oversized legacy docs instead of aborting the whole run: a user whose legacy blob holds one ~500 KB doc (legal under the old cap) keeps the other contexts migrated and the original bytes preserved in the legacy entry. The success stamp stays at migrated:0 in that case so a future build (or pruning of the doc) can complete the migration without data loss. Issue #7749 * test(plugins): e2e migration of legacy single-blob to keyed entries The migration logic in document-mode is unit-tested against a mock PluginAPI, which can't catch real-iframe quirks (postMessage handling of undefined second args, commit-chain timing under the host's per-entity rate limiter, hydration ordering against the op-log). Add two end-to-end scenarios: - Fresh install: enable the plugin, verify the __meta__ stamp lands at migrated:1 (the migration's final write — observing it implies every earlier step completed). - Legacy blob: seed a pre-Stage-A single-blob entry via the e2e helper store, enable the plugin, verify the legacy entry is tombstoned, meta carries the enabledCtxIds, and each doc landed under its own doc:${ctxId} key. Issue #7749 * chore(plugins): drop dead code and review-driven polish Four small follow-ups from the multi-review pass: - Don't log the plugin-supplied `key` value. Plugins may use user content (search queries, doc titles) as keys; the log history is exportable. Log `keyLen` instead, per CLAUDE.md rule 9. - Delete `detectStaleLegacyWrite` and its 3 specs. Exported and fully tested, but zero non-test callers — banner UI is forbidden by project convention for transient-only messaging. If the need resurfaces, the implementation is four lines. - Drop the `attemptedAt` field from `MigrationStamp`. It was written but never read; the success stamp is the only re-entry gate, and the resume path is just "re-run the loop" — re-writes are content- idempotent. Saves one rate-limited write per fresh migration. - Update `docs/plans/2026-05-23-stage-a-keyed-plugin-persistence.md` with an implementation-status table referencing the shipping commits, so future readers don't have to dig through git. Issue #7749 |
||
|---|---|---|
| .. | ||
| ai-productivity-prompts | ||
| api-test-plugin | ||
| automations | ||
| boilerplate-solid-js | ||
| brain-dump | ||
| caldav-calendar-provider | ||
| clickup-issue-provider | ||
| document-mode | ||
| github-issue-provider | ||
| google-calendar-provider | ||
| procrastination-buster | ||
| scripts | ||
| sync-md | ||
| voice-reminder | ||
| yesterday-tasks-plugin | ||
| .gitignore | ||
| package-lock.json | ||
| package.json | ||
| PLUGIN_I18N.md | ||
| QUICK_START.md | ||
| README.md | ||
Super Productivity Plugin Development
This directory contains tools and examples for developing plugins for Super Productivity.
Quick Commands
# Build all plugins
npm run build
# Install dependencies for all plugins
npm run install:all
# Clean build artifacts
npm run clean:dist
# List available plugins
npm run list
Getting Started
Prerequisites
- Node.js 18 or higher
- npm or yarn
- TypeScript knowledge (recommended)
Quick Start
-
Copy the example plugin:
cp -r example-plugin my-plugin cd my-plugin -
Install dependencies:
npm install -
Update plugin metadata:
- Edit
manifest.jsonwith your plugin details - Update
package.jsonwith your plugin name and description
- Edit
-
Start development:
npm run dev -
Build for production:
npm run build
Project Structure
my-plugin/
├── package.json # NPM package configuration
├── tsconfig.json # TypeScript configuration
├── webpack.config.js # Build configuration
├── manifest.json # Plugin manifest (metadata)
├── src/
│ └── index.ts # Main plugin code
├── assets/
│ ├── index.html # Optional UI (for iframe plugins)
│ └── icon.svg # Plugin icon
├── scripts/
│ └── package.js # Script to create plugin.zip
└── dist/ # Build output
├── plugin.js # Compiled plugin code (optional for iframe-only plugins)
├── manifest.json # Copied manifest
└── plugin.zip # Packaged plugin
Development Workflow
1. Local Development
For rapid development within the Super Productivity repo:
# Build and install to local Super Productivity
npm run install-local
# This copies your built plugin to:
# ../../../src/assets/my-plugin/
Then run Super Productivity in development mode to test your plugin.
2. Watch Mode
Keep the plugin building automatically as you make changes:
npm run dev
3. Type Checking
Ensure your code is type-safe:
npm run typecheck
4. Linting
Check code quality:
npm run lint
Plugin API
The plugin receives a global PluginAPI object with these capabilities:
Configuration
cfg- Current app configuration (theme, platform, version)
UI Integration
registerMenuEntry()- Add menu itemsregisterHeaderButton()- Add header buttonsregisterSidePanelButton()- Add side panel buttonsregisterShortcut()- Register keyboard shortcutsshowIndexHtmlAsView()- Display plugin UI
Data Access
getTasks()- Get all tasksgetArchivedTasks()- Get archived tasksgetCurrentContextTasks()- Get current project/tag tasksupdateTask()- Update a taskaddTask()- Create new taskgetAllProjects()- Get all projectsgetAllTags()- Get all tags
User Interaction
showSnack()- Display snack bar notificationsnotify()- Show system notificationsopenDialog()- Open custom dialogs
Data Persistence
persistDataSynced()- Save plugin dataloadSyncedData()- Load saved data
Internationalization (i18n)
translate(key, params?)- Get translated textformatDate(date, format)- Format dates with localegetCurrentLanguage()- Get current language code
See PLUGIN_I18N.md for the complete i18n guide.
Hooks
Register handlers for lifecycle events:
taskComplete- Task marked as donetaskUpdate- Task modifiedtaskDelete- Task removedcurrentTaskChange- Active task changedlanguageChange- App language changedfinishDay- End of day
Example Usage
// Register a task complete handler
PluginAPI.registerHook('taskComplete', async (task) => {
console.log('Task completed:', task);
PluginAPI.showSnack({
msg: `Great job completing: ${task.title}`,
type: 'SUCCESS',
});
});
// Add a keyboard shortcut
PluginAPI.registerShortcut({
id: 'my-action',
label: 'My Plugin Action',
onExec: async () => {
const tasks = await PluginAPI.getTasks();
console.log(`You have ${tasks.length} tasks`);
},
});
// Use translations (if plugin has i18n support)
const greeting = PluginAPI.translate('MESSAGES.GREETING');
const taskCount = PluginAPI.translate('TASK_COUNT', { count: tasks.length });
const dueDate = PluginAPI.formatDate(task.dueDate, 'short');
Building for Distribution
1. Create Plugin Package
npm run build
npm run package
This creates dist/plugin.zip ready for distribution.
2. File Size Limits
- Plugin ZIP: 50MB maximum
- Plugin code (plugin.js): 10MB maximum
- Manifest: 100KB maximum
- index.html: 100KB maximum
3. Required Files
Your plugin ZIP must contain:
manifest.json- Plugin metadataplugin.js- Main plugin code, unless this is an iframe-only plugin withiFrame: trueandindex.html
Optional files:
index.html- UI for iframe pluginsicon.svg- Plugin iconi18n/*.json- Translation files for multi-language support
Publishing Your Plugin
GitHub Release (Recommended)
- Create a GitHub repository for your plugin
- Use GitHub Actions to build releases:
name: Build Plugin
on:
release:
types: [created]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci
- run: npm run build
- run: npm run package
- uses: softprops/action-gh-release@v1
with:
files: dist/plugin.zip
- Users can download the
.zipfile from your releases
NPM Package
You can also publish your plugin source to npm:
- Update
package.jsonwith your npm scope - Build your plugin:
npm run build - Publish:
npm publish
Users would need to build it themselves or you can include the built files.
Testing Your Plugin
1. In Development Mode
# Build your plugin
npm run build
# Copy to Super Productivity assets
npm run install-local
# Run Super Productivity in dev mode
cd ../../.. && npm start
2. In Production Build
- Build your plugin:
npm run package - Open Super Productivity
- Go to Settings → Plugins
- Click "Upload Plugin"
- Select your
plugin.zipfile
3. Debugging
- Open browser DevTools to see console logs
- Check the Console for plugin errors
- Use
console.log()in your plugin code - The plugin runs in the main window context
TypeScript Development
Benefits
- Type Safety: Full IntelliSense and compile-time checking
- API Discovery: Auto-complete for all PluginAPI methods
- Refactoring: Safe code refactoring with TypeScript
- Documentation: Inline documentation in your IDE
Example with Types
import type { TaskData, ProjectData } from '@super-productivity/plugin-api';
// Type-safe task handling
async function processTask(task: TaskData): Promise<void> {
if (task.projectId) {
const projects = await PluginAPI.getAllProjects();
const project = projects.find((p) => p.id === task.projectId);
if (project) {
console.log(`Task "${task.title}" belongs to project "${project.title}"`);
}
}
}
// Type-safe hook registration
PluginAPI.registerHook('taskUpdate', (data: unknown) => {
const task = data as TaskData;
processTask(task);
});
Best Practices
- Error Handling: Always wrap async operations in try-catch
- Performance: Don't block the main thread with heavy computations
- State Management: Use
persistDataSynced()for plugin state - User Experience: Provide clear feedback with snack messages
- Permissions: Only request permissions you actually need
- Version Compatibility: Set appropriate
minSupVersion - Internationalization: Add i18n support to reach more users (see PLUGIN_I18N.md)
Troubleshooting
Plugin not loading
- Check browser console for errors
- Verify manifest.json is valid JSON
- Ensure all required fields are present
- Check file size limits
TypeScript errors
- Run
npm run typecheckto see all errors - Ensure
@super-productivity/plugin-apiis installed - Check tsconfig.json settings
Build issues
- Delete
dist/and rebuild - Check webpack.config.js for errors
- Ensure all dependencies are installed
Examples
Available Examples
- minimal-plugin - The simplest possible plugin (10 lines)
- simple-typescript-plugin - TypeScript with minimal tooling
- example-plugin - Full featured example with webpack
- boilerplate-solid-js - Modern Solid.js boilerplate with i18n support
- procrastination-buster - SolidJS plugin with modern UI
Example Features
boilerplate-solid-js demonstrates:
- SolidJS for reactive UI
- Vite for fast builds
- Internationalization (i18n) support with example translations
- Modern component architecture
- Plugin-to-iframe communication
- Best practices for plugin development
example-plugin demonstrates:
- TypeScript setup with webpack
- All API methods
- iframe UI integration
- State persistence
- Hook handling
- Build configuration
procrastination-buster demonstrates:
- SolidJS for reactive UI
- Vite for fast builds
- Modern component architecture
- Plugin-to-iframe communication
- Real-world use case
Support
- GitHub Issues: Super Productivity Issues
- Plugin API Docs: See
packages/plugin-api/README.md