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:

.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):

public/notify-sw.js
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.

src/routes/__root.tsx
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.

src/components/EnableNotifications.tsx
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.

src/functions/notify.ts
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);
  });
client or server
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.