{"id":27641310,"url":"https://github.com/streamui/ssr-electron","last_synced_at":"2025-04-23T23:43:36.210Z","repository":{"id":288317071,"uuid":"967633890","full_name":"StreamUI/ssr-electron","owner":"StreamUI","description":"Electron SSR w/ htmx, Alpine.js, Datastar, and SSE","archived":false,"fork":false,"pushed_at":"2025-04-19T18:59:44.000Z","size":150,"stargazers_count":12,"open_issues_count":1,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-19T20:43:46.452Z","etag":null,"topics":["alpine","alpinejs","datastar","electron","htmx","ssr"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/StreamUI.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-04-16T18:55:03.000Z","updated_at":"2025-04-19T19:38:36.000Z","dependencies_parsed_at":"2025-04-17T05:44:54.767Z","dependency_job_id":"75f423f5-bfd2-4300-95a3-46189867ec4a","html_url":"https://github.com/StreamUI/ssr-electron","commit_stats":null,"previous_names":["streamui/ssr-electron","streamui/electron-ssr"],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StreamUI%2Fssr-electron","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StreamUI%2Fssr-electron/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StreamUI%2Fssr-electron/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/StreamUI%2Fssr-electron/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/StreamUI","download_url":"https://codeload.github.com/StreamUI/ssr-electron/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250535057,"owners_count":21446503,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alpine","alpinejs","datastar","electron","htmx","ssr"],"created_at":"2025-04-23T23:43:31.595Z","updated_at":"2025-04-23T23:43:34.258Z","avatar_url":"https://github.com/StreamUI.png","language":"TypeScript","readme":"# SSR in Electron using htmx and Datastar – no more IPC\n\n## Avoid the electron IPC using SSR instead (with htmx, AlpineJS or Datastar)\n\n\n## Get started right away\n\n### Setup\n\n```bash\n# Install dependencies\nnpm install ssr-electron\n```\n\n### Simple Example\n\n```javascript\n// main.js\nimport { app, BrowserWindow } from 'electron';\nimport { createSSR } from 'ssr-electron';\n\n// Create the SSR bridge instance - it automatically registers schemes and handlers\nconst ssr = createSSR({ debug: true });\n\nfunction createWindow() {\n  const win = new BrowserWindow({\n    width: 800,\n    height: 600,\n    webPreferences: {\n      nodeIntegration: false,\n      contextIsolation: true,\n    },\n  });\n\n  // Load the app from the virtual URL\n  win.loadURL('http://localhost/');\n  \n  return win;\n}\n\napp.whenReady().then(() =\u003e {\n  // Register route for the main page\n  ssr.registerRoute('/', (request, url) =\u003e {\n    // HTML is generated and served directly from the main process\n    return new Response(`\n      \u003c!DOCTYPE html\u003e\n      \u003chtml lang=\"en\"\u003e\n      \u003chead\u003e\n        \u003cmeta charset=\"UTF-8\"\u003e\n        \u003ctitle\u003eElectron SSR Example\u003c/title\u003e\n        \u003cscript src=\"https://unpkg.com/htmx.org@2.0.4\"\u003e\u003c/script\u003e\n        \u003cstyle\u003e\n          body {\n            font-family: system-ui, -apple-system, sans-serif;\n            max-width: 800px;\n            margin: 0 auto;\n            padding: 20px;\n          }\n          button {\n            background-color: #4a86e8;\n            color: white;\n            border: none;\n            border-radius: 4px;\n            padding: 10px 15px;\n            cursor: pointer;\n          }\n        \u003c/style\u003e\n      \u003c/head\u003e\n      \u003cbody\u003e\n        \u003ch1\u003eElectron SSR Example\u003c/h1\u003e\n        \u003cbutton\n          hx-get=\"/system-info\"\n          hx-target=\"#content\"\u003e\n          Load System Info\n        \u003c/button\u003e\n        \u003cdiv id=\"content\"\u003e\n          \u003cp\u003eClick the button to load data from the main process\u003c/p\u003e\n        \u003c/div\u003e\n      \u003c/body\u003e\n      \u003c/html\u003e\n    `, {\n      headers: { 'Content-Type': 'text/html' }\n    });\n  });\n\n  // Register route for system info\n  ssr.registerRoute('/system-info', (request, url) =\u003e {\n    // This data is only available in the main process\n    return new Response(`\n      \u003cdiv\u003e\n        \u003ch2\u003eSystem Information\u003c/h2\u003e\n        \u003cul\u003e\n          \u003cli\u003ePlatform: ${process.platform}\u003c/li\u003e\n          \u003cli\u003eArchitecture: ${process.arch}\u003c/li\u003e\n          \u003cli\u003eNode.js Version: ${process.version}\u003c/li\u003e\n          \u003cli\u003eElectron Version: ${process.versions.electron}\u003c/li\u003e\n        \u003c/ul\u003e\n        \u003cp\u003e\u003cem\u003eThis data comes directly from the Electron main process!\u003c/em\u003e\u003c/p\u003e\n      \u003c/div\u003e\n    `, {\n      headers: { 'Content-Type': 'text/html' }\n    });\n  });\n\n  createWindow();\n});\n```\n\n### The struggle with Electron IPC\n\nIf you've built an Electron app, you're familiar with the pain of IPC (Inter-Process Communication). The main and renderer processes are completely isolated, forcing developers to set up complex messaging systems just to share data and trigger actions across the boundary. This results in:\n\n- Verbose boilerplate code for sending and receiving messages\n- Complex state synchronization between processes\n- Error-prone message handling\n- Type safety challenges across the boundary\n\n### Existing solutions\n\nSeveral projects make working with IPC slightly more fun:\n\n- [electron-trpc](https://github.com/jsonnull/electron-trpc) - Uses tRPC to create type-safe APIs across the IPC boundary\n- [zubridge](https://github.com/goosewobbler/zubridge) - Brings Zustand state management across the IPC boundary\n- I even played around with re-building the above with Effect.ts which was quite nice.\n\nI wanted something simpler.\n\n### A different approach: Server-Side Rendering in Electron\n\nI stumbled upon this article in my rabbit hole:  [The ultimate Electron app with Next.js and React Server Components](https://medium.com/@kirill.konshin/the-ultimate-electron-app-with-next-js-and-react-server-components-a5c0cabda72b). I ended up not going this direction as I deeply dislike NextJS, but later I stumbled upon Datastar and HTMX and remembered this article and I wondered if I could adapt the idea to work them instead. Potentially I could also get this working with just bare React 🤔\n\nThe result is Electron SSR - a library that lets you use server-side rendering patterns directly in your Electron app without dealing with IPC.\n\n## Why Electron SSR?\n\nThe biggest advantage of Electron SSR is that it allows you to **return views from the main process that have direct access to Node.js modules** that are normally unavailable in the renderer.\n\n### Direct access to native modules\n\nWith traditional Electron development, if you want to use Node.js modules in your renderer or access data from the main process, you need to:\n\n1. Create a preload script to safely expose main process functionality\n2. Set up contextBridge to define the API that will be available in the renderer\n3. Create IPC channels for each type of communication needed\n4. Set up handlers in the main process for each channel\n5. Call these exposed IPC methods from the renderer\n6. Parse and handle the responses\n7. Update your UI accordingly\n\nThis requires careful coordination between multiple files and introduces complexity:\n\n### 😢 Not fun way:\n\n```javascript\n// preload.js\nconst { contextBridge, ipcRenderer } = require('electron')\n\ncontextBridge.exposeInMainWorld('electronAPI', {\n  readNote: () =\u003e ipcRenderer.invoke('read-note'),\n  saveNote: (content) =\u003e ipcRenderer.invoke('save-note', content),\n  // Expose a function to listen for notifications from main\n  onNoteUpdated: (callback) =\u003e ipcRenderer.on('note-updated', (_event, value) =\u003e callback(value))\n})\n\n// main.js\nconst { app, BrowserWindow, ipcMain, safeStorage } = require('electron')\nconst fs = require('fs/promises')\n\n// Creating the window\nlet mainWindow;\n\nfunction createWindow() {\n  mainWindow = new BrowserWindow({\n    width: 800,\n    height: 600,\n    webPreferences: {\n      preload: path.join(__dirname, 'preload.js'),\n      contextIsolation: true\n    }\n  });\n  mainWindow.loadFile('index.html');\n}\n\n// IPC handlers\nipcMain.handle('read-note', async () =\u003e {\n  try {\n    const encryptedContent = await fs.readFile('user-notes.enc')\n    return await safeStorage.decryptString(encryptedContent)\n  } catch (error) {\n    return { error: error.message }\n  }\n})\n\nipcMain.handle('save-note', async (event, content) =\u003e {\n  try {\n    const encrypted = await safeStorage.encryptString(content)\n    await fs.writeFile('user-notes.enc', encrypted)\n    \n    // After saving, notify the renderer about the update\n    mainWindow.webContents.send('note-updated', 'Note saved successfully!')\n    return { success: true }\n  } catch (error) {\n    return { error: error.message }\n  }\n})\n\n// renderer.js\ndocument.getElementById('load-button').addEventListener('click', async () =\u003e {\n  const content = await window.electronAPI.readNote()\n  if (content.error) {\n    document.getElementById('note-container').innerText = `Error: ${content.error}`\n  } else {\n    document.getElementById('note-container').innerText = content\n  }\n})\n\ndocument.getElementById('save-button').addEventListener('click', async () =\u003e {\n  const content = document.getElementById('note-input').value\n  const result = await window.electronAPI.saveNote(content)\n  if (result.error) {\n    alert(`Failed to save: ${result.error}`)\n  } else {\n    alert('Saved successfully!')\n  }\n})\n\n// Listen for updates from the main process\nwindow.electronAPI.onNoteUpdated((message) =\u003e {\n  document.getElementById('status-container').innerText = message\n  // You could also refresh the note content here\n})\n```\n\nWith Electron SSR, you can simply define a route handler that:\n\n1. Directly accesses Node.js modules and Electron APIs\n2. Returns HTML with the results already integrated\n3. The renderer just makes a request and gets the rendered result\n\n### 😊 Fun way with Electron SSR:\n\n```javascript\n// In your main process setup\nimport { createSSR } from 'ssr-electron';\nimport fs from 'fs/promises';\nimport { safeStorage } from 'electron';\n\nconst ssr = createSSR({ debug: true });\n\n// Register a route that reads a file and returns its content\nssr.registerRoute('/notes', async (request, url) =\u003e {\n  try {\n    // Access Node.js modules directly\n    const encryptedContent = await fs.readFile('user-notes.enc');\n    const content = await safeStorage.decryptString(encryptedContent);\n    \n    // Return HTML with the content already integrated\n    return new Response(`\n      \u003cdiv id=\"notes-container\"\u003e\n        \u003ch1\u003eYour Notes\u003c/h1\u003e\n        \u003cdiv class=\"note-content\"\u003e${content}\u003c/div\u003e\n        \u003cbutton hx-post=\"/save-note\" hx-target=\"#notes-container\"\u003eSave\u003c/button\u003e\n      \u003c/div\u003e\n    `, {\n      headers: { 'Content-Type': 'text/html' }\n    });\n  } catch (error) {\n    return new Response(`\u003cdiv\u003eError: ${error.message}\u003c/div\u003e`, { status: 500 });\n  }\n});\n\n// Handle saving notes\nssr.registerRoute('/save-note', async (request, url) =\u003e {\n  // Get form data from the request\n  const formData = await request.formData();\n  const noteContent = formData.get('content');\n  \n  // Encrypt and save to filesystem\n  const encrypted = await safeStorage.encryptString(noteContent);\n  await fs.writeFile('user-notes.enc', encrypted);\n  \n  // Return updated UI\n  return new Response(`\n    \u003cdiv id=\"notes-container\"\u003e\n      \u003ch1\u003eYour Notes\u003c/h1\u003e\n      \u003cdiv class=\"note-content\"\u003e${noteContent}\u003c/div\u003e\n      \u003cbutton hx-post=\"/save-note\" hx-target=\"#notes-container\"\u003eSave\u003c/button\u003e\n      \u003cdiv class=\"success-message\"\u003eSaved successfully!\u003c/div\u003e\n    \u003c/div\u003e\n  `, {\n    headers: { 'Content-Type': 'text/html' }\n  });\n}, 'POST');\n```\n\nIn your renderer, the code is pure HTML and HTMX - no IPC needed:\n\n```html\n\u003cbody\u003e\n  \u003cdiv id=\"app\"\u003e\n    \u003cdiv hx-get=\"/notes\" hx-trigger=\"load\"\u003eLoading...\u003c/div\u003e\n  \u003c/div\u003e\n  \n  \u003cscript src=\"https://unpkg.com/htmx.org@2.0.4\"\u003e\u003c/script\u003e\n\u003c/body\u003e\n```\n\n### Real-time updates with SSE\n\nElectron SSR also supports Server-Sent Events, making it easy to push updates from the main process to the renderer:\n\n```javascript\n// In main process\nimport { watch } from 'fs';\n\n// Set up file watcher\nwatch('user-notes.enc', () =\u003e {\n  // When file changes, broadcast to all clients\n  ssr.broadcastContent('note-updated', `\n    \u003cdiv class=\"note-content\"\u003e${decryptedContent}\u003c/div\u003e\n  `);\n});\n\n// In renderer\n\u003cdiv hx-sse=\"connect:sse://events\"\u003e\n  \u003cdiv hx-sse=\"swap:note-updated\" id=\"note-content\"\u003e\n    Initial content\n  \u003c/div\u003e\n\u003c/div\u003e\n```\n\nThis approach completely eliminates the need to manually handle IPC communication, making your Electron apps feel more like traditional web development while still leveraging the full power of Node.js and native modules.\n\n## How it works\n\nElectron SSR works by\n\n1. Register [HTTP routes and handle requests directly in Electron](https://www.electronjs.org/docs/latest/api/protocol)\n2. Creating a virtual \"server\" that runs in the main process\n3. Handling HTTP-like requests from the renderer\n4. Supporting Server-Sent Events (SSE) for real-time updates\n5. Integrating seamlessly with HTMX and Datastar\n\nEssentially, it turns your main process into a server that your renderer can communicate with using standard web protocols, **without actually running a server or opening any ports**.\n\n\n## Examples\n\n\nSee the `examples` directory for complete examples:\n\n- `simple-example.js` - Basic example with HTMX and Alpine.js\n- `simple-alpine.js` - Basic example, but using only AlpineJS\n- `htmx-notes.js` - Example with realtime synced secure notes\n- `datastar.js` - Example using Datastar\n- `realtime.js` - Testing streaming HTML updates over SSE at 60+ FPS with Datastar\n\n\n### Contributing\n\nI just started playing around with HTMX and Datastar, so feel free to submit updates or more examples to show off the power!\n\n## License\n\nMIT\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstreamui%2Fssr-electron","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstreamui%2Fssr-electron","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstreamui%2Fssr-electron/lists"}