|
| 1 | +# ADR-0001: Browser VFS And Cloud Storage For Scripts And Settings |
| 2 | + |
| 3 | +Status: Implemented |
| 4 | +Date: 2026-04-01 |
| 5 | +Related Features: [Architecture Overview](/Users/ksemenenko/Developer/PrompterLive/docs/Architecture.md) |
| 6 | + |
| 7 | +## Implementation plan (step-by-step) |
| 8 | + |
| 9 | +- [x] Analyze the existing browser-only storage shape for scripts, folders, and settings. |
| 10 | +- [x] Add ManagedCode.Storage packages and register browser storage plus virtual file system in the WASM host. |
| 11 | +- [x] Integrate browser storage plus VFS for cloud/import-export flows while keeping the primary script and folder runtime store on authoritative browser JSON/localStorage for product stability. |
| 12 | +- [x] Add provider preference and credential persistence for Dropbox, OneDrive, Google Drive, Google Cloud Storage, and CloudKit. |
| 13 | +- [x] Add scripts/settings import-export through the configured provider set. |
| 14 | +- [x] Add automated component and browser coverage for the new settings flow and storage contracts. |
| 15 | +- [x] Update architecture documentation and record this ADR. |
| 16 | + |
| 17 | +## Context |
| 18 | + |
| 19 | +- `PrompterLive` is a standalone Blazor WebAssembly app with no backend runtime. |
| 20 | +- Scripts and folders were persisted through ad-hoc browser localStorage JSON blobs. |
| 21 | +- The product now needs browser-local persistence plus optional cloud import/export for scripts and settings. |
| 22 | +- The user explicitly wants Managed Code storage providers, browser localStorage for provider keys and metadata, and no server-side secret store. |
| 23 | +- Future video-stream export is expected, but this task must stop at scripts and settings. |
| 24 | + |
| 25 | +Goals: |
| 26 | + |
| 27 | +- Keep the app browser-only. |
| 28 | +- Add a durable browser-storage boundary for cloud transfers now, while keeping the primary script and folder runtime path stable. |
| 29 | +- Let Settings configure real cloud providers for import/export. |
| 30 | +- Keep provider credentials and connection metadata in browser `localStorage`. |
| 31 | + |
| 32 | +Non-goals: |
| 33 | + |
| 34 | +- Recorded video upload or archive flows. |
| 35 | +- Background sync engines or multi-device conflict resolution. |
| 36 | +- OAuth popup/redirect orchestration hosted by PrompterLive. |
| 37 | + |
| 38 | +## Stakeholders |
| 39 | + |
| 40 | +| Role | What they need to know | Questions this ADR must answer | |
| 41 | +| --- | --- | --- | |
| 42 | +| Product | What users can store now and what is deferred | Can users keep scripts local-first and move them to cloud now? | |
| 43 | +| Engineering | Which boundary owns persistence and cloud transfers | Where do browser VFS, provider credentials, and import/export logic live? | |
| 44 | +| QA | Which flows must stay green | How do we verify local persistence, provider setup, and import/export safely? | |
| 45 | +| Security | Where secrets sit in a backend-free app | Why are provider credentials in browser localStorage, and what risk does that create? | |
| 46 | + |
| 47 | +## Decision |
| 48 | + |
| 49 | +PrompterLive will register `ManagedCode.Storage.Browser` plus `ManagedCode.Storage.VirtualFileSystem` inside the WASM host for browser-local blob storage and provider-backed transfers, while the primary day-to-day script and folder runtime store remains an authoritative browser JSON/localStorage repository. Browser `localStorage` also remains the store for provider credentials, provider metadata, and lightweight settings values. Cloud providers are configured in Settings and currently support snapshot import/export of scripts and settings only. |
| 50 | + |
| 51 | +Key points: |
| 52 | + |
| 53 | +- Primary scripts and folders stay on an authoritative browser JSON/localStorage path so editor/library autosave stays stable under real WASM browser flows. |
| 54 | +- Browser storage plus VFS remain active as the local storage abstraction for cloud snapshot import/export and future stream-export expansion. |
| 55 | +- Provider credentials stay client-side because the runtime has no backend secret store. |
| 56 | +- Cloud import/export is explicit and user-driven, not automatic bidirectional sync. |
| 57 | +- The requested Google Cloud Storage provider is implemented through the actual package `ManagedCode.Storage.Gcp`. |
| 58 | + |
| 59 | +## Diagram |
| 60 | + |
| 61 | +```mermaid |
| 62 | +flowchart LR |
| 63 | + Settings["Settings UI"] |
| 64 | + BrowserStore["BrowserCloudStorageStore"] |
| 65 | + LocalStorage["browser localStorage<br/>credentials + provider metadata"] |
| 66 | + Transfer["CloudStorageTransferService"] |
| 67 | + ProviderFactory["CloudStorageProviderFactory"] |
| 68 | + LocalRuntime["Browser JSON/localStorage<br/>authoritative scripts + folders"] |
| 69 | + LocalVfs["IVirtualFileSystem"] |
| 70 | + BrowserProvider["ManagedCode.Storage.Browser<br/>IndexedDB + OPFS"] |
| 71 | + Scripts["BrowserScriptRepository"] |
| 72 | + Folders["BrowserLibraryFolderRepository"] |
| 73 | + Providers["Dropbox / OneDrive / Google Drive / GCS / CloudKit"] |
| 74 | +
|
| 75 | + Settings --> BrowserStore |
| 76 | + BrowserStore --> LocalStorage |
| 77 | + Settings --> Transfer |
| 78 | + Transfer --> ProviderFactory |
| 79 | + Transfer --> LocalVfs |
| 80 | + Scripts --> LocalRuntime |
| 81 | + Folders --> LocalRuntime |
| 82 | + LocalVfs --> BrowserProvider |
| 83 | + ProviderFactory --> Providers |
| 84 | +``` |
| 85 | + |
| 86 | +## Alternatives considered |
| 87 | + |
| 88 | +### Keep everything in ad-hoc localStorage blobs |
| 89 | + |
| 90 | +- Pros: minimal code churn, no extra package integration. |
| 91 | +- Cons: no file model, no VFS abstraction, brittle migration path, harder future expansion to cloud or stream artifacts. |
| 92 | +- Rejected because the product now needs a durable storage boundary and provider-backed transfers. |
| 93 | + |
| 94 | +### Introduce a backend for secrets and sync |
| 95 | + |
| 96 | +- Pros: stronger secret handling, server-owned OAuth flows, easier centralized sync. |
| 97 | +- Cons: violates the browser-only runtime, adds deployment and operational scope, blocks the current delivery target. |
| 98 | +- Rejected because the app must remain standalone WASM. |
| 99 | + |
| 100 | +### Add live cloud sync instead of snapshot import/export |
| 101 | + |
| 102 | +- Pros: tighter cloud parity across devices. |
| 103 | +- Cons: conflict resolution, offline semantics, provider-specific edge cases, higher failure surface. |
| 104 | +- Rejected because scripts/settings transfer is needed now, while continuous sync is not. |
| 105 | + |
| 106 | +## Consequences |
| 107 | + |
| 108 | +### Positive |
| 109 | + |
| 110 | +- Cloud transfer and future media-export work now have a real browser-storage and VFS abstraction available inside the app boundary. |
| 111 | +- Cloud providers can be connected without introducing backend infrastructure. |
| 112 | +- Import/export logic is isolated from page markup and provider SDK details. |
| 113 | +- The storage boundary is reusable for later video-stream export work without forcing the editor and library to ride a less stable primary persistence path today. |
| 114 | + |
| 115 | +### Negative / risks |
| 116 | + |
| 117 | +- Provider credentials live in browser localStorage and are therefore exposed to the current browser profile. |
| 118 | + Mitigation: keep the app backend-free by design, persist only the provider credentials and metadata required for runtime use, and document that these connections are browser-profile scoped. |
| 119 | +- Cloud import/export is a snapshot flow, so concurrent edits across devices can overwrite expectations. |
| 120 | + Mitigation: keep transfers explicit and user-triggered, not silent background sync. |
| 121 | +- Browser test infrastructure must serve static web assets for storage packages as well as app assets. |
| 122 | + Mitigation: the UI test harness now reads `staticwebassets.development.json` and maps package `_content/*` assets dynamically. |
| 123 | + |
| 124 | +## Impact |
| 125 | + |
| 126 | +### Code |
| 127 | + |
| 128 | +- Affected modules and services: |
| 129 | + - `src/PrompterLive.Shared/AppShell/Services/PrompterLiveServiceCollectionExtensions.cs` |
| 130 | + - `src/PrompterLive.Shared/Library/Services/Storage/*` |
| 131 | + - `src/PrompterLive.Shared/Storage/*` |
| 132 | + - `src/PrompterLive.Shared/Storage/Cloud/*` |
| 133 | + - `src/PrompterLive.Shared/Settings/Components/SettingsCloudSection.*` |
| 134 | + - `tests/PrompterLive.App.Tests/Settings/*` |
| 135 | + - `tests/PrompterLive.App.UITests/Settings/*` |
| 136 | + - `tests/PrompterLive.App.UITests/Infrastructure/*` |
| 137 | +- New boundaries and responsibilities: |
| 138 | + - Browser JSON/localStorage owns primary script and folder runtime persistence. |
| 139 | + - Browser storage plus VFS own the local blob boundary used by cloud import/export and future expansion. |
| 140 | + - `BrowserCloudStorageStore` owns provider preference and credential persistence. |
| 141 | + - `CloudStorageTransferService` owns snapshot import/export. |
| 142 | + - `CloudStorageProviderFactory` owns runtime provider construction and validation. |
| 143 | +- Feature flags and toggles: |
| 144 | + - none |
| 145 | + |
| 146 | +### Data / configuration |
| 147 | + |
| 148 | +- Scripts and folders now materialize into authoritative browser JSON/localStorage payloads with an explicit version marker so seed data and user changes stay stable across reloads. |
| 149 | +- Browser storage plus VFS stay registered for provider-backed transfers instead of becoming the primary editor/library persistence path. |
| 150 | +- Browser localStorage keys remain the source of truth for provider credentials and metadata. |
| 151 | +- No server secrets, environment variables, or backend config were added. |
| 152 | + |
| 153 | +### Documentation |
| 154 | + |
| 155 | +- Architecture docs updated in [docs/Architecture.md](/Users/ksemenenko/Developer/PrompterLive/docs/Architecture.md). |
| 156 | +- This ADR is the source of truth for the storage boundary and credential policy. |
| 157 | +- Root `AGENTS.md` now records the durable rule that standalone cloud credentials live in browser localStorage. |
| 158 | + |
| 159 | +## Verification |
| 160 | + |
| 161 | +### Objectives |
| 162 | + |
| 163 | +- Prove that local scripts and folders persist through the authoritative browser JSON/localStorage path without editor autosave hangs. |
| 164 | +- Prove that cloud provider preferences and credentials persist through browser localStorage. |
| 165 | +- Prove that the Settings cloud flow works in a real browser, including reload behavior. |
| 166 | +- Prove that the UI-test self-hosted server can boot the app with package static assets present. |
| 167 | + |
| 168 | +### Test environment |
| 169 | + |
| 170 | +- Environment: local browser-hosted WASM runtime and self-hosted Playwright acceptance harness. |
| 171 | +- Data and reset strategy: browser-local seed data only; tests rely on isolated runtime contexts. |
| 172 | +- External dependencies: no real cloud credentials required for the regression suite; validation paths cover safe failure messages and state persistence. |
| 173 | + |
| 174 | +### Testing methodology |
| 175 | + |
| 176 | +- Core flows and invariants that must be proven: |
| 177 | + - scripts and folders persist locally through the authoritative browser repository path |
| 178 | + - provider settings survive reload |
| 179 | + - the chosen primary cloud provider opens and remains configurable after reload |
| 180 | + - invalid provider credentials fail safely with a visible validation message |
| 181 | +- Positive flows that must pass: |
| 182 | + - component-level persistence for cloud preferences |
| 183 | + - browser-level configuration and reload of the Dropbox provider path |
| 184 | +- Negative and safe-failure flows that must be covered: |
| 185 | + - missing Dropbox credentials do not connect and instead surface validation text |
| 186 | + - disconnect removes stored credentials and resets the UI subtitle |
| 187 | +- Edge flows that must be covered: |
| 188 | + - runtime seed data and existing browser-local payloads can still materialize into the authoritative repositories without resurrecting deleted items |
| 189 | + - self-hosted browser suite serves package `_content/*` assets from the static web assets manifest |
| 190 | +- Required realism level: |
| 191 | + - real browser flow for acceptance |
| 192 | + - no mocks or service doubles in verification flows added for this change |
| 193 | +- Pass criteria: |
| 194 | + - build, relevant tests, full solution tests, coverage, and format all pass |
| 195 | + |
| 196 | +### Test commands |
| 197 | + |
| 198 | +- build: `dotnet build /Users/ksemenenko/Developer/PrompterLive/PrompterLive.slnx -warnaserror` |
| 199 | +- test: `dotnet test /Users/ksemenenko/Developer/PrompterLive/PrompterLive.slnx` |
| 200 | +- format: `dotnet format /Users/ksemenenko/Developer/PrompterLive/PrompterLive.slnx` |
| 201 | +- coverage: `dotnet test /Users/ksemenenko/Developer/PrompterLive/PrompterLive.slnx --collect:"XPlat Code Coverage"` |
| 202 | + |
| 203 | +### New or changed tests |
| 204 | + |
| 205 | +| ID | Scenario | Level | Expected result | Notes | |
| 206 | +| --- | --- | --- | --- | --- | |
| 207 | +| TST-001 | Save Dropbox draft settings and reload the Settings page | UI | Provider selection, label, and validation message persist | `SettingsCloudStorageFlowTests` | |
| 208 | +| TST-002 | Disconnect Dropbox and clear persisted credentials | Component | Credentials are removed and subtitle resets to disconnected | `SettingsInteractionTests` | |
| 209 | +| TST-003 | Load the stored primary provider and auto-open its card | Component | Selected provider card renders open after state load | `SettingsInteractionTests` | |
| 210 | + |
| 211 | +### Regression and analysis |
| 212 | + |
| 213 | +- Regression suites to run: |
| 214 | + - `tests/PrompterLive.Core.Tests` |
| 215 | + - `tests/PrompterLive.App.Tests` |
| 216 | + - `tests/PrompterLive.App.UITests` |
| 217 | + - full solution tests |
| 218 | +- Static analysis: |
| 219 | + - repo build under `-warnaserror` |
| 220 | + - `dotnet format` |
| 221 | +- Coverage comparison: |
| 222 | + - solution coverage must stay at least at the previous baseline or improve |
| 223 | + |
| 224 | +## Rollout and migration |
| 225 | + |
| 226 | +- Migration steps: |
| 227 | + - ship the browser-storage and VFS wiring with the updated Settings UI |
| 228 | + - allow repository services to materialize runtime seeds and prior browser payloads into authoritative local browser JSON on first mutation |
| 229 | +- Backwards compatibility: |
| 230 | + - existing local library payloads are read and migrated instead of being dropped |
| 231 | + - settings still use browser-local persistence |
| 232 | +- Rollback: |
| 233 | + - revert the storage wiring, cloud UI, and repository changes together; partial rollback would break the storage boundary |
| 234 | + |
| 235 | +## References |
| 236 | + |
| 237 | +- [managedcode/Storage](https://github.com/managedcode/Storage) |
| 238 | +- [ManagedCode Storage docs](https://storage.managed-code.com/) |
| 239 | +- [docs/Architecture.md](/Users/ksemenenko/Developer/PrompterLive/docs/Architecture.md) |
| 240 | + |
| 241 | +## Filing checklist |
| 242 | + |
| 243 | +- [x] File saved under `docs/ADR/ADR-0001-browser-vfs-and-cloud-storage.md`. |
| 244 | +- [x] Status reflects the implemented state. |
| 245 | +- [x] Diagram section contains at least one Mermaid diagram. |
| 246 | +- [x] Testing methodology includes positive, negative, and edge flows plus pass criteria. |
| 247 | +- [x] New or updated automated tests exist for the changed behaviour. |
| 248 | +- [x] `docs/Architecture.md` updated for the new storage boundary. |
0 commit comments