docs/long-term-plans/sync-provider-plugins.md
Status: Planned
Enable community developers to build sync providers (Google Drive, OneDrive, S3, etc.) as plugins, using the existing plugin system's runtime loading and sandboxed execution.
persistDataLocal() API (IndexedDB, never synced)FileBasedSyncAdapterService)registerSyncProvider()Added to PluginAPI interface. A plugin calls this during initialization:
plugin.registerSyncProvider({
id: 'google-drive',
label: 'Google Drive',
icon: 'cloud', // material icon name or inline SVG
// Core file operations
getFileRev: async (path, localRev) => {
// Return { rev: string } or throw if not found
},
downloadFile: async (path) => {
// Return { rev: string, dataStr: string }
},
uploadFile: async (path, dataStr, revToMatch, isForceOverwrite) => {
// Return { rev: string }
},
removeFile: async (path) => {},
// State
isReady: async () => true, // true if configured & authenticated
// Optional
listFiles: async (path) => [], // directory listing
isUploadForcePossible: true, // can force-overwrite on conflict
maxConcurrentRequests: 4, // concurrent upload/download limit
});
Only one sync provider per plugin. Calling registerSyncProvider() a second time replaces the first.
persistDataLocal() / loadLocalData()General-purpose local-only storage. Stored in IndexedDB, never synced.
// Store credentials locally
await plugin.persistDataLocal(
JSON.stringify({
accessToken: '...',
refreshToken: '...',
}),
);
// Load on startup
const data = await plugin.loadLocalData();
const creds = data ? JSON.parse(data) : null;
Same constraints as persistDataSynced() (1 MB limit, rate limiting), but data stays on-device.
New class in src/app/plugins/ that wraps plugin callbacks into SyncProviderServiceInterface:
src/app/plugins/plugin-sync-provider-adapter.ts
SyncProviderServiceInterface<SyncProviderId>PluginBridgeServiceprivateCfg uses a no-op credential store (plugin manages its own creds)isReady() delegates to the plugin's isReady() callbackFile: src/app/op-log/sync-providers/provider-manager.service.ts
Currently: static SYNC_PROVIDERS array populated at construction.
Changes:
registerPluginProvider(adapter: PluginSyncProviderAdapter) methodunregisterPluginProvider(providerId: string) methodSYNC_PROVIDERS becomes a mutable list (or better: maintain a separate pluginProviders map)SyncProviderId enum extended with a dynamic/string approach for plugin IDs (e.g., plugin:google-drive)activeProviderId$ and related observables react to plugin provider registrationFile: src/app/features/config/form-cfgs/sync-form.const.ts
Currently: hardcoded provider dropdown options.
Changes:
plugin.openDialog(), a side panel, or plugin.showIndexHtmlAsView())isReady() resultStartup with plugin sync provider selected:
plugin:google-driveSyncProviderManager sees unknown provider ID → isProviderReady$ emits falseregisterSyncProvider(...) → adapter registered with managerisProviderReady$ emits truePlugin disabled/uninstalled:
PluginService calls cleanup → unregisterPluginProvider('plugin:google-drive')SyncProviderManager removes the provider → isProviderReady$ emits falseEncryption:
FileBasedSyncAdapterService| File | Change |
|---|---|
packages/plugin-api/src/types.ts | Add registerSyncProvider() and persistDataLocal()/loadLocalData() to API types |
src/app/plugins/plugin-api.ts | Implement new API methods |
src/app/plugins/plugin-bridge.service.ts | Add bridge methods for sync provider registration and local data persistence |
src/app/plugins/plugin-cleanup.service.ts | Unregister sync provider on plugin disable/unload |
New: src/app/plugins/plugin-sync-provider-adapter.ts | Adapter wrapping plugin callbacks → SyncProviderServiceInterface |
src/app/op-log/sync-providers/provider-manager.service.ts | Add registerPluginProvider() / unregisterPluginProvider(), dynamic provider list |
src/app/op-log/sync-providers/provider.const.ts | Support dynamic plugin provider IDs alongside the enum |
src/app/features/config/form-cfgs/sync-form.const.ts | Dynamic provider dropdown, "Configure" button for plugin providers |
src/app/plugins/store/ | Add reducer/actions for local plugin data persistence |
src/app/plugins/plugin-persistence.model.ts | Add PluginLocalData model |
src/app/plugins/plugin-sync-provider-adapter.ts
Thin adapter that implements SyncProviderServiceInterface by delegating to plugin callbacks. ~50-80 lines.
PluginSyncProviderAdapter correctly delegates all methodsSyncProviderManager handles dynamic registration/unregistrationA minimal Google Drive sync plugin would look like:
// manifest.json
{
"name": "Google Drive Sync",
"id": "google-drive-sync",
"version": "1.0.0",
"manifestVersion": 1,
"minSupVersion": "11.0.0",
"description": "Sync via Google Drive",
"hooks": [],
"permissions": ["syncProvider", "localData"]
}
// plugin.js
const GDRIVE_API = 'https://www.googleapis.com/drive/v3';
let credentials = null;
async function init() {
const data = await plugin.loadLocalData();
credentials = data ? JSON.parse(data) : null;
}
plugin.registerSyncProvider({
id: 'google-drive',
label: 'Google Drive',
icon: 'cloud',
maxConcurrentRequests: 4,
isUploadForcePossible: true,
isReady: async () => {
await init();
return !!credentials?.accessToken;
},
downloadFile: async (path) => {
// Use fetch() to call Google Drive API
// Return { rev, dataStr }
},
uploadFile: async (path, dataStr, revToMatch, isForceOverwrite) => {
// Upload to Google Drive
// Return { rev }
},
getFileRev: async (path, localRev) => {
// Check file metadata on Google Drive
// Return { rev }
},
removeFile: async (path) => {
// Delete file from Google Drive
},
});
// Auth UI via menu entry
plugin.registerMenuEntry({
label: 'Configure Google Drive Sync',
icon: 'settings',
onClick: async () => {
// Show auth dialog, store credentials
await plugin.persistDataLocal(JSON.stringify(credentials));
},
});