Skip to Content
API ReferenceBackend (@tynd/core)

Backend — @tynd/core

The backend module drives the app’s lifecycle and declares window/menu/tray config.

app.start(config)

Call once at the bottom of your backend entry file. The host reads the config on stdout’s first line, builds the window, and starts the event loop.

import { app } from "@tynd/core"; app.start({ frontendDir: import.meta.dir + "/../dist", window: { title: "My App", width: 1200, height: 800, center: true, }, });

AppConfig

FieldTypeDescription
windowWindowConfigWindow options (see below)
frontendDirstringPath to built frontend assets
devUrlstringDev server URL (auto-detected; overrides frontendDir in dev)
menuMenuSubmenu[]Native menu bar
trayTrayConfigSystem tray

WindowConfig

FieldDefaultDescription
title""Window title
width1200Initial width
height800Initial height
minWidth / minHeightMinimum size
maxWidth / maxHeightMaximum size
resizabletrueAllow resize
decorationstrueShow title bar
transparentfalseTransparent background
alwaysOnTopfalsePin above other windows
centerfalseCenter on screen at startup
fullscreenfalseStart fullscreen
maximizedfalseStart maximized

app.onReady(fn)

Fires on __tynd_page_ready — a one-shot postMessage sent from the JS_PAGE_READY init script on DOMContentLoaded.

app.onReady(() => { console.log("window shown, WebView alive"); });

app.onClose(fn)

Fires when the user closes the primary window. The window is hidden immediately; you have 2 seconds to run handlers before a watchdog force-exits.

app.onClose(() => { // quick cleanup only — don't block });

createEmitter<T>()

Create a typed event bus. Exporting the result makes it subscribable from the frontend.

import { createEmitter } from "@tynd/core"; export const events = createEmitter<{ fileChanged: { path: string }; progress: { percent: number }; }>(); events.emit("fileChanged", { path: "/foo.ts" }); events.emit("progress", { percent: 42 });

Frontend subscribes:

api.on("fileChanged", ({ path }) => { /* … */ }); api.once("progress", ({ percent }) => { /* … */ });

Emitters must be exported. The frontend’s type-only typeof backend import needs to see them for api.on("…", …) to type-check.

Native menu bar

app.start({ menu: [ { type: "submenu", label: "File", items: [ { label: "New", id: "file.new", accelerator: "CmdOrCtrl+N" }, { label: "Open", id: "file.open", accelerator: "CmdOrCtrl+O" }, { type: "separator" }, { role: "quit" }, ], }, { type: "submenu", label: "Edit", items: [ { role: "undo" }, { role: "redo" }, { type: "separator" }, { role: "cut" }, { role: "copy" }, { role: "paste" }, ], }, ], // ... });

React to clicks from the frontend with the menu API:

import { menu } from "@tynd/core/client"; menu.onClick("file.new", () => createDocument());
  • Action{ label, id, accelerator?, enabled?, checkbox?, radio? }
  • Separator{ type: "separator" }
  • Role{ role: "quit" | "copy" | "paste" | "undo" | "redo" | "cut" | "selectAll" | "minimize" | "close" | … }

System tray

app.start({ tray: { icon: import.meta.dir + "/assets/tray.png", tooltip: "My App", menu: [ { label: "Show", id: "show" }, { label: "Quit", id: "quit" }, ], }, // ... });

Handle clicks with the tray API:

import { tray } from "@tynd/core/client"; tray.onClick(() => tyndWindow.show()); tray.onMenu("quit", () => process.exit(0));

Frontend RPC — createBackend<T>()

From the frontend:

import { createBackend } from "@tynd/core/client"; import type * as backend from "../../backend/main"; const api = createBackend<typeof backend>(); const msg = await api.greet("Alice"); // fully typed api.on("fileChanged", (evt) => { /* … */ }); api.once("progress", (evt) => { /* … */ });
  • Types flow from typeof backend — no codegen.
  • Errors thrown on the backend surface as rejected promises with { name, message } preserved.
  • async function* backend handlers return StreamCall handles — awaitable + async-iterable.

Next

Last updated on