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
| Field | Type | Description |
|---|---|---|
window | WindowConfig | Window options (see below) |
frontendDir | string | Path to built frontend assets |
devUrl | string | Dev server URL (auto-detected; overrides frontendDir in dev) |
menu | MenuSubmenu[] | Native menu bar |
tray | TrayConfig | System tray |
WindowConfig
| Field | Default | Description |
|---|---|---|
title | "" | Window title |
width | 1200 | Initial width |
height | 800 | Initial height |
minWidth / minHeight | — | Minimum size |
maxWidth / maxHeight | — | Maximum size |
resizable | true | Allow resize |
decorations | true | Show title bar |
transparent | false | Transparent background |
alwaysOnTop | false | Pin above other windows |
center | false | Center on screen at startup |
fullscreen | false | Start fullscreen |
maximized | false | Start 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());Menu item shapes
- 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.