TanStack Start
Add native browser push to a TanStack Start app — using the root route’s head for the script and a server function to send.
Prerequisites
Get your keys with npx notify-dev init and add them to .env:
# publishable key — safe in the browser VITE_NOTIFY_KEY=ntfy_pk_your_publishable_key # secret key — server-side only, never VITE_ NOTIFY_SECRET=ntfy_sk_your_secret_key
Two keys: the publishable key (VITE_, inlined into the browser bundle) and the secret key, read at runtime in the server function via process.env — never expose it with VITE_.
1. Add the service worker
Create public/notify-sw.js (served at /notify-sw.js, same-origin with your pages):
importScripts("https://api.getnotify.dev/sw.js");2. Load notify.js
Inject the script from your root route’s head() — if you already return meta/links, just add the scripts array next to them.
import { createRootRoute } from "@tanstack/react-router";
export const Route = createRootRoute({
head: () => ({
// add this alongside any existing meta / links
scripts: [
{
src:
"https://api.getnotify.dev/notify.js?token=" +
import.meta.env.VITE_NOTIFY_KEY,
},
],
}),
// ...your existing shellComponent
});3. Subscribe a user
A component that subscribes once the user opts in.
declare global {
interface Window {
notify?: { subscribe: (userId: string) => Promise<unknown> };
}
}
export function EnableNotifications({ userId }: { userId: string }) {
return (
<button onClick={() => window.notify?.subscribe(userId).catch(console.error)}>
Enable notifications
</button>
);
}4. Send from a server function
A createServerFn keeps the request server-side; call it from a component, action, or loader.
import { createServerFn } from "@tanstack/react-start";
export const notifyUser = createServerFn({ method: "POST" })
.validator((data: { userId: string; body: string }) => data)
.handler(async ({ data }) => {
const res = await fetch("https://api.getnotify.dev/send", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
token: process.env.NOTIFY_SECRET,
userId: data.userId,
title: "Acme",
body: data.body,
}),
});
if (!res.ok) throw new Error("notify: send failed " + res.status);
});await notifyUser({ data: { userId: user.id, body: "Your export is ready!" } });VITE_NOTIFY_KEY (publishable) is inlined at build time, so set it before building — that’s fine, it’s meant for the browser. Set NOTIFY_SECRET as a server-side env var / Worker secret only.