Push notifications from Next.js
Next.js on Vercel runs serverless. Un-awaited promises can be dropped when the response ends. Here’s how to send notifications correctly from App Router.
In an API route (Route Handler)
Use waitUntil from @vercel/functions:
import { waitUntil } from "@vercel/functions"
import fyi from "trigger.fyi"
import { NextResponse } from "next/server"
export async function POST(req: Request) {
const user = await createUser(await req.json())
const response = NextResponse.json(user)
waitUntil(fyi("New signup", { body: user.email, plan: user.plan }))
return response
}Or just await it — fyi() never throws, completes in one edge roundtrip:
export async function POST(req: Request) {
const user = await createUser(await req.json())
await fyi("New signup", { body: user.email })
return NextResponse.json(user)
}In a Server Action
Server Actions run on the server. await works directly:
"use server"
import fyi from "trigger.fyi"
export async function signupAction(formData: FormData) {
const user = await createUser({
email: formData.get("email") as string,
plan: formData.get("plan") as string,
})
await fyi("New signup", { body: user.email, plan: user.plan })
return { success: true, userId: user.id }
}The action waits for the notification before returning. fyi() adds ~100ms — acceptable for form submissions.
In a Stripe webhook handler
import { NextResponse } from "next/server"
import { waitUntil } from "@vercel/functions"
import Stripe from "stripe"
import fyi from "trigger.fyi"
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(req: Request) {
const body = await req.text()
const sig = req.headers.get("stripe-signature")!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
} catch {
return NextResponse.json({ error: "Bad signature" }, { status: 400 })
}
const response = NextResponse.json({ received: true })
if (event.type === "payment_intent.succeeded") {
const pi = event.data.object as Stripe.PaymentIntent
const amount = (pi.amount / 100).toFixed(2)
waitUntil(fyi(`$${amount} payment`, { body: pi.receipt_email ?? undefined }))
}
return response
}In a background route (after response)
For routes that process slow work:
import { waitUntil } from "@vercel/functions"
import fyi from "trigger.fyi"
export async function POST(req: Request) {
const job = await req.json()
// Return immediately — process in background
const response = new Response("Accepted", { status: 202 })
waitUntil(
processJob(job).then((result) =>
fyi("Job complete", {
body: `${result.count} items · ${result.elapsed}s`,
jobId: job.id,
})
)
)
return response
}Environment variable
# .env.local
TRIGGER_FYI_SECRET_KEY=your_key_here
The SDK reads it at call time — no module-level initialization. Works in Edge Runtime, Node.js Runtime, and Server Actions.
Edge Runtime
fyi() uses fetch under the hood and works in Edge Runtime:
export const runtime = "edge"
export async function POST(req: Request) {
const data = await req.json()
await fyi("Edge function called", { body: data.id })
return new Response("OK")
}Middleware
For lightweight notifications from middleware:
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export function middleware(req: NextRequest) {
// Don't await in middleware — use waitUntil via context
// Or use a fire-and-forget fetch directly
fetch(`https://trigger.fyi/${process.env.TRIGGER_FYI_SECRET_KEY}`, {
method: "POST",
body: `Middleware: ${req.nextUrl.pathname}`,
}).catch(() => {}) // swallow errors
return NextResponse.next()
}Setup
npx trigger.fyiGenerates a key, subscribes your device. Done.
Related: Push notifications from Node.js · Push notifications from Supabase · Deploy notifications on your phone · What is trigger.fyi?