Single Instance
Most desktop apps should only ever run one copy per user. When a user double-clicks the app icon while it’s already open, you want the existing window to focus — not a second window.
API
import { singleInstance } from "@tynd/core/client";
const { acquired } = await singleInstance.acquire("com.example.myapp");
if (!acquired) {
// We're the second instance. The primary has already auto-focused itself
// and will receive {argv, cwd} via onSecondLaunch. Exit silently.
process.exit(0); // or return from main()
}
// Only the primary reaches this point. Set up second-launch handling:
singleInstance.onSecondLaunch(({ argv, cwd }) => {
console.log("user tried to launch again with", argv, "from", cwd);
// typical: focus the main window + interpret argv as a file-open request
});acquire(id)uses a cross-OS exclusive lock + a local socket for forwarding argv/cwd.- Hold the lock for the process lifetime — don’t release and re-acquire. The lock releases on process exit.
iddoubles as the OS lock name and the socket name. Use a stable reverse-DNS identifier.
What happens under the hood
When acquire() returns { acquired: false }, the host has already:
- Connected to the primary instance’s local socket (named pipe on Windows, abstract socket on Linux, CFMessagePort on macOS).
- Sent the forwarded payload as a single JSON line
{ argv, cwd }. - Triggered the primary window’s
setFocus()+ un-minimize via the host’s native event loop — no IPC round-trip needed.
Use cases
Deep-link handling
Register a custom scheme in tynd.config.ts:
export default {
runtime: "lite",
backend: "backend/main.ts",
frontendDir: "dist",
protocols: ["myapp"], // myapp:// links now launch this app
bundle: { identifier: "com.example.myapp" },
} satisfies TyndConfig;Handle both cold-start and duplicate-launch cases with onOpenUrl:
singleInstance.onOpenUrl((url) => {
// e.g. "myapp://invite/abc123"
const parsed = new URL(url);
router.navigate(parsed.pathname);
});onOpenUrl fires for:
- Cold start — argv contains the URL.
- Duplicate launch — the primary receives the forwarded URL.
See Deep Linking for scheme registration details per OS.
File-open
Users drag a file onto your app icon while it’s running. The OS relaunches the app with the file path as argv[1]:
singleInstance.onSecondLaunch(({ argv }) => {
const file = argv.find((a) => a.endsWith(".myapp"));
if (file) openFile(file);
});File type associations are a separate concern (not currently exposed as a first-class config field — you’d hand-edit the .desktop / .app / installer metadata).
Platform notes
- Windows — named pipe (
\\.\pipe\<id>). Windows frees the pipe when the owning process dies; no stale-lock cleanup needed. - Linux — abstract socket (Linux-specific, not filesystem-visible). Auto-released by the kernel on process exit.
- macOS —
CFMessagePort(user-session-scoped, Mach-kernel-backed). If a crash leaves a zombie,killall MyAppor reboot.
Error modes
acquire() returns false even when alone
Stale lock. Usually self-heals on process exit (see above). If it doesn’t:
- Windows — nothing to do; Windows frees named pipes aggressively.
- Linux — abstract socket auto-released.
- macOS —
killall MyAppto clear the zombie.
onSecondLaunch fires twice
You called singleInstance.onSecondLaunch(...) inside a handler that itself re-fires (React useEffect rerender, component re-mount). The API is idempotent for a given handler function, but registering two different handler functions means both fire.
// Register once, not in a render function
const unsub = singleInstance.onSecondLaunch(onRelaunch);
// call unsub() if you want to stop handlingId convention
Use a stable reverse-DNS identifier that matches bundle.identifier:
singleInstance.acquire("com.example.myapp");Don’t include version numbers — a user upgrading from 1.0.0 to 1.1.0 expects the upgrade to auto-focus the already-running old version (so the old one can shut down cleanly, then the new one starts). Different ids break that.
What this doesn’t give you
- Multi-user isolation on Windows (service / tenancy setups) —
single-instanceis user-scoped, so two users on the same box each get their own primary. - Cross-machine coordination — no network-wide lock. If you want a license server, build your own.