Desktop app integration planning.
How kifuliiru-data-generators (web) hands off to KEd Studio (desktop) without port conflicts.
Two approaches that do not scale well:
| Approach | Why it fails | |----------|----------------| | Generator opens http://localhost:3000?data=... | Port clash if either app uses 3000; URL size limits for large datasets | | sessionStorage + navigate to another origin | Different apps = different origins; storage does not transfer | | Fixed localhost bridge | User must run both servers; firewall and port conflicts |
Goal: User clicks Open in Editor in the generator → KEd Studio opens with the book loaded. No manual import, no port coordination.
sequenceDiagram
participant User
participant Generator as kifuliiru-data-generators (web)
participant OS as OS / browser
participant Studio as KEd Studio (desktop)
User->>Generator: Generate numbers / sentences
User->>Generator: Click "Open in Editor"
Generator->>Generator: Build .ked.json payload
Generator->>OS: Write temp file book.ked.json
Generator->>OS: Open kedstudio://open?path=...
OS->>Studio: Launch or focus app
Studio->>Studio: Read file, loadPackage()
Studio->>User: Book preview ready
```text
### Why desktop-first is the right call
1. **No port dependency** — generator stays a normal deployed web app; studio runs its own embedded server on `127.0.0.1` with an ephemeral port inside the desktop shell.
2. **Reliable export** — Puppeteer PDF and Node APIs already need a server; desktop avoids serverless limits.
3. **File system** — temp packages, recent files, native Save/Open dialogs.
4. **Custom URL protocol** — standard pattern (`vscode://`, `slack://`, `figma://`); browser can trigger it from the generator with one click.
5. **Same codebase** — wrap existing Next.js app; no rewrite of book logic.
### Trade-offs
| Cost | Mitigation | |------|------------| | Install step for studio | Offer download link + keep **Import .ked.json** fallback | | macOS/Windows signing | Start with dev builds; notarize before public release | | Two deliverables (web optional, desktop primary) | Web remains for dev/CI; desktop is the publisher build |
---
## Technical design
### 1. Desktop shell: Tauri 2 (recommended)
| Option | Verdict | |--------|---------| | **Tauri** | Small binary, Rust shell, WebView — good fit | | Electron | Heavier; fine if team already uses it | | PWA only | Cannot register custom protocols reliably; weak file access |
**Runtime layout:**
```text
┌─────────────────────────────────────┐
│ Tauri window (WebView) │
│ → http://127.0.0.1:<random>/ │
├─────────────────────────────────────┤
│ Sidecar: next start (or node server) │
│ - export-pdf / export-docx │
│ - puppeteer │
└─────────────────────────────────────┘
```text
- On app start: spawn Next production server on `127.0.0.1:0`, open WebView to that URL.
- On quit: stop child process.
- Dev mode: `npm run dev` + Tauri dev pointing at `localhost:3000`.
### 2. Custom URL protocol
Register **`kedstudio://`** (or `kifuliiru-ked://`).
**Open package:**
```text
kedstudio://open?path=/Users/me/Library/Caches/kifuliiru/studio-inbox/numbers-0-100.ked.json
```text
**Optional query params (v2):**
```text
kedstudio://open?path=...&page=cover
```text
**Studio handler (pseudo):**
```ts
// On deep link / second instance:
const filePath = decodeURIComponent(new URL(url).searchParams.get("path"));
const raw = await readFile(filePath, "utf8");
loadPackage(JSON.parse(raw), basename(filePath));
```text
Tauri: use `tauri-plugin-deep-link` + single-instance plugin so a second link focuses the existing window.
### 3. Generator side (`kifuliiru-data-generators`)
**Button: Open in Editor**
```ts
async function openInKedStudio(editorData: KedPackage) {
const payload = buildKedJson(editorData); // ked_version, book_type, rows, ...
// 1. POST to a small "handoff" endpoint that returns a signed temp URL — OR —
// 2. Client-side: trigger download + protocol (desktop-only path)
// Recommended: generator API writes temp file server-side is NOT needed.
// Client writes via File System Access API where available, else download fallback.
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
const filename = `kifuliiru-${payload.book_type}-${Date.now()}.ked.json`;
// Desktop path: save to known inbox via optional native helper, or:
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click(); // ensures file exists in Downloads
// Then attempt protocol open with path — desktop app registers handler for Downloads scan
// BETTER: Tauri / native helper exposes kedstudio://import with base64 for small payloads
// BEST: write to OS temp via @tauri-apps/api from a tiny companion — see below
}
```text
**Practical v1 handoff (simplest):**
1. Generator builds `.ked.json` in memory.
2. `POST` to generator backend `/api/ked-handoff` → stores blob 5 min → returns `handoff_id`.
3. Open `kedstudio://open?handoff=https://generators.../api/ked-handoff/{id}`.
4. Desktop app fetches handoff URL, loads package, deletes handoff.
This avoids writing local paths from the browser (browsers cannot pass arbitrary file paths to custom protocols). **File path protocol works when the desktop app writes the file** (e.g. generator downloads, user picks "Open with KEd Studio") or when using **handoff URL**.
**Recommended v1 for browser → desktop:**
| Step | Action | |------|--------| | 1 | Generator creates `.ked.json` | | 2 | `POST /api/ked-handoff` → `{ id, url }` (short-lived, auth optional) | | 3 | `window.location.href = 'kedstudio://open?url=' + encodeURIComponent(handoffUrl)` | | 4 | Desktop app fetches URL, `loadPackage()`, shows success |
No local ports. No huge URLs. Works from any deployed generator.
**v2:** Add `path=` for when both apps are local and a native bridge writes the file.
### 4. Shared contract (already in place)
- [KED_JSON_CONTRACT.md](/ked-studio/guides/ked-json-contract)
- `book_settings` + `template` round-trip on Save
- Legacy `editorData` normalization in `lib/package.ts`
Generator must emit:
```json
{
"ked_version": "1.0",
"source_app": "kifuliiru-data-generators",
"source_app_version": "1.2.0",
"book_type": "numbers",
"title": "...",
"subtitle": "...",
"languages": ["kifuliiru", "english"],
"columns": [...],
"rows": [...],
"metadata": { "range": [0, 100], "author": "..." }
}
```text
Studio applies defaults for `book_settings` and `template` if omitted.
### 5. Detect desktop app installed
Generator UI:
```ts
const INSTALLED_KEY = "ked-studio-available";
async function tryOpenInEditor(payload: KedPackage) {
const handoff = await createHandoff(payload);
const deepLink = `kedstudio://open?url=${encodeURIComponent(handoff.url)}`;
// Timeout trick: if app opens, page may blur; else show install CTA
window.location.href = deepLink;
setTimeout(() => {
if (!document.hidden) {
showDialog({
title: "KEd Studio not found",
actions: [
{ label: "Download desktop app", href: RELEASES_URL },
{ label: "Download .ked.json instead", onClick: () => downloadJson(payload) },
],
});
}
}, 1500);
}
```text
---
## Implementation phases
### Phase A — Desktop MVP (this repo)
- [ ] Add `src-tauri/` (Tauri 2) wrapping Next.js production build
- [ ] Spawn `next start` on random localhost port; load WebView
- [ ] Register `kedstudio://` protocol (macOS + Windows)
- [ ] Handle `open?url=` → fetch handoff → `loadPackage()`
- [ ] Menu: Open file, Recent files, Quit
- [ ] CI: build `.dmg` / `.msi`
**Estimate:** 3–5 days for MVP shell + protocol.
### Phase B — Generator handoff (kifuliiru-data-generators)
- [ ] `buildKedJson()` helper per data type (numbers first)
- [ ] `POST /api/ked-handoff` (Redis or filesystem temp, 10 min TTL)
- [ ] **Open in Editor** button on numbers page (then sentences, etc.)
- [ ] Install detection + fallback download
- [ ] Link to desktop releases README
**Estimate:** 2–3 days for numbers end-to-end.
### Phase C — Polish
- [ ] Handoff auth (signed tokens, single use)
- [ ] Pass `book_settings` presets from generator (optional)
- [ ] Auto-update desktop app (Tauri updater)
- [ ] Linux `.AppImage` if needed
### Phase D — Web studio (optional)
Keep `npm run dev` for development. Production web deploy is optional (export APIs need Node + Chromium). **Desktop is the primary publisher artifact.**
---
## What not to do
- Do not depend on both apps sharing `localhost:3000`.
- Do not put full `rows` arrays in query strings.
- Do not rely on `sessionStorage` across separate web apps.
- Do not block manual Import — always keep file picker + sample data.
---
## Open questions
1. **Release hosting** — GitHub Releases vs kifuliiru.com download page?
2. **Handoff TTL** — 10 minutes enough for slow networks?
3. **Offline generator** — is a local-only generator build needed later?
4. **Code signing** — Apple Developer ID timeline for macOS Gatekeeper?
---
## References
- [BOOK_EDITOR_DATA_INTEGRATION.md](./reference/web-editor/BOOK_EDITOR_DATA_INTEGRATION.md) — legacy sessionStorage approach (superseded by this plan)
- [ROADMAP.md](./ROADMAP.md) — product phases
- [Tauri deep link plugin](https://v2.tauri.app/plugin/deep-link/)
- [Tauri single instance](https://v2.tauri.app/plugin/single-instance/)