# Murasaki complete documentation corpus Framework version: 0.52.0 (pre-1.0). Status rule: planned is unavailable; partial and experimental entries retain every limitation stated in the capability manifest. # English # Introduction (https://murasaki.ichi10.com/docs) **Murasaki** lets you build native desktop apps the way you build a Next.js app: file-based routing, layouts, server actions, API routes, and React 19 — running in a Rust-native window, not Electron's bundled Chromium. You write React and TypeScript. Murasaki ships a small Rust core (`@murasakijs/native`, built on tao/wry/muda) that gives you a real native window, native menus, and OS integration — **without you writing any Rust**. ## Why Murasaki [#why-murasaki] | | Murasaki | Electron | Tauri | | -------------- | --------------------------------------------------- | ------------------ | --------------------- | | UI runtime | OS WebView (wry) | bundled Chromium | OS WebView | | Language | TypeScript / React | TypeScript / React | TypeScript + **Rust** | | DX | **Next.js-style** (Vite HMR) | manual wiring | manual wiring | | Memory (idle) | \~1/5 of Electron\* | baseline | small | | Installer size | **\~43 MB `.dmg`** / **\~120 MB `.app`**† | \~80–150 MB\* | \~3–10 MB\* | | Server actions | `defineAction` / `useAction` | manual IPC | manual IPC / commands | \* Commonly-cited ballpark for Electron/Tauri — not a measured benchmark. † Measured on macOS — the `.app` bundles a full Node runtime, which is where most of its size comes from. **Choose Murasaki if** you know React/Next.js and want a small-footprint desktop app without learning Rust or hand-wiring IPC. ## What you get [#what-you-get] * **File-based routing** — `src/app/**/page.tsx`, layouts, dynamic segments, `loading` / `error` / `not-found`, `middleware.ts`. See [Routing](/docs/guides/routing). * **Server Actions** — `'use server'` + `defineAction` / `useAction`, the React 19 shape you already know. See [Server Actions](/docs/guides/server-actions). * **Node Main** — a long-lived Node lifecycle plus typed `'use main'` calls for databases, sockets, workers, and background work. See [Node Main](/docs/guides/node-main). * **API Routes** — Next.js-style `src/api/**/route.ts` HTTP endpoints. See [API Routes](/docs/guides/api-routes). * **Native window & menus** — a real native window, a native menu bar, and scoped native context menus (`NSMenu`, not HTML popups). See [Native APIs](/docs/guides/native-apis). * **UI kit** — `@murasakijs/ui`, a shadcn-style component library. See [Styling](/docs/guides/styling) and the live [Components](/docs/components). * **Real distribution** — portable, cross-arch native installers for macOS and Windows, optional code signing + notarization. See [Distribution](/docs/building/distribution). Bundling ships installers for macOS (`.app` / `.dmg`) and Windows (`.zip` / `.exe` / `.msi`) today; Linux (`.AppImage`) is on the roadmap. The Rust core already builds for all five targets. Murasaki is pre-1.0. Review the [platform and feature status](/docs/core-concepts/platform-feature-status) for production blockers such as global shortcuts, Linux packaging, and Linux package signing. ## Next steps [#next-steps] --- # Project structure (https://murasaki.ichi10.com/docs/getting-started/project-structure) A scaffolded app looks like a Next.js project. You only touch `src/` and `murasaki.config.ts` — Murasaki owns the app shell and the client bootstrap, so there's no `index.html` or entry file to maintain. ```txt my-app/ ├─ src/ │ ├─ app/ # pages, layouts, globals.css (file-based routing) │ │ ├─ layout.tsx # root layout — wraps children in │ │ ├─ page.tsx # "/" route │ │ └─ about/page.tsx # "/about" route │ ├─ api/ # API route handlers -> /api/* │ │ ├─ hello/route.ts # GET/POST handlers │ │ └─ greet/[name]/route.ts # dynamic segment -> /api/greet/:name │ ├─ main.ts # long-lived Node lifecycle (optional) │ ├─ backend/ # common home for 'use main' modules (optional) │ ├─ middleware.ts # runs before every navigation (optional) │ ├─ lib/ # your app code (stores, context-menu actions, helpers) │ └─ assets/ # images, icons ├─ murasaki.config.ts # app identity, window, signing ├─ tailwind.config.ts ├─ vite.config.ts # wires up the murasaki Vite plugin └─ package.json ``` There's no `src/actions.ts` in the scaffold itself, but it's the common spot for `'use server'` functions — see [Server Actions](/docs/guides/server-actions). A `'use server'` file can live anywhere under `src/`; it isn't tied to `src/app/` or `src/api/`. ## `src/app/` — routes [#srcapp--routes] File-based routing over `src/app/**/page.tsx`, with `layout.tsx`, dynamic `[param]` segments, and `loading` / `error` / `not-found` boundaries. Your root layout wraps everything in ``. See [Routing](/docs/guides/routing). ## `src/api/` — API routes [#srcapi--api-routes] `src/api//route.ts` exports one function per HTTP method and is served at `/api/`. These run on the server (Node) in both dev and prod. See [API Routes](/docs/guides/api-routes). ## Server actions [#server-actions] Unlike `src/app/` and `src/api/`, there's no dedicated folder — a `'use server'` function runs on the server and is called from the client with `useAction`, wherever you put the file. See [Server Actions](/docs/guides/server-actions). ## Node Main and `'use main'` [#node-main-and-use-main] `src/main.ts` owns long-lived Node resources and graceful shutdown. A module with a top-level `'use main'` directive exposes typed renderer-to-Node functions and can live anywhere under `src/`; `src/backend/` is only a useful convention. See [Node Main](/docs/guides/node-main). ## `murasaki.config.ts` [#murasakiconfigts] Your app's identity, window, and build settings: ```ts title="murasaki.config.ts" import { defineConfig } from 'murasaki' export default defineConfig({ appId: 'com.example.my-app', productName: 'My App', version: '0.1.0', icon: 'src/assets/icon.png', window: { width: 1000, height: 700 }, }) ``` See [Configuration](/docs/building/configuration) for every field. ## Next [#next] --- # Quick start (https://murasaki.ichi10.com/docs/getting-started/quick-start) ## Prerequisites [#prerequisites] * **Node.js 22.12+** (Node 20 is end-of-life and is not supported) * `murasaki dev` runs on **macOS, Windows, and Linux**. A macOS `.app` / `.dmg` must be built on macOS; a portable Windows folder / `.zip` can be cross-built from any host. Linux app packaging is on the roadmap. ## Scaffold [#scaffold] ```bash pnpm create murasaki@latest my-app cd my-app ``` This creates a React 19 + Vite + Tailwind app with a Next.js-like layout, a native window, and a working context menu — no `index.html` or entry file to maintain (Murasaki owns the app shell). ## Develop [#develop] ```bash pnpm dev ``` A native window opens on top of the Vite dev server — **React Fast Refresh and HMR work as usual**. Edit `src/app/page.tsx` and the window updates instantly. Right-click anywhere for the app's native menu; right-click the card for its own scoped menu — both are real `NSMenu`s, not HTML popups. ## Build a distributable [#build-a-distributable] ```bash pnpm bundle pnpm installer ``` The bundle ships a portable Node runtime and a native launcher, so it runs on other machines — not just your build machine. On macOS, the first two commands produce an `.app` and `.dmg`. A portable Windows folder / `.zip` can be built from any host by selecting a Windows target: ```bash pnpm bundle --arch x64 # choose the target architecture pnpm bundle --target win32-x64 # portable Windows folder + .zip ``` `pnpm installer --target win32-x64` additionally creates an NSIS `.exe` when `makensis` is installed, and an MSI `.msi` when WiX v4 is installed on Windows. If neither optional tool is available, no Windows installer is produced; the portable `.zip` still works. See [Distribution](/docs/building/distribution) for tool setup and all targets. On macOS, unsigned builds trigger Gatekeeper on other machines (recipients right-click → **Open** the first time). To ship a signed, notarized app with no warning, see [Distribution → Signing](/docs/building/distribution#signing--notarization). ## Next [#next] --- # Architecture (https://murasaki.ichi10.com/docs/core-concepts/architecture) Murasaki is a three-layer desktop runtime. React renders in the operating system WebView, application backend code runs in a bundled Node.js process, and a small Rust host owns the native window and application lifecycle. ```txt ┌──────────────── Native application ────────────────┐ │ │ │ Rust host (tao / wry / muda) │ │ windows · WebViews · native menus · update handoff│ │ │ │ │ │ launches / supervises │ │ ▼ │ │ Local Node runtime │ │ src/main.ts · use-main · use-server · API routes │ │ │ │ │ │ authenticated loopback HTTP│ │ ▼ │ │ OS WebView │ │ React 19 · Murasaki router · your renderer code │ │ │ └────────────────────────────────────────────────────┘ ``` This is deliberately different from Electron's embedded Chromium + Node renderer model. Renderer code does not receive Node globals. Put filesystem, database, socket, worker, and secret-bearing code in [`src/main.ts`](/docs/guides/node-main), a `'use main'` module, a [`'use server'` action](/docs/guides/server-actions), or an [`src/api/` route](/docs/guides/api-routes). ## Layer responsibilities [#layer-responsibilities] | Layer | Owns | Does not own | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | | Rust host | Native event loop, declared application windows, per-window navigation/capability policy, OS open-request normalization, menus, Node child supervision, final update handoff | Business logic and React state | | Local Node runtime | Long-lived main lifecycle, registered URL/file handling, actions, API routes, update verification, application resources and data | Arbitrary native window creation | | Renderer | UI, routing, browser APIs, typed backend calls, permitted management of declared windows | Direct filesystem/Node/native module access | Murasaki is pre-1.0. Declared windows have independent, deny-by-default command allowlists, but capability arguments and path/URL scopes are not yet policy-controlled. Packaged macOS/Windows apps enforce a single instance, can register URL schemes and file associations, and expose matching activations to Node Main through `openRequested()`. Check [Platform & feature status](/docs/core-concepts/platform-feature-status) before committing a production architecture to a feature. ## Development and packaged runtime [#development-and-packaged-runtime] The application code keeps the same boundaries in both modes, but the host processes differ: | | `murasaki dev` | Packaged app | | --------------------- | ----------------------------------------------- | ------------------------------------------------ | | Web assets | Vite dev server with HMR | `dist/client` served by bundled Node | | Node backend | Vite child process | Portable Node runtime inside app resources | | Native host | CLI process loads `@murasakijs/native` | Standalone Rust launcher | | Declared windows | All routes created with individual dev WebViews | All routes created from resolved bundle metadata | | Main entry | Loaded with Vite SSR; exact entry hot-reloads | Compiled to `server/main.mjs` | | Backend calls | Vite middleware | Bundled loopback HTTP server | | URL/file registration | Not installed by dev mode | macOS app metadata or Windows installer registry | Production uses a deterministic loopback port derived from `appId`, giving the renderer a stable origin across launches. The Windows WebView2 profile is also isolated by `appId`. Keep `appId` stable after release: changing it can change the application data paths, browser profile, bundle identity, single-instance lock, and local origin. ## Next.js-like, not Next.js itself [#nextjs-like-not-nextjs-itself] Murasaki adopts familiar conventions—`src/app`, layouts, route handlers, directives, and React 19—but it is not the Next.js runtime. There are no React Server Components or Edge runtime, and middleware only participates in client navigation. Each guide documents the supported subset; do not assume an API is available because it exists in Next.js. ## Next [#next] --- # Platform & Feature Status (https://murasaki.ichi10.com/docs/core-concepts/platform-feature-status) Murasaki is pre-1.0 software. Some parts are ready to use, some have meaningful platform gaps, and others are designs rather than shipping APIs. This page is generated from [`packages/murasaki/capabilities.json`](https://github.com/murasakijs/murasaki/blob/main/packages/murasaki/capabilities.json), the repository's canonical capability manifest. **Planned does not mean available.** A dependency, Rust module, type, or roadmap entry is not marked as shipping until Murasaki exposes a supported path and records its limitations here. ## Status definitions [#status-definitions] * **Stable** — shipping public surface with a compatibility commitment for the current release line. * **Experimental** — shipping and tested, but its API or wire contract may change before 1.0. * **Partial** — usable today, with a material API or platform gap described in the table. * **Planned** — no supported implementation is available. Platform labels are separate from feature maturity. **Dev only** means the development runtime exists on that platform but there is no production packaging claim. **Unavailable** means that feature does not apply or has no implementation there. ## Reading the evidence [#reading-the-evidence] Open **API and evidence** in any row to see the supported public symbols and the automated test or workflow used as evidence. A build-only workflow proves that the code compiles on that platform; it does not turn an unexposed native helper into a supported framework API. If implementation and this manifest disagree, treat the lower capability as the safe assumption and [report the mismatch](https://github.com/murasakijs/murasaki/issues). --- # Process model (https://murasaki.ichi10.com/docs/core-concepts/process-model) Murasaki separates the native event loop from Node's event loop. In a packaged application the Rust launcher starts and supervises the bundled Node child; the Node child starts your Main lifecycle and local HTTP server; only then does the host load the renderer. ```txt launch → Rust host acquires the per-user app lock → host chooses the app-local origin and creates a runtime token → Node child loads src/main.ts → main.ready(context) finishes → local server begins listening → registered cold-start URLs/files are delivered to main.openRequested() → declared WebViews load their renderer routes ``` If `ready()` rejects, startup fails instead of showing a renderer attached to a half-initialized backend. Use it for resources that must exist before the UI is usable: opening a database, running migrations, or starting an app-owned service. In packaged macOS and Windows apps, a second launch does not start another backend. Its `argv` / working directory are delivered to `main.secondInstance()`, and the host focuses the primary window. Development mode and Linux do not currently provide this lock. ## Open requests [#open-requests] Packaged macOS and Windows apps can register custom URL schemes and document extensions with `protocols` and `fileAssociations` in `murasaki.config.ts`. After `ready()` has completed, matching activations are normalized and delivered to `main.openRequested()`: * `activation` is `cold-start`, `second-instance`, or `os-event`. * `transport` is `argv`, `open-url`, or `open-file`. * `targets` contains `{ kind: 'url', url, scheme }` or `{ kind: 'file', path }` values. On a second launch, `secondInstance()` still receives the raw process arguments while `openRequested()` receives only registered URLs and files. Use `openRequested()` for cross-platform open behavior and `secondInstance()` for other secondary-launch arguments. URL and file activations are untrusted operating-system input. Registration selects which inputs reach the hook; it does not authenticate the sender or make the URL contents or file contents safe. ## Quit sequence [#quit-sequence] Normal window-close and app-quit requests use the lifecycle below: ```txt quit request → beforeQuit(context) may return false ┐ → context.signal aborts → shutdown(context) ┘ bounded together by shutdownTimeoutMs → Node child exits → native host exits or applies an update ``` `beforeQuit()` may cancel a normal close—for example while a document has unsaved changes. Forced shutdown paths, including process signals and dev Main reloads, ignore cancellation. The combined hooks default to a 10-second limit; `shutdown()` should stop accepting work first, then flush and close resources. The OS close control and `appWindow.close()` hide a secondary, so it can be shown again with `windows.open()`. Closing the primary `main` window is an application quit request and follows the sequence above. Cancellation is application-wide; Murasaki does not expose per-window close lifecycle events. Explicit `windows.close(label)` destroys a secondary target, which cannot currently be recreated in the same process. ## Development reloads [#development-reloads] During `murasaki dev`, a change to the configured Main entry triggers: 1. the current `shutdown({ reason: 'dev-reload' })`, 2. Vite invalidation of that module, 3. construction of a fresh lifecycle, 4. a new `ready()` call. Changes imported by Main still follow Vite's module graph, but only the exact configured entry currently triggers the lifecycle restart. Avoid module-level timers or sockets: create them inside `ready()` and close them inside `shutdown()` so reloads do not leak work. ## Application paths [#application-paths] `MainContext.paths` gives stable, per-`appId` locations: | Path | Intended data | | ------- | --------------------------------------------------------- | | `data` | Databases, user documents owned by the app, durable state | | `cache` | Re-creatable caches | | `logs` | Application logs | | `temp` | Ephemeral staging files | Use these instead of writing beside the executable. Packaged resources may be read-only, app locations vary by OS, and updates may replace the application bundle. ## Process failures [#process-failures] The host owns the Node child lifetime. On macOS/Linux the bundled server exits if it becomes orphaned; on Windows it is assigned to a Job Object that is closed with the launcher. Murasaki does not yet expose a public crash-restart policy, health-check hook, or multiple supervised worker processes. If you spawn workers or child processes from Main, you own their shutdown behavior. ## Next [#next] --- # AI tooling (https://murasaki.ichi10.com/docs/guides/ai-tools) Murasaki publishes structured documentation for coding agents and other LLM clients. All compatibility answers are grounded in the repository's canonical `packages/murasaki/capabilities.json` manifest, so planned APIs are not presented as shipping features. ## LLM text endpoints [#llm-text-endpoints] | Endpoint | Use it for | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | [`/llms.txt`](/llms.txt) | A compact, absolute-URL index of the important documentation. | | [`/llms-full.txt`](/llms-full.txt) | The complete processed English and Japanese documentation corpus. | | [`/llms-api.txt`](/llms-api.txt) | JSON containing feature maturity, platform support, limitations, public symbols, evidence, and the configuration schema. | | `/llms.mdx/{lang}/docs/{slug}/content.md` | One documentation page as Markdown, for example `/llms.mdx/en/docs/guides/routing/content.md`. | The feature labels are deliberate: **planned** means unavailable, **experimental** means the API or wire contract may change, and **partial** means a material limitation remains. See [Platform & Feature Status](/docs/core-concepts/platform-feature-status). ## MCP server [#mcp-server] `@murasakijs/mcp` is a local stdio MCP server. It is read-only: there are no shell, build, release, publish, or file-writing tools. The package is published on npm. Configure an MCP client to run the latest stable version without a repository checkout: ```json { "mcpServers": { "murasaki": { "command": "npx", "args": ["-y", "@murasakijs/mcp@latest"] } } } ``` ## Tools [#tools] * `search_docs` — searches checked-in English and Japanese documentation and returns bounded excerpts with canonical URLs. * `get_api_reference` — finds public symbols and returns their maturity, platform status, limitations, docs, and test evidence. * `get_config_schema` — returns the full Murasaki configuration JSON Schema or one property selected by dot path or JSON Pointer. * `doctor` — reads known project metadata and entry paths. It does not import the config, execute project code, or change files. * `list_recipes` / `get_recipe` — returns task-oriented guides backed by the checked-in docs. * `check_compatibility` — checks canonical feature IDs for macOS, Windows, or Linux without upgrading partial or planned work to “supported.” For compatibility planning, call `get_api_reference` without a symbol to list the valid feature IDs, then pass the required IDs to `check_compatibility`. --- # API Routes (https://murasaki.ichi10.com/docs/guides/api-routes) A `src/api//route.ts` file exports one function per HTTP method, served at `/api/`: ```ts title="src/api/hello/route.ts" // GET /api/hello import type { RouteHandler } from 'murasaki' export const GET: RouteHandler = async (request) => { return Response.json({ message: `Hello from Node ${process.version}` }) } export const POST: RouteHandler = async (request) => { const body = await request.json() return Response.json({ received: body }) } ``` `RouteHandler` takes a Web `Request` and a `context` with `params`, and returns a Web `Response` — `Response.json(...)`, `new Response(...)`, status codes, headers, all standard. Export any of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, or `OPTIONS` — whichever ones a route needs. A request for a method the module doesn't export gets a `405`; a request under `/api/` that doesn't match any route gets a `404` rather than falling through to the app's HTML. ## Dynamic segments [#dynamic-segments] A `[name]` folder captures a segment, exposed on `context.params`: ```ts title="src/api/greet/[name]/route.ts" // GET /api/greet/:name import type { RouteHandler } from 'murasaki' export const GET: RouteHandler = async (_request, { params }) => { return Response.json({ greeting: `Hello, ${params.name}!` }) } ``` Catch-all (`[...path]`) and optional catch-all (`[[...path]]`) folders are also supported. Their values are arrays of decoded path segments; an omitted optional catch-all is `undefined`: ```ts title="src/api/files/[[...path]]/route.ts" import type { RouteHandler } from 'murasaki' export const GET: RouteHandler = async (_request, { params }) => { return Response.json({ path: params.path ?? [] }) } ``` ## Calling a route [#calling-a-route] ```ts const res = await fetch('/api/hello') const data = await res.json() ``` Handlers run on the server in both dev (a Vite middleware) and prod (the bundled Node server), so they can reach the filesystem, a database, or secrets. ## API routes vs. server actions [#api-routes-vs-server-actions] Both run on the server — pick by shape: * **API routes** are app-local HTTP endpoints for the renderer and Node main process. Packaged apps protect them with an app-runtime session, so they are not public webhook endpoints. * **Server actions** ([guide](/docs/guides/server-actions)) are typed RPC wired into React 19's form / `useAction` flow — no URL, no `fetch` boilerplate. They coexist in the same app. ## Next [#next] --- # App Menu (https://murasaki.ichi10.com/docs/guides/app-menu) `useAppMenu` declares your app's menu bar — the menu at the top of the screen on macOS, and the window's menu bar on Windows. Like [`useContextMenu`](/docs/guides/context-menu), it's a hook, not markup: items are posted to the Rust side, which builds a real OS menu (`NSMenu` / `HMENU`). It's the Murasaki equivalent of Electron's `Menu.setApplicationMenu` or Tauri's menu API. It's **opt-in**. Without it, your app still gets Murasaki's standard menu (an app menu with About/Quit, plus Edit and Window). Call `useAppMenu` only when you want your own. Because the application menu is process-global, only the primary `main` renderer may replace it, and that renderer needs `menu:application`: ```ts title="murasaki.config.ts" window: { capabilities: [ 'menu:application', 'window:close', 'clipboard:readText', 'clipboard:writeText', ], } ``` Native roles also require their matching capabilities. The list above covers both `{ role: 'close' }` and the complete `{ role: 'editMenu' }` submenu in the example. ## Declaring a menu [#declaring-a-menu] Call it once in your root layout. A menu is either a `{ label, items }` group or a standard `{ role }` submenu; each item is a custom entry, a standard `{ role }`, or a divider: ```tsx title="src/app/layout.tsx" import { useAppMenu, Action } from 'murasaki' useAppMenu([ { label: 'File', items: [ { label: 'New Window', shortcut: 'command,N', action: () => openWindow() }, { separator: true }, { role: 'close' }, ], }, { role: 'editMenu' }, { label: 'View', items: [{ label: 'Reload', shortcut: 'command,R', action: }], }, ]) ``` On macOS the standard app-name menu (About / Hide / Quit) is always added ahead of your menus, so `Cmd+Q` and friends keep working — you don't declare it yourself. ## Roles [#roles] A `role` pulls in a standard item or submenu: it's localized for you and wired to the native behavior (via `NSMenu`'s responder chain on macOS), so you don't supply an `action`. * **Item roles**: `quit`, `close`, `minimize`, `zoom`, `undo`, `redo`, `cut`, `copy`, `paste`, `selectAll`, `reload`. * **Menu roles**: `editMenu` and `windowMenu` — the standard Edit and Window submenus in one line. Use roles for anything standard (especially the Edit items — `copy`/`paste` need native behavior to reach the focused field), and custom entries with an `action` for the rest. Every custom application menu requires `menu:application`. Add the following capabilities for the native roles you include: | Role | Additional capability | | ------------------------------------- | --------------------------------------------------- | | `quit` | `app:quit` | | `close` | `window:close` | | `minimize` | `window:minimize` | | `zoom` | `window:toggleMaximize` | | `cut`, `copy` | `clipboard:writeText` | | `paste` | `clipboard:readText` | | `editMenu` | Both `clipboard:readText` and `clipboard:writeText` | | `windowMenu` | Both `window:minimize` and `window:toggleMaximize` | | `undo`, `redo`, `selectAll`, `reload` | None beyond `menu:application` | If any required capability is missing, Murasaki rejects the entire replacement and keeps the current application menu instead of installing a partially working menu. ## Shape [#shape] ```ts type AppMenu = | { label: string; items: AppMenuItemSpec[] } | { role: 'editMenu' | 'windowMenu' } type AppMenuItemSpec = AppMenuEntry | { role: AppMenuItemRole } | { separator: true } interface AppMenuEntry { label: string shortcut?: string // e.g. "command,N" disabled?: boolean action?: AppMenuAction // a built-in element, or a function items?: AppMenuItemSpec[] // a submenu, instead of an action } ``` Item shape and `action` are the same as [`useContextMenu`](/docs/guides/context-menu#actions): `action` is your own function or a built-in ``, and `shortcut` sets the native accelerator label. Reuse your app's actions with [`createActions`](/docs/guides/context-menu#reusable-actions-with-createactions). Mount `useAppMenu` once in the primary layout. Calls from secondary windows are ignored so a hidden renderer cannot replace process-global native chrome. Not calling it keeps Murasaki's default menu. ## Platform notes [#platform-notes] * **macOS** — a top-of-screen `NSMenu`; the app-name menu is always prepended. * **Windows** — an in-window `HMENU` bar; it inherits the OS's classic styling and doesn't follow dark mode. The `close` role hides a secondary so it can be reopened; closing the primary `main` window and quitting enter the app lifecycle. * **Linux** — no default menu bar yet. ## Next [#next] --- # Auto-update (https://murasaki.ichi10.com/docs/guides/auto-update) Murasaki apps can check for, download, and install updates themselves. There's no update service to run — the manifest is a JSON file next to your release artifacts (GitHub Releases by default, or any static host you control), and the whole trust model is a single Ed25519 keypair you generate once. macOS and Windows only. Linux app packaging doesn't exist in Murasaki yet (see [CLI reference](/docs/building/cli)), so there's nothing to update on Linux either. ## Quick start [#quick-start] 1. Generate a signing key, once, from your project root: ```bash murasaki release --keygen ``` This writes `.murasaki/update-key.pub` (commit it) and `.murasaki/update-key` (gitignored automatically), and prints the private key **once** — save it as a `MURASAKI_UPDATE_KEY` GitHub secret (the command prints the exact `gh secret set` invocation to do that). 2. Turn the updater on: ```ts title="murasaki.config.ts" export default defineConfig({ // ... updater: true, }) ``` `true` is a complete config: the GitHub repo is inferred from `package.json`'s `repository` field, and the public key from `.murasaki/update-key.pub`. Enabling the updater also grants the primary window its internal `app:quit` permission so the verified install can complete the graceful restart. If the update UI lives in a secondary window, grant `app:quit` to that window explicitly. 3. Drop in the button: ```tsx import { UpdateButton } from 'murasaki' export default function Settings() { return } ``` That covers the app side. Publishing signed releases is [below](#publishing-releases). ## How it works [#how-it-works] `useUpdate()`'s check/download/verify logic runs in **Node**, reached from the page over the same local HTTP server that already serves your app (the same mechanism Server Actions and API routes use) — not the native IPC bridge the context menu and app menu use. Only the final "quit and apply" step touches the native launcher, since it has to keep running after your app process exits. ```tsx import { useUpdate } from 'murasaki' const { status, latest, notes, progress, check, download, install, dismiss } = useUpdate() ``` `status` moves through `idle → checking → available → downloading → ready` (or `not-available` / `error`): * **`check()`** fetches the manifest, verifies its signature, and compares versions. * **`download()`** streams the platform-matching asset to disk and verifies its SHA-256. * **`install()`** hands the verified payload to the native launcher and quits the app; the launcher applies the update and relaunches. `useUpdate()` is headless — no rendering, no styling opinions. `` (also from `murasaki`, styled with `@murasakijs/ui`) is the ready-made presentation for it: * renders nothing while `idle` / `checking` / `not-available` / `error`, * **"Update to vX"** once `available` — click to download, * a progress bar while `downloading`, * **"Restart to update"** once `ready` — click to install and relaunch, * checks once, itself, when it mounts. `` renders nothing on `error` — read `update.error` from `useUpdate()` yourself if you want to surface failures in your own UI. Likewise, a manifest's `mandatory` flag is carried on `useUpdate()`'s state, but `` doesn't special-case it (no forced, non-dismissable flow) — build your own UI around `update.mandatory` if you need one. `channel` changes which manifest URL gets resolved (see [Self-hosting the manifest](#self-hosting-the-manifest)). `checkOnStart` and `checkInterval` drive the updater engine's scheduler. The engine checks once at startup by default and then every six hours; overlapping checks are coalesced. `` also checks when it mounts, and that call joins an in-flight scheduled check instead of starting a duplicate. Set `checkOnStart: false` and/or `checkInterval: false` for fully manual checks. In dev, `check()` works — so you can validate a manifest/key pair without a full bundle — but `download()` / `install()` fail fast with `status: 'error'` and `error: 'Updates only apply to a bundled app. Run \`murasaki bundle\` first.'\`: there's no packaged app or launcher binary to apply an update to yet. ## The manifest [#the-manifest] `murasaki release --manifest` writes `dist/latest.json`, always published alongside a detached signature, `dist/latest.json.sig`: ```json title="latest.json" { "version": "1.2.0", "publishedAt": "2026-07-12T09:00:00.000Z", "notes": "markdown release notes", "mandatory": false, "assets": { "darwin-arm64": { "url": "https://.../App-1.2.0-darwin-arm64.app.zip", "sha256": "" }, "darwin-x64": { "url": "https://.../App-1.2.0-darwin-x64.app.zip", "sha256": "" }, "win32-x64": { "url": "https://.../App-1.2.0-setup.exe", "sha256": "" } } } ``` `assets` keys are `-`, matching Node's `process.platform` / `process.arch` on the running app. A missing key for the running platform means "no update available for you," not an error — an app can ship for fewer platforms than a manifest might otherwise cover. `latest.json.sig` is the base64 of a detached Ed25519 signature over `latest.json`'s **exact raw bytes**. The client verifies those bytes before ever parsing them as JSON, never the other way around — there's no JSON canonicalization ambiguity to worry about. ## Publishing releases [#publishing-releases] ```bash murasaki release --keygen [--force] murasaki release --manifest --base-url --version [--notes ] [--mandatory] murasaki release --sign ``` * **`--keygen`** — generates the Ed25519 keypair (see [Quick start](#quick-start)). Refuses to overwrite an existing key unless you pass `--force` — rotating the key invalidates trust for any already-shipped app still holding the old public key. * **`--manifest`** — scans `dist/` for this version's payloads (the macOS `.app.zip` files `murasaki bundle` produces, the Windows `-setup.exe` `murasaki installer` produces), hashes whichever exist, and writes `dist/latest.json`. A missing target is skipped, not an error — only zero payloads found is fatal. * **`--sign`** — signs `dist/latest.json` into `dist/latest.json.sig`. Reads the private key from `$MURASAKI_UPDATE_KEY`, falling back to `.murasaki/update-key`. (`--generate-manifest` still works as a deprecated alias of `--manifest`.) ### GitHub Actions [#github-actions] A release workflow needs to build the update payloads for each platform, generate and sign the manifest, and upload everything to the same GitHub Release — so the default `updater: true` URL (`releases/latest/download/latest.json`) actually resolves them: ```yaml title=".github/workflows/release.yml" name: Release on: push: tags: ['v*'] jobs: macos: runs-on: macos-14 steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: { node-version: 24 } - run: pnpm install - run: pnpm exec murasaki bundle # darwin-arm64 (host arch) - run: pnpm exec murasaki bundle --arch x64 # darwin-x64 (cross-arch) - uses: actions/upload-artifact@v4 with: name: macos-payloads path: dist/bundle/*.app.zip windows: runs-on: windows-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: { node-version: 24 } - run: pnpm install - name: Install NSIS shell: pwsh run: | choco install nsis -y echo "C:\Program Files (x86)\NSIS" >> $env:GITHUB_PATH - run: pnpm exec murasaki installer - uses: actions/upload-artifact@v4 with: name: windows-payload path: dist/*-setup.exe publish: needs: [macos, windows] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: { node-version: 24 } - run: pnpm install - uses: actions/download-artifact@v4 with: { name: macos-payloads, path: dist/bundle } - uses: actions/download-artifact@v4 with: { name: windows-payload, path: dist } - name: Build + sign the manifest env: MURASAKI_UPDATE_KEY: ${{ secrets.MURASAKI_UPDATE_KEY }} run: | VERSION="${GITHUB_REF_NAME#v}" BASE_URL="https://github.com/${{ github.repository }}/releases/download/${GITHUB_REF_NAME}" pnpm exec murasaki release --manifest --base-url "$BASE_URL" --version "$VERSION" pnpm exec murasaki release --sign - uses: softprops/action-gh-release@v2 with: files: | dist/bundle/*.app.zip dist/*-setup.exe dist/latest.json dist/latest.json.sig ``` Add `MURASAKI_UPDATE_KEY` as a repository secret (the value `--keygen` printed). This is independent of code-signing — see [Distribution](/docs/building/distribution#signing--notarization) if you also want a Developer ID-signed, notarized `.dmg`; nothing stops you from adding those steps to the same `macos` job. This example only publishes `win32-x64` — see [Platform support](#platform-support) below for why `win32-arm64` isn't publishable yet. If you build a `win32-arm64` installer too, keep it in a separate output path: the NSIS filename doesn't encode architecture, so an `x64` and an `arm64` build landing in the same `dist/` would overwrite each other before `--manifest` ever runs. ## Security model [#security-model] Signature verification is **mandatory — there is no config option to disable it.** Every `check()` fetches `latest.json` and `latest.json.sig`, verifies the Ed25519 signature over the manifest's raw bytes against the app's public key, and refuses to trust the manifest if that fails. Every `download()` separately re-verifies the payload's SHA-256 against the hash inside the (already-verified) manifest. That combination means an attacker who controls only the file host (a compromised CDN, a MITM'd mirror, a malicious PR to a self-hosted `dist/` bucket) can't push a fake update without also holding the private key — which never leaves `.murasaki/update-key` / your CI secret. Use `murasaki installer --target win32-x64 --sign` for Authenticode in addition to this manifest signature — see [Distribution](/docs/building/distribution#signing--notarization). Without `--sign`, Ed25519 is the update's only authenticity guarantee on Windows. Keep `.murasaki/update-key` out of version control (`--keygen` gitignores it automatically) and treat `MURASAKI_UPDATE_KEY` like any other release-signing secret. ## Platform support [#platform-support] | Platform | Update payload | Status | | --------------- | ------------------------------------ | -------------------------------- | | macOS (arm64) | `-darwin-arm64.app.zip` | Supported | | macOS (x64) | `-darwin-x64.app.zip` | Supported | | Windows (x64) | `--setup.exe` | Supported | | Windows (arm64) | — | Not yet — see below | | Linux | — | No app packaging in Murasaki yet | Windows arm64 has no update path today: the NSIS installer's filename doesn't encode architecture, so `murasaki release --manifest` can only ever produce a `win32-x64` entry. An arm64 Windows app checking for updates just gets `not-available` — it degrades gracefully, it doesn't crash or error. ## Self-hosting the manifest [#self-hosting-the-manifest] Not using GitHub Releases? Point `endpoint` at any URL serving `latest.json` (with `latest.json.sig` alongside it) instead of `repo`: ```ts title="murasaki.config.ts" export default defineConfig({ // ... updater: { endpoint: 'https://updates.example.com/latest.json', }, }) ``` `repo` and `endpoint` are mutually exclusive — `murasaki` throws at build/dev time if both are set. Run `murasaki release --manifest --base-url https://updates.example.com` and upload `dist/latest.json` + `dist/latest.json.sig` + the payloads to wherever that URL resolves. For a non-`stable` channel on GitHub, Murasaki points at `releases/download//latest.json` instead of `releases/latest/download/…` — a moving tag you re-push on every release for that channel (e.g. a `beta` tag for a beta channel). ## Next [#next] --- # Context Menu (https://murasaki.ichi10.com/docs/guides/context-menu) Murasaki's context menu is declared with a hook, not markup, so it lives right next to your state — its `action`s can close over `useState` setters. None of it renders HTML: items are posted to the Rust side, which pops a real OS menu (`NSMenu` on macOS and `HMENU` on Windows). Grant `menu:context` to each renderer that declares one: ```ts title="murasaki.config.ts" window: { capabilities: ['menu:context', 'clipboard:writeText'] } ``` Native roles require their own capability as well. The example grants `clipboard:writeText` for ``; a plain custom callback needs only `menu:context`. ## The whole-window menu [#the-whole-window-menu] Calling `useContextMenu` with no id declares the default menu — it opens anywhere the user right-clicks that isn't claimed by a scoped menu: ```tsx title="src/app/layout.tsx" import { useContextMenu, Action } from 'murasaki' useContextMenu([ { label: 'Reload', shortcut: 'command,R', action: }, { separator: true }, { label: 'Copy', action: }, ]) ``` ## Scoped menus [#scoped-menus] Give the menu an id and tag a region with a matching `` — only that region opens it, and it takes priority over the window default: ```tsx import { useContextMenu, ContextMenuTrigger } from 'murasaki' import { useState } from 'react' function Counter() { const [count, setCount] = useState(0) useContextMenu('card', [ { label: 'Increment', action: () => setCount((n) => n + 1) }, ]) return (
Clicked {count} times
) } ``` `` clones its child by default when it has exactly one element child (no wrapper node); pass `asChild={false}` to force a `display: contents` `` wrapper instead (e.g. with multiple children). ## Item shape [#item-shape] Each entry is either a divider or: ```ts interface ContextMenuEntry { label: string shortcut?: string // e.g. "command,I" — also wired to keydown directly disabled?: boolean action?: ContextMenuAction // a built-in element, or a function items?: ContextMenuItemSpec[] // a submenu, instead of an action } type ContextMenuItemSpec = ContextMenuEntry | { separator: true } ``` `shortcut` both sets the menu's native accelerator label and registers a `keydown` handler, so it fires without the menu being open. ## Actions [#actions] `action` is either your own function, or one of the built-in `` elements — native OS roles (`Copy`, `Paste`, `Cut`, `SelectAll`, `Undo`, `Redo`, `Quit`) or client behaviors (`Reload`, `Navigate`, `Run`). See [Native APIs](/docs/guides/native-apis#built-in-menu-actions) for the full list. Every native context menu requires `menu:context`. Native role actions also require the same permission as their direct native API: | Action | Additional capability | | --------------------------- | -------------------------- | | `Copy`, `Cut` | `clipboard:writeText` | | `Paste` | `clipboard:readText` | | `Quit` | `app:quit` | | `SelectAll`, `Undo`, `Redo` | None beyond `menu:context` | Murasaki rejects the complete menu payload when a required role capability is missing. Client behaviors and your own callbacks need no additional native capability. ## Reusable actions with `createActions` [#reusable-actions-with-createactions] Define your app's actions once — typically in `src/lib/action.ts`, backed by a store so they're callable from anywhere — and drop them into any menu as ``: ```ts title="src/lib/action.ts" import { createActions } from 'murasaki' import { useCounter } from './counter' export const Action = createActions({ increment: () => useCounter.getState().increment(), reset: () => useCounter.getState().reset(), }) ``` ```tsx import { Action } from '@/lib/action' useContextMenu('card', [ { label: 'Increment', shortcut: 'command,I', action: }, { label: 'Reset counter', action: }, ]) ``` The object `createActions` returns also includes the built-ins (`Action.Copy`, `Action.Reload`, …), so a file can import a single `Action` for everything. Only one whole-window (`useContextMenu(items)`, no id) menu should be mounted at a time — mounting a second one logs a dev warning, and the last one mounted wins. ## Next [#next] --- # Deep links and file associations (https://murasaki.ichi10.com/docs/guides/deep-links) Murasaki can register custom URL schemes and document extensions for packaged macOS and Windows applications. Both a cold launch and a request delivered to an already-running app arrive at the same `openRequested()` Node Main hook. ## Configure handlers [#configure-handlers] Declare the schemes and extensions your application owns in `murasaki.config.ts`: ```ts title="murasaki.config.ts" import { defineConfig } from 'murasaki' export default defineConfig({ appId: 'com.example.violet', productName: 'Violet', protocols: [ { scheme: 'violet', name: 'Violet Link' }, ], fileAssociations: [ { extensions: ['vnote'], name: 'Violet Note', description: 'A note created with Violet', role: 'editor', mimeType: 'application/x-violet-note', }, ], }) ``` Use extensions without a leading dot. Schemes and extensions are normalized to lowercase. Murasaki rejects duplicate values and reserved browser/OS schemes (including `blob`, `file`, `http`, `https`, `javascript`, `mailto`, `ms-settings`, `tel`, and `murasaki`), and malformed schemes, extensions, or MIME types at build time. See the [configuration reference](/docs/building/configuration#protocols) for every field and default. ## Handle open requests [#handle-open-requests] Create a Node Main entry and implement `openRequested(context, event)`. Branch on each target's `kind`, not on the platform or transport: ```ts title="src/main.ts" import { defineMain } from 'murasaki/main' export default defineMain({ async ready({ paths }) { // Open databases and initialize services first. console.log('data directory:', paths.data) }, async openRequested(_context, event) { for (const target of event.targets) { if (target.kind === 'url') { const url = new URL(target.url) if (url.hostname !== 'open') continue // Validate and authorize the route before acting on it. console.log('open link:', url.pathname) } else { // Validate ownership, size, and contents before reading the file. console.log('open document:', target.path) } } }, }) ``` Murasaki waits for `ready()` before calling `openRequested()`. This lets you initialize databases and other long-lived resources once, then use the same handler for cold-start and running-app requests. ## Event reference [#event-reference] ```ts type OpenTarget = | { kind: 'url'; url: string; scheme: string } | { kind: 'file'; path: string } interface OpenRequestEvent { activation: 'cold-start' | 'second-instance' | 'os-event' transport: 'argv' | 'open-url' | 'open-file' targets: OpenTarget[] cwd?: string } ``` | Field | Meaning | | ------------ | ----------------------------------------------------------------------------------------------------------------------- | | `activation` | Whether this request accompanied the first launch, was forwarded from a second process, or came from a native OS event. | | `transport` | Native delivery mechanism. It is useful for diagnostics; normal application logic should use `target.kind`. | | `targets` | Only URLs and files matching configured schemes/extensions. An event can contain more than one file. | | `cwd` | Working directory for an argv-based launch. Absent for native OS events. | On a second executable launch, `secondInstance()` still receives the raw `argv`/`cwd`, while `openRequested()` receives the recognized URL/file targets. Do not perform the same open action in both hooks. Use `secondInstance()` for generic activation behavior and `openRequested()` for semantic link/file handling. ## Packaging behavior [#packaging-behavior] | Artifact | Registration and delivery | | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | macOS `.app` | `murasaki bundle` writes URL and document metadata into the app's `Info.plist`. Launch Services delivers URL/file opens, including opens sent to an already-running app. The `.dmg` carries this `.app`; move the app to Applications before final testing. | | Windows NSIS `.exe` | The installer writes per-user or per-machine protocol and file-association registration according to `installer.windows.installMode`. A launched URL/file is normalized from argv. | | Windows MSI | The MSI writes per-machine protocol and file-association registration. A launched URL/file is normalized from argv. | | Windows portable archive | No registry entries are installed. `protocols` and `fileAssociations` do not make the archive an OS handler. | | Linux | Protocol and MIME registration is not implemented yet. Do not advertise Linux deep-link/file-association support. | | `murasaki dev` | Does not install system handler metadata. Test the installed/package artifact for OS integration. | Windows registration makes the app an available handler; it does not silently override the user's current default application. Keep `appId` stable between releases so installer ownership and association identifiers remain stable. ## Test packaged handlers [#test-packaged-handlers] After installing or moving the packaged app to Applications, test both a cold launch and an already-running app. ```bash title="macOS" open 'violet://open/note-42' open -a Violet ./example.vnote ``` ```powershell title="Windows PowerShell" Start-Process 'violet://open/note-42' Start-Process '.\example.vnote' ``` Also test the real NSIS/MSI uninstall and upgrade paths on a clean machine. Launching the executable manually with an argument is useful for handler code, but it does not verify that the OS registration was installed correctly. ## Security [#security] Every URL, file path, and launch argument is untrusted input. Any local process can launch your executable with a forged URL or path; a custom scheme does not prove who sent it. * Allowlist expected URL hosts, paths, actions, and parameter shapes. * Do not put bearer tokens or long-lived credentials in a deep-link URL. For sign-in callbacks, use a one-time code plus `state` and PKCE. * Redact query strings, fragments, and sensitive paths from logs and crash reports. * Check file size, permissions, format, and content before parsing. An expected extension is not proof of a safe file. * Account for symlinks and files changing between validation and use. * Keep handling in Node Main. Send only validated, minimal data to the renderer. ## Next [#next] --- # Metadata (https://murasaki.ichi10.com/docs/guides/metadata) A page or layout can export a static `metadata` object, or an async `generateMetadata()` for values that depend on route params: ```tsx title="src/app/blog/[slug]/page.tsx" import type { Metadata } from 'murasaki' export const metadata: Metadata = { title: 'Blog', description: 'Posts about murasaki.', } ``` ```tsx import type { GenerateMetadata } from 'murasaki' export const generateMetadata: GenerateMetadata = async ({ params }) => { return { title: `Post: ${params.slug}` } } ``` `generateMetadata` receives `{ params }` — the same dynamic-segment params `useParams()` reads on the client — so it can fetch a title from a CMS, an API route, or anything else async. ## The `Metadata` shape [#the-metadata-shape] ```ts interface Metadata { title?: string description?: string icons?: { icon?: string shortcut?: string apple?: string } openGraph?: { title?: string description?: string images?: string[] } } ``` Of `icons`, only `icon` is actually rendered to the document today (as a ``) — `shortcut` and `apple` are typed for forward compatibility but aren't applied yet. Don't rely on them showing up in the DOM. ## Layouts set defaults, pages override them [#layouts-set-defaults-pages-override-them] A layout's `metadata` (or its page's) merges root → leaf: each ancestor layout's fields apply first, then the matched page's on top — so a layout can set a fallback `description` or `openGraph.images` that every page under it inherits unless it sets its own: ```tsx title="src/app/layout.tsx" export const metadata: Metadata = { title: 'My App', openGraph: { images: ['/og-default.png'] }, } ``` ```tsx title="src/app/blog/[slug]/page.tsx" export const metadata: Metadata = { title: 'My Post', // openGraph.images still resolves to '/og-default.png' from the layout — // only fields a page actually sets override the ones above it. } ``` The merge is per-field, not whole-object: `icons` and `openGraph` are merged key by key (a page setting `openGraph.title` doesn't drop the layout's `openGraph.images`), while top-level fields like `title` simply overwrite. A page's `generateMetadata` (if present) is awaited and merged on top of that whole static chain last — so it can override any field the static exports set, including a layout's. ## How it's applied [#how-its-applied] On every navigation, `` resolves this whole chain and applies it to the document: * `document.title` (only touched when a title actually resolves — never blanked out) * `` * Open Graph `` tags (falling back to `title` / `description` when `openGraph` fields are omitted) * ``, from `icons.icon` Tags Murasaki previously added are removed before the new ones are applied, so navigating between routes replaces them cleanly instead of accumulating stale tags from the last route. `applyMetadata` (the function doing this, exported if you ever need to call it yourself) is a no-op outside a DOM environment. This sets the **document's** title/meta tags, not the native window's title bar — the window title comes from `window.title` in `murasaki.config.ts` (see [Configuration](/docs/building/configuration)) and is set once at launch. ## Next [#next] --- # Middleware (https://murasaki.ichi10.com/docs/guides/middleware) `src/middleware.ts` may export a default function that runs before every navigation — initial mount, `push`/`replace`, and browser back/forward — and can redirect it: ```ts title="src/middleware.ts" import type { Middleware } from 'murasaki' const middleware: Middleware = ({ pathname }) => { if (pathname === '/admin') return { redirect: '/' } } export default middleware ``` `` (mounted for you by the scaffold's entry) picks this up automatically from the virtual route table — there's nothing to register by hand, and nothing happens if the file doesn't exist. ## The signature [#the-signature] ```ts export interface MiddlewareContext { pathname: string } export type MiddlewareResult = { redirect: string } | void | undefined export type Middleware = ( ctx: MiddlewareContext, ) => MiddlewareResult | Promise ``` `ctx` is deliberately small — just the pathname being navigated to. There's no `request`/`response`, no headers, no cookies: this runs entirely client-side, inside the same WebView as your routes, not an edge or server runtime. To return early without redirecting, return nothing (`undefined`). It may be async — `` awaits it before rendering the matched route, rendering nothing in between so the guarded route never flashes on screen. ## Guarding a route [#guarding-a-route] The most common shape is an auth check that reads some client-side state (a store, `localStorage`, a cookie via `document.cookie`) and redirects unauthenticated visitors away from a protected path: ```ts title="src/middleware.ts" import type { Middleware } from 'murasaki' import { useAuth } from '@/lib/auth' const PROTECTED = ['/settings', '/billing'] const middleware: Middleware = ({ pathname }) => { const isProtected = PROTECTED.some((p) => pathname.startsWith(p)) if (isProtected && !useAuth.getState().isSignedIn) { return { redirect: '/login' } } } export default middleware ``` ## Side effects without redirecting [#side-effects-without-redirecting] `middleware` doesn't have to redirect anything — returning `void` lets the navigation through as-is, so it's also a convenient place to run a side effect on every navigation (analytics page views, for example): ```ts title="src/middleware.ts" import type { Middleware } from 'murasaki' import { trackPageview } from '@/lib/analytics' const middleware: Middleware = ({ pathname }) => { trackPageview(pathname) } export default middleware ``` A redirect loop (5+ consecutive redirects) is caught automatically: Murasaki logs a warning and renders the current path as-is rather than hanging. There's no `matcher`/`config` export to scope which paths run middleware — unlike Next.js, `middleware` always runs on every navigation, and you branch on `pathname` yourself inside the function (as in the examples above). This is Murasaki's client-side equivalent of Next.js middleware — there's no edge runtime involved, since it runs in the same WebView as your routes. ## Next [#next] --- # Native APIs (https://murasaki.ichi10.com/docs/guides/native-apis) Murasaki's native window, menus, and OS integration are powered by [`@murasakijs/native`](https://www.npmjs.com/package/@murasakijs/native) — a self-authored Rust binding (tao/wry/muda) — so you never write Rust. This page covers what's actually reachable **from your React app** today. ## Window & native menu bar [#window--native-menu-bar] Window shape—size, title, route, visibility, permissions, and macOS vibrancy—is declared in `murasaki.config.ts`. `window` is the primary `main` window; `windows` contains labeled secondary windows. See [Windows & permissions](/docs/guides/windows) for the full model. On macOS, the standard App/Edit/Window menu bar (About, Services, Hide, Quit, Edit, Window) is generated for you and localized from `config.locales` — no code required. ## Context menu [#context-menu] The right-click context menu is declared with `useContextMenu` — a hook, not markup — and posts to the Rust side, which pops a real `NSMenu` / `HMENU` on macOS and Windows. See the dedicated [Context Menu](/docs/guides/context-menu) guide. ## Dialogs, clipboard, notifications, shell, and window controls [#dialogs-clipboard-notifications-shell-and-window-controls] Import renderer-safe native capabilities from `murasaki/native`. Every call is Promise-based and uses a request-correlated IPC message handled directly by the Rust host: ```tsx 'use client' import { app, appWindow, clipboard, dialog, notification, shell, systemPermission, tray, windows } from 'murasaki/native' const files = await dialog.openFile({ multiple: true, filters: [{ name: 'Images', extensions: ['png', 'jpg'] }], }) await clipboard.writeText(files.join('\n')) await notification.show({ title: 'Files selected', body: `${files.length} files` }) await appWindow.setTitle('Import complete') console.log(await appWindow.getLabel()) await windows.open('preview') await shell.showItemInFolder(files[0]) ``` | API | Operations | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | `app` | `quit` (graceful application shutdown) | | `dialog` | `openFile`, `openDirectory`, `saveFile` | | `clipboard` | `readText`, `writeText` | | `notification` | `show` | | `shell` | `openExternal` (safe URL schemes only), `showItemInFolder` | | `systemPermission` | macOS `status`, `request` for camera, microphone, screen recording, and accessibility | | `appWindow` | `getLabel`, `setTitle`, `setSize`, `minimize`, `toggleMaximize`, `show`, `hide`, `focus`, `close`, `setAlwaysOnTop`, and state queries | | `windows` | `open`, `list`, `show`, `hide`, `focus`, `close` for declared labels | | `tray` | `create`, `remove`, `setTooltip`, `setIcon`, `setMenu`, `onClick`, `onMenuItem` | `app.quit()` and the root `quit()` helper both require `app:quit`; an unprivileged secondary renderer cannot terminate the whole application. **0.50 migration:** `quit()` used to post an unpermissioned raw IPC message. Existing non-updater apps that call it must add `app:quit` to that window. Enabling the built-in updater grants it to the primary window only so the verified install/restart handshake remains backwards-compatible. These APIs run in trusted renderer code, not directly in `src/main.ts`. The bridge accepts messages only from the app's exact origin and exposes a fixed, default-deny command allowlist. Grant only the commands each renderer uses through `window.capabilities` / `windows[label].capabilities`. Argument and path/URL scopes are still planned, so do not load untrusted remote content into a privileged app window. ## Tray icon [#tray-icon] Create one system tray icon from a client component. It uses `config.icon` by default, or an explicit 8-bit RGB/RGBA PNG. Every operation is independently permissioned: ```ts import { tray } from 'murasaki/native' await tray.create({ tooltip: 'Sync is running', template: true, // macOS status-item menu / Windows system-tray menu menu: [ { id: 'open', label: 'Open Murasaki' }, { separator: true }, { id: 'quit', label: 'Quit' }, ], menuOnLeftClick: navigator.userAgent.includes('Mac OS X'), menuOnRightClick: true, }) const unsubscribe = tray.onClick(({ button, double }) => { console.log({ button, double }) }) const unsubscribeMenu = tray.onMenuItem(async (id) => { if (id === 'open') await appWindow.show() if (id === 'quit') await app.quit() }) await tray.setTooltip('Sync complete') await tray.setIcon('/absolute/path/to/synced.png') await tray.setMenu([{ id: 'open', label: 'Open Murasaki' }]) // Later: unsubscribe(); unsubscribeMenu(); await tray.remove() ``` ```ts title="murasaki.config.ts" export default defineConfig({ // ... capabilities: [ 'tray:create', 'tray:setTooltip', 'tray:setIcon', 'tray:setMenu', 'tray:remove', 'window:show', 'app:quit', ], }) ``` `template` is macOS-specific. Creating a second tray icon replaces the first. Tray menu items are event-driven: every clickable item needs a unique `id`, and privileged actions such as quitting still pass through their own native capability. Closing the renderer that created the process-wide icon removes it. Global shortcuts and Linux tray support are not available yet. ## System permissions [#system-permissions] OS consent is separate from Murasaki's renderer capability allowlist. For a packaged macOS app, declare purpose text and optional launch-time prompts in config. Purpose strings are written into `Info.plist`; missing camera or microphone descriptions are rejected before a prompt can crash the app: ```ts title="murasaki.config.ts" export default defineConfig({ // ... systemPermissions: { macOS: { camera: { usageDescription: 'Use your camera for video calls.', requestOnLaunch: true, }, microphone: { usageDescription: 'Use your microphone for voice calls.', }, screenRecording: { requestOnLaunch: false }, accessibility: { requestOnLaunch: false }, }, }, capabilities: [ 'systemPermission:status', 'systemPermission:request', ], }) ``` For contextual prompts from a client component: ```ts const status = await systemPermission.status('microphone') if (status === 'notDetermined') { await systemPermission.request('microphone') } ``` Camera and microphone prompts complete asynchronously at the OS level, so `request()` may initially return `notDetermined`; query `status()` again when the app regains focus before enabling the protected feature. `requestOnLaunch` applies to packaged macOS apps. Test TCC behavior from a packaged build because development runs under the terminal/Node host identity. On Windows, unpackaged desktop camera/microphone consent is requested by the device API when it is used, not through a generic application-start prompt; Murasaki therefore reports `unsupported` instead of pretending permission was granted. Screen recording and accessibility return `notGranted` when macOS cannot distinguish first-use from denial. ## Built-in menu actions [#built-in-menu-actions] An item's `action` can be one of the built-in `` elements instead of a function — these run a native role (handled by the OS itself) or a small client-side behavior: | Action | Behavior | | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | ``, ``, ``, ``, ``, `` | Native OS edit roles | | `` | Native quit role | | `` | Reloads the window (`location.reload()`) | | `` | Client-side navigation via the router | | `` | Runs a plain function (same as passing the function directly) | ## Auto-update [#auto-update] `useUpdate()` checks for, downloads, and installs updates — its check/download logic runs in Node and is reached over the same local HTTP server that serves the rest of the app (like Server Actions and API routes), not the IPC bridge the context menu and app menu use. `` (also from `murasaki`, styled with `@murasakijs/ui`) is a ready-made button that wraps it: ```tsx import { useUpdate } from 'murasaki' const { status, latest, check, download, install } = useUpdate() ``` `status` moves through `idle → checking → available → downloading → ready` (or `not-available` / `error`). Setup is two commands — see the dedicated [Auto-update](/docs/guides/auto-update) guide for the manifest format, the security model, and a GitHub Actions release workflow. Global shortcuts and argument/path-scoped capability rules are not public yet. Check the [platform feature status](/docs/core-concepts/platform-feature-status) before designing around them. ## Next [#next] --- # Node Main (https://murasaki.ichi10.com/docs/guides/node-main) Use Node Main for work that must live as long as the desktop application: database connections, filesystem watchers, sockets, worker pools, background queues, and cleanup that must finish before the native host exits. ## Define the lifecycle [#define-the-lifecycle] Create `src/main.ts` and default-export `defineMain()`: ```ts title="src/main.ts" import { defineMain } from 'murasaki/main' import { watch, type FSWatcher } from 'node:fs' let watcher: FSWatcher | undefined export default defineMain({ async ready({ paths, signal, isPackaged }) { watcher = watch(paths.data, { recursive: true }, (_event, filename) => { console.log('changed', filename) }) signal.addEventListener('abort', () => watcher?.close(), { once: true }) console.log(isPackaged ? 'packaged main ready' : 'development main ready') }, async beforeQuit({ reason }) { // Return false only when a normal close really must be cancelled. console.log('quit requested:', reason) }, async secondInstance(_context, { argv, cwd }) { // Packaged macOS/Windows: another launch was redirected here. console.log('second launch:', { argv, cwd }) }, async openRequested(_context, event) { // Registered URL schemes and file types arrive here after ready(). console.log('open targets:', event.targets) }, async shutdown() { watcher?.close() watcher = undefined }, }) ``` Murasaki detects `src/main.ts` by default. Configure another entry or cleanup deadline in `murasaki.config.ts`: ```ts title="murasaki.config.ts" export default defineConfig({ appId: 'com.example.notes', productName: 'Notes', main: { entry: 'src/backend/main.ts', shutdownTimeoutMs: 15_000, }, }) ``` Set `main: false` to disable discovery even when `src/main.ts` exists. ## Context reference [#context-reference] | Field | Meaning | | --------------------------------- | ---------------------------------------------------------------------- | | `appId`, `productName`, `version` | Resolved application identity | | `isPackaged` | `false` under `murasaki dev`, `true` in a bundle | | `platform`, `arch` | Runtime target (`process.platform` / `process.arch`) | | `projectRoot` | Project root in dev; packaged resource working directory in production | | `resourcesPath` | Read-only application resources location | | `paths.data` | Durable app data | | `paths.cache` | Re-creatable cache data | | `paths.logs` | Log files | | `paths.temp` | Temporary staging data | | `signal` | Aborts after `beforeQuit` and before `shutdown` | Quit reasons are `window-close`, `app-quit`, `signal`, `restart`, `dev-reload`, and `startup-failure`. See the [process model](/docs/core-concepts/process-model) for ordering and cancellation behavior. ### `secondInstance(context, event)` [#secondinstancecontext-event] Packaged macOS and Windows apps use a per-user, per-`appId` lock. When another launch is redirected to the primary app, Murasaki focuses its window and calls `secondInstance()` with: | Field | Meaning | | ------------ | -------------------------------------------------------------------------------------------- | | `event.argv` | Arguments passed to the second launcher, including URL/file arguments supplied by the caller | | `event.cwd` | Working directory of the second launch | Validate every argument before use. `secondInstance()` is the low-level process activation hook: it receives raw launch arguments whether or not they match a registered URL scheme or file type. It is not currently delivered by `murasaki dev` or Linux. ### `openRequested(context, event)` [#openrequestedcontext-event] Use `openRequested()` for configured URL schemes and file associations. It normalizes cold-start argv, second-instance argv, and native URL/file events into typed URL/file targets, and runs only after `ready()` has completed. On a second launch with a recognized target, both `secondInstance()` and `openRequested()` run. Do not open the same item in both hooks: keep generic activation behavior in `secondInstance()` and semantic URL/file handling in `openRequested()`. See [Deep links and file associations](/docs/guides/deep-links) for the event shape, configuration, packaging behavior, and security requirements. ## Call Node from the renderer with `'use main'` [#call-node-from-the-renderer-with-use-main] A top-level `'use main'` directive exposes the module's exported functions as typed calls to the same Node module graph as `src/main.ts`. ```ts title="src/backend/checksum.ts" 'use main' import { createHash } from 'node:crypto' export async function sha256(text: string): Promise { if (typeof text !== 'string' || text.length > 1_000_000) { throw new TypeError('text must be a string no larger than 1 MB') } return createHash('sha256').update(text).digest('hex') } ``` Import it normally from a client component. The renderer bundle receives a fetch stub; the Node implementation does not ship to the WebView. ```tsx title="src/app/page.tsx" 'use client' import { useState } from 'react' import { sha256 } from '../backend/checksum' export default function Page() { const [digest, setDigest] = useState('') return ( ) } ``` ## Stream Main events to the renderer [#stream-main-events-to-the-renderer] Use the typed event channel for connection status, progress, device events, watcher output, or other values that originate in long-lived Node work: ```ts title="src/main.ts" import { defineMain, emitMainEvent } from 'murasaki/main' export default defineMain({ async ready({ signal }) { const timer = setInterval(() => { emitMainEvent('relay.status', { connected: true, at: new Date() }) }, 1_000) signal.addEventListener('abort', () => clearInterval(timer), { once: true }) }, }) ``` Subscribe in a client component: ```tsx 'use client' import { useEffect, useState } from 'react' import { subscribeMainEvent } from 'murasaki/main-client' export function RelayStatus() { const [connected, setConnected] = useState(false) useEffect(() => subscribeMainEvent<{ connected: boolean; at: Date }>( 'relay.status', (event) => setConnected(event.connected), ), []) return {connected ? 'Connected' : 'Disconnected'} } ``` Events use the same rich-value wire codec as `'use main'` and travel over an authenticated app-local SSE connection. EventSource reconnects after a temporary disconnect, but events are live-only: there is no replay buffer or durable queue. Store durable state in Node and expose a `'use main'` snapshot function when a subscriber must recover missed values. `'use main'` is an experimental RPC boundary, not an authorization boundary. Treat every argument as untrusted renderer input: validate types, normalize filesystem paths, and authorize access to application data inside the Node function. ## Wire values and limits [#wire-values-and-limits] Calls use Murasaki's versioned wire codec. It supports primitives, `undefined`, `bigint`, `Date`, `Map`, `Set`, cyclic plain objects/arrays, `ArrayBuffer`, typed arrays, `Blob`, `File`, `FormData`, and structured `Error` values. * Maximum request or response payload: **32 MiB**. * Functions, symbols, and instances with custom prototypes are rejected. * Calls are request/response today; use Main events for live push. Returning `AsyncIterable` directly from a `'use main'` call is not implemented. * Export named `function`, `async function`, `const`, `let`, or `var` values. At runtime the selected export must be callable. For large files, do not send the bytes through RPC. Pass a validated app-owned identifier or path and stream the data entirely within Node. ## `'use main'` vs `'use server'` vs API routes [#use-main-vs-use-server-vs-api-routes] | Choose | When | | --------------------- | ----------------------------------------------------------------- | | `'use main'` | Typed imperative calls into the long-lived application backend | | `'use server'` | React action/form-shaped mutations using `ActionState` | | `src/api/**/route.ts` | Request/Response semantics, streaming bodies, HTTP method routing | All three execute in the app-local Node runtime. API routes are not a public network server; Murasaki protects them with the same app runtime session as the internal RPC endpoints. ## Current limits [#current-limits] Node Main does not provide an imperative native-window creation API; declared windows are managed from trusted renderers through `murasaki/native`. Tray menus are available through that renderer API, while Node Main itself still has no direct tray-menu or global-shortcut API. Packaged macOS/Windows single-instance delivery is available through `secondInstance()`; configured URL schemes and file associations are delivered through `openRequested()`. Windows portable archives, Linux, and `murasaki dev` do not install system association metadata. Renderer-safe dialogs, clipboard, notifications, shell, and basic window commands are separately available from `murasaki/native` behind the `capabilities` allowlist. Node packages may also require explicit `bundle.external` / `bundle.resources` entries when they use computed imports, dynamic native addons, or runtime-discovered assets; always test the installed artifact on a clean machine, not only `murasaki dev`. ## Next [#next] --- # Routing (https://murasaki.ichi10.com/docs/guides/routing) Every `src/app/**/page.tsx` becomes a route automatically — no router config to write. A Vite plugin scans `src/app` and emits a virtual route table that `` (mounted for you by the scaffold's entry) matches on every navigation. ```txt src/app/ ├─ layout.tsx # wraps every route ├─ page.tsx # "/" ├─ about/ │ └─ page.tsx # "/about" └─ blog/ └─ [slug]/ └─ page.tsx # "/blog/:slug" ``` ## Layouts [#layouts] `layout.tsx` wraps every route beneath it (nested layouts compose root → leaf). Your **root layout** should wrap its children in `` — a full-window frame component from `murasaki`: ```tsx title="src/app/layout.tsx" import type { ReactNode } from 'react' import { App, useContextMenu } from 'murasaki' export default function Layout({ children }: { children: ReactNode }) { useContextMenu([{ label: 'Reload', shortcut: 'command,R', action: () => location.reload() }]) return {children} } ``` ## Dynamic segments [#dynamic-segments] A `[param]` folder captures a URL segment; read it with `useParams()`: ```tsx title="src/app/blog/[slug]/page.tsx" import { useParams } from 'murasaki' export default function BlogPost() { const { slug } = useParams() return

Post: {slug}

} ``` Static segments always win over dynamic ones, and a more-specific match (more literal segments) wins over a less-specific one — the same precedence Next.js uses. A `(group)` folder — parens, no brackets — organizes routes without adding a URL segment. ## Loading, error, and not-found boundaries [#loading-error-and-not-found-boundaries] Drop these files next to (or above) a `page.tsx`: * **`loading.tsx`** — shown while the page suspends (wrapped in a ``). * **`error.tsx`** — an error boundary; exports a component receiving `{ error, reset }`. Catches render errors thrown under that route. * **`not-found.tsx`** — rendered when no route matches; the nearest one in the URL's ancestor chain wins. Each is scoped to its own subtree, the same as Next.js App Router. ## Navigation [#navigation] Use `` for client-side navigation (no full reload): ```tsx import { Link } from 'murasaki' About ``` `useRouter()` gives you `push` / `replace` / `back` / `pathname` imperatively, and `usePathname()` reads the current path. A plain `` to an off-origin URL opens in the user's **default system browser**, not inside the app window — you don't need to special-case external links. ## Next [#next] --- # Server Actions (https://murasaki.ichi10.com/docs/guides/server-actions) React 19-style server actions — same shape as `useActionState`. A `'use server'` function actually runs in **Node**, not the browser. ## Define an action [#define-an-action] ```ts title="src/actions.ts" 'use server' import { defineAction } from 'murasaki' import type { ActionState } from 'murasaki' export const greet = defineAction( async (_prev: ActionState, formData: FormData): Promise> => { const name = formData.get('name') return { data: `Hello, ${name}!`, error: null, isPending: false } }, ) ``` `defineAction` is a typed passthrough — it exists so the `'use server'` semantics carry through TypeScript. The actual code-splitting happens in a Vite plugin that detects the directive. ## Call it from a page [#call-it-from-a-page] ```tsx title="src/app/page.tsx" import { useAction } from 'murasaki' import { greet } from '../actions' export default function Home() { const [state, run, isPending] = useAction(greet, { data: null, error: null, isPending: false, }) return (
{state.data &&

{state.data}

}
) } ``` `useAction` wraps React 19's `useActionState` directly, so `[state, run, isPending]` is exactly the shape you already know from Next.js. To invoke an action outside a `
` (e.g. from a button's `onClick`), call it directly — `callAction(fn, ...args)` is a thin passthrough for that case. ## How it runs [#how-it-runs] The Vite plugin splits a `'use server'` module in two: the client bundle gets a typed `fetch` stub (`fetch('/__murasaki/action/…')`), and the function body stays server-side. * **Dev** — a Vite middleware receives the stub's request and invokes the real function via `ssrLoadModule`. * **Prod** — `murasaki bundle` compiles a self-contained action registry (`dist/server`), and the bundled Node child server (spawned by the native launcher) serves the same `/__murasaki/action/…` requests against it. Either way, the action itself never ships to the client — only the stub does. A `'use server'` file can live anywhere under `src/` (`src/actions.ts` is the common convention) — it isn't tied to `src/app/` or `src/api/`. ## Next [#next] --- # Styling (https://murasaki.ichi10.com/docs/guides/styling) The scaffold ships Tailwind CSS and [`@murasakijs/ui`](https://www.npmjs.com/package/@murasakijs/ui) — a shadcn/ui-style component library built on Radix primitives, styled with the same `cn()` + `class-variance-authority` conventions shadcn/ui users already know. Every component it ships is documented, live, in the [Components](/docs/components) section — this guide only covers how the library is wired into a scaffolded app. ## Setup [#setup] Import the stylesheet once (the scaffold already does this in the root layout), and extend your Tailwind config with the shared preset: ```tsx title="src/app/layout.tsx" import '@murasakijs/ui/styles.css' import './globals.css' ``` ```ts title="tailwind.config.ts" import type { Config } from 'tailwindcss' export default { presets: [require('@murasakijs/ui/tailwind-preset')], content: [ './src/**/*.{ts,tsx}', './node_modules/@murasakijs/ui/dist/**/*.js', ], darkMode: ['selector', '[data-theme="dark"]'], } satisfies Config ``` ## Components [#components] Import components directly from `@murasakijs/ui`: ```tsx import { Button, Card, CardHeader, CardTitle, CardContent } from '@murasakijs/ui' function Example() { return ( Try it out ) } ``` The package currently ships: * **Layout & content** — `Card`, `Separator`, `Accordion`, `Tabs`, `ScrollArea`, `Table`, `Skeleton` * **Forms & inputs** — `Button`, `Input`, `Textarea`, `Label`, `Checkbox`, `Switch`, `RadioGroup`, `Select` * **Overlays** — `Dialog`, `Sheet`, `Popover`, `Tooltip`, `DropdownMenu`, `Command` (a command palette) * **Feedback** — `Alert`, `Badge`, `Progress`, `Toast` / `Toaster` (+ `useToast`) * **Misc** — `Avatar`, `cn` (the `clsx` + `tailwind-merge` class helper) `Button` supports `variant` (`default` / `secondary` / `outline` / `ghost` / `destructive` / `link`) and `size` (`default` / `sm` / `lg` / `icon`) props, plus `asChild` to render as a different element (Radix `Slot`) — the same pattern as the rest of the library. Every one of these is documented on its own page under [Components](/docs/components), with a live, interactive preview and the full prop table — not just the short list above. `@murasakijs/ui` is not required — it's Tailwind + Radix under the hood, so you can swap in your own components or another library instead. ## Next [#next] --- # Windows & permissions (https://murasaki.ichi10.com/docs/guides/windows) Murasaki creates application windows from `murasaki.config.ts`. The primary window is the existing `window` field and always has the label `main`. Additional entries live in `windows`, keyed by a stable label: ```ts title="murasaki.config.ts" import { defineConfig } from 'murasaki' export default defineConfig({ appId: 'com.example.notes', productName: 'Notes', capabilities: ['clipboard:readText'], // fallback for main only window: { route: '/', width: 1100, height: 760, capabilities: ['window:getLabel', 'window:open', 'window:list', 'window:manage'], }, windows: { settings: { route: '/settings', title: 'Settings', width: 720, height: 560, // visible defaults to false for secondary windows capabilities: [], }, preview: { route: '/preview', visible: false, capabilities: ['clipboard:readText'], }, }, }) ``` All declared windows are created by the host. Secondary windows start hidden unless `visible: true` is set. A secondary window with no `capabilities` is deny-all; it never inherits the top-level list. For backwards compatibility, the primary window resolves permissions as `window.capabilities ?? capabilities ?? []`. Open a hidden secondary from a visible renderer, normally `main`. WebKit and WebView2 may either run that renderer before it is shown or defer its JavaScript until after it becomes visible, so do not depend on either timing and do not make the hidden renderer responsible for calling `windows.open()` on itself. Native visibility is applied by the OS event loop; when code must observe the post-open state through `windows.list()`, wait for `visible` to become `true` instead of assuming the first read is synchronous with the compositor. The Windows backend `console` option is primary-only because it controls the application backend console, not an individual native window. Putting `console` on a secondary declaration is rejected during configuration resolution. Labels must be 1–64 characters, start with a letter or number, and contain only letters, numbers, `.`, `_`, or `-`. `main` is reserved. A `route` must be a same-origin path beginning with `/`; full URLs, protocol-relative URLs, and backslashes are rejected during configuration resolution. ## Identify and manage windows [#identify-and-manage-windows] Use `appWindow` for the renderer's own native window and `windows` for a declared window by label: ```tsx 'use client' import { appWindow, windows } from 'murasaki/native' const current = await appWindow.getLabel() await windows.open('settings') await windows.focus('settings') const all = await windows.list() const settings = all.find((item) => item.label === 'settings') console.log({ current, settings }) await windows.hide('settings') ``` `windows.list()` returns `WindowInfo[]`: ```ts interface WindowInfo { label: string primary: boolean visible: boolean focused: boolean minimized: boolean maximized: boolean } ``` | API | Capability | Behavior | | ----------------------------------------------------------------------- | ----------------- | ---------------------------------------------------- | | `appWindow.getLabel()` | `window:getLabel` | Returns the current renderer's label. | | `windows.open(label)` | `window:open` | Shows, restores, and focuses a live declared window. | | `windows.list()` | `window:list` | Returns state for all live declared windows. | | `windows.show(label)` / `hide(label)` / `focus(label)` / `close(label)` | `window:manage` | Manages another declared window. | Only labels declared in configuration are available. These APIs do not create an arbitrary route or a new runtime window. The OS close control and `appWindow.close()` hide a secondary window, so `windows.open(label)` can show that same declared window again. In contrast, `windows.close(label)` explicitly destroys a secondary target; a destroyed secondary cannot be recreated in the current release. Closing `main` remains an application quit request. ## Primary close and application lifecycle [#primary-close-and-application-lifecycle] Using the OS close control or `appWindow.close()` on a secondary hides that window and affects no other window. Closing `main` is an application quit request: it runs `beforeQuit()`, can be cancelled there, and then runs bounded `shutdown()` before the native host exits. Murasaki does not currently expose a separate per-window close hook. Use `windows.close(label)` only when the secondary should be destroyed for the rest of the process lifetime. ## Per-window least privilege [#per-window-least-privilege] Permissions belong to the renderer that makes the call. A settings window that only edits React state can use `capabilities: []`; a preview that reads the clipboard can receive only `clipboard:readText`. Management permissions should normally live on the trusted primary renderer: ```ts title="murasaki.config.ts" export default defineConfig({ appId: 'com.example.notes', productName: 'Notes', window: { route: '/', capabilities: ['window:open', 'window:list', 'window:manage'], }, windows: { settings: { route: '/settings', capabilities: [] }, importer: { route: '/import', capabilities: ['dialog:openFile'] }, }, }) ``` Per-window capability lists limit which native command names a compromised renderer can call. They do not scope arguments: `window:manage` is broad, and `dialog:openFile` cannot yet be limited to one directory. Do not load remote or user-authored executable content into any privileged renderer, and validate app-level intent before broad operations. This is a native-command boundary, not a complete renderer sandbox. Declared windows currently share the same application origin and authenticated local Node session, so Server Actions, `'use main'`, API routes, and updater HTTP endpoints are not separated by these lists. Authenticate and authorize application operations inside those handlers; do not treat a deny-all window as safe for untrusted executable content. ## Next [#next] --- # Components (https://murasaki.ichi10.com/docs/components) [`@murasakijs/ui`](https://www.npmjs.com/package/@murasakijs/ui) is the component library the scaffold installs by default — built on Radix primitives, styled with Tailwind CSS and `class-variance-authority`, using the same `cn()` conventions shadcn/ui users already know. Every component on this page is the real, published component, rendered live. See the [Styling](/docs/guides/styling) guide for how the library is wired into a scaffolded app (stylesheet import, Tailwind preset, dark mode). --- # Accordion (https://murasaki.ichi10.com/docs/components/accordion) ## Usage [#usage] ```tsx import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@murasakijs/ui"; ``` ```tsx Is it accessible? Yes. It adheres to the WAI-ARIA design pattern. ``` ## API Reference [#api-reference] ### `Accordion` [#accordion] | Prop | Type | Default | | ------------------------ | ------------------------------------------ | ------------ | | `type` | `"single"` \| `"multiple"` | — (required) | | `collapsible` | `boolean` (only when `type="single"`) | `false` | | `value` / `defaultValue` | `string` (single) or `string[]` (multiple) | — | | `onValueChange` | `(value) => void` | — | ### Other subcomponents [#other-subcomponents] | Component | Description | | ------------------ | ---------------------------------------------------------------- | | `AccordionItem` | A single collapsible section. Accepts `value` (required). | | `AccordionTrigger` | The clickable heading; renders a rotating chevron automatically. | | `AccordionContent` | The collapsible panel, animated in/out via CSS. | All subcomponents wrap Radix UI's [`Accordion`](https://www.radix-ui.com/primitives/docs/components/accordion) primitives and forward every other prop. --- # Alert (https://murasaki.ichi10.com/docs/components/alert) ## Usage [#usage] ```tsx import { Alert, AlertDescription, AlertTitle } from "@murasakijs/ui"; ``` ```tsx Heads up! You can add components to your app using the CLI. ``` ## Variants [#variants] Use the `variant` prop to switch to the destructive style for errors and warnings. ## API Reference [#api-reference] ### Props [#props] | Prop | Type | Default | | --------- | ------------------------------ | ----------- | | `variant` | `"default"` \| `"destructive"` | `"default"` | `Alert`, `AlertTitle`, and `AlertDescription` all extend the matching native HTML element's attributes. A leading `` child (e.g. a `lucide-react` icon) is automatically positioned in the top-left corner via CSS. --- # Avatar (https://murasaki.ichi10.com/docs/components/avatar) ## Usage [#usage] ```tsx import { Avatar, AvatarFallback, AvatarImage } from "@murasakijs/ui"; ``` ```tsx MU ``` ## API Reference [#api-reference] ### Subcomponents [#subcomponents] | Component | Description | | ---------------- | ------------------------------------------------------------------------------------------------------ | | `Avatar` | The root container (fixed `h-10 w-10` circle by default). | | `AvatarImage` | The image; renders nothing until it loads successfully. Accepts `src`, `alt`, `onLoadingStatusChange`. | | `AvatarFallback` | Shown while the image is loading or if it fails to load. Accepts `delayMs` to defer rendering. | All subcomponents wrap Radix UI's [`Avatar`](https://www.radix-ui.com/primitives/docs/components/avatar) primitives and forward every other prop. --- # Badge (https://murasaki.ichi10.com/docs/components/badge) ## Usage [#usage] ```tsx import { Badge } from "@murasakijs/ui"; ``` ```tsx Badge ``` ## API Reference [#api-reference] ### Props [#props] | Prop | Type | Default | | --------- | -------------------------------------------------------------- | ----------- | | `variant` | `"default"` \| `"secondary"` \| `"outline"` \| `"destructive"` | `"default"` | `Badge` renders a `
`; all other props (`className`, `onClick`, ...) are forwarded to it. --- # Button (https://murasaki.ichi10.com/docs/components/button) ## Usage [#usage] ```tsx import { Button } from "@murasakijs/ui"; ``` ```tsx ``` ## Variants [#variants] Use the `variant` and `size` props to change a button's appearance. ## API Reference [#api-reference] ### Props [#props] | Prop | Type | Default | | --------- | --------------------------------------------------------------------------------------- | ----------- | | `variant` | `"default"` \| `"secondary"` \| `"outline"` \| `"ghost"` \| `"destructive"` \| `"link"` | `"default"` | | `size` | `"default"` \| `"sm"` \| `"lg"` \| `"icon"` | `"default"` | | `asChild` | `boolean` | `false` | `asChild` renders `Button` as its child element (via Radix `Slot`) instead of a `