easy-pingv0.7.0

Get in touch

Questions, bug reports, or anything about easy-ping. Either of these reaches me.

Emailteklumo.jembere@gmail.comTelegram@teklumt

For anything others would benefit from, a GitHub issue is better than a DM, because it's searchable.

GitHub

Getting started / Quickstart

Edit this page

Quickstart

A working inbox, email and push in five files. This walks the Postgres and Drizzle path. MongoDB differs only in step 2.

1 · Create the tables

The library owns four tables and never touches your user table. It references user ids instead.

// db/schema.ts
import { createSchema } from "easy-ping/adapters/drizzle";

export const { notification, notificationDelivery, notificationPreference } =
  createSchema();

Push the Drizzle schema with drizzle-kit. On Postgres via raw SQL, read Upgrading before you rely on renderPostgresDdl past the first deploy. It only creates tables, it does not migrate them.

2 · Configure

Two things are yours and can't be guessed. How a request identifies its user, and how a user id becomes an email address.

notify.ts
export const notify = easyPing({
  database: drizzleAdapter(db),
  secret: process.env.NOTIFY_SECRET!,

  // Required. These endpoints serve a user's private inbox.
  session: {
    getUserId: async (request) =>
      (await auth.api.getSession({ headers: request.headers }))?.user.id ?? null,
  },

  // Batched. One call per send, never one per recipient.
  getRecipients: async (userIds) => lookupUsers(userIds),

  channels: {
    inApp: { enabled: true },
    email: { provider: resend({ apiKey: process.env.RESEND_API_KEY! }) },
  },

  notifications: {
    commentReply: defineNotification({
      schema: z.object({ authorName: z.string(), commentId: z.string() }),
      channels: ["inApp", "email"],
    }),
  },
});
Heads up

session.getUserId is not optional. The mounted routes return one person's private inbox, so a client-supplied user id can never be trusted.

See Configuration for the full option reference.

3 · Mount the endpoints

One line gives you the whole inbox API. Plugins add their own routes to the same mount.

app/api/notifications/[[...notify]]/route.ts
export const { GET, POST } = notify.handler;
RouteMethodDoes
/GETPaginated feed, cursor-based. Carries unseenCount on the first page
/countGETUnseen badge count, as { unseen }
/seenPOSTClears the badge
/readPOSTMarks items read, idempotently
/cronPOSTThe delivery sweep. Bearer-authenticated.

4 · Send

Payload is typed against the schema you declared, so a rename fails at build rather than in production.

wherever the event happens
await notify.send("commentReply", {
  to: threadOwnerId,
  payload: { authorName: "Dana", commentId: "c_123" },
});

5 · Render the bell

The hook is headless. It keeps itself fresh over a server-sent event stream (one per browser, polling as the fallback), handles cursor pagination and optimistic updates, and holds no opinion about your markup.

components/Bell.tsx
const { notifications, unseenCount, markAsRead, markSeen } = useNotifications();

Seen and read are different states. Opening the dropdown clears the badge, but an item stays bold until it is actually clicked. See In-app inbox.

6 · Wire the cron

Every 1–5 minutes, from Vercel Cron, GitHub Actions, or anything that can POST. This is the durability floor beneath every delivery mode, as Delivery modes explains.

any scheduler
POST /api/notifications/cron
Authorization: Bearer $NOTIFY_CRON_SECRET