npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

townkrier-in-app

v1.0.0-alpha.3

Published

In-app/database notification adapter for Townkrier notification system

Readme

townkrier-in-app

In-app/database notification adapter for the TownKrier notification system. This package provides a channel for storing notifications in a database for display within your application (similar to notification bells in modern apps).

Installation

npm install townkrier-in-app
# or
pnpm add townkrier-in-app
# or
yarn add townkrier-in-app

Features

  • 📬 Store notifications in any database
  • 📖 Mark notifications as read/unread
  • 🗑️ Delete notifications
  • 📊 Count unread notifications
  • 🔍 Query notifications by user
  • 💾 Pluggable storage adapter architecture

Usage

Basic Setup

import { NotificationManager } from 'townkrier-core';
import { createInAppChannel, InMemoryInAppStorageAdapter } from 'townkrier-in-app';

// 1. Create a storage adapter
const storageAdapter = new InMemoryInAppStorageAdapter();

// 2. Setup notification manager
const notificationManager = new NotificationManager({
  defaultChannel: 'in-app',
  channels: [
    {
      name: 'in-app',
      enabled: true,
      config: {
        storageAdapter,
      },
    },
  ],
});

// 3. Register the in-app channel
notificationManager.registerFactory('in-app', createInAppChannel);

Sending In-App Notifications

import { NotificationChannel } from 'townkrier-core';

// Send to single user
await notificationManager.send(
  {
    title: 'Welcome!',
    message: 'Welcome to our application',
    type: 'welcome',
    actionUrl: '/getting-started',
    icon: '🎉',
  },
  {
    [NotificationChannel.IN_APP]: { userId: 'user-123' },
  },
);

// Send to multiple users
await notificationManager.send(
  {
    title: 'New Feature',
    message: 'Check out our new feature',
    type: 'announcement',
  },
  {
    [NotificationChannel.IN_APP]: [
      { userId: 'user-123' },
      { userId: 'user-456' },
      { userId: 'user-789' },
    ],
  },
);

Reading Notifications

const channel = notificationManager.getChannel('in-app') as DatabaseInAppChannel;

// Get notifications for a user
const notifications = await channel.getNotificationsForUser('user-123', {
  limit: 10,
  unreadOnly: true,
});

// Count unread notifications
const unreadCount = await channel.countUnread('user-123');

// Mark a notification as read
await channel.markAsRead('notification-id');

// Mark all notifications as read
await channel.markAllAsRead('user-123');

// Delete a notification
await channel.deleteNotification('notification-id');

Custom Storage Adapter

The in-memory adapter is suitable for testing, but for production you should implement a database-backed adapter:

import { InAppStorageAdapter, InAppNotificationData } from 'townkrier-in-app';

class MongoDBStorageAdapter implements InAppStorageAdapter {
  private db: MongoClient;

  constructor(connectionString: string) {
    this.db = new MongoClient(connectionString);
  }

  async save(notification: InAppNotificationData): Promise<string> {
    const result = await this.db
      .db('app')
      .collection('notifications')
      .insertOne({
        ...notification,
        _id: new ObjectId(),
        createdAt: new Date(),
      });

    return result.insertedId.toString();
  }

  async get(id: string): Promise<InAppNotificationData | null> {
    const doc = await this.db
      .db('app')
      .collection('notifications')
      .findOne({ _id: new ObjectId(id) });

    if (!doc) return null;

    return {
      id: doc._id.toString(),
      userId: doc.userId,
      title: doc.title,
      message: doc.message,
      type: doc.type,
      actionUrl: doc.actionUrl,
      icon: doc.icon,
      data: doc.data,
      read: doc.read,
      readAt: doc.readAt,
      createdAt: doc.createdAt,
      metadata: doc.metadata,
    };
  }

  async getForUser(
    userId: string,
    options?: {
      limit?: number;
      offset?: number;
      unreadOnly?: boolean;
    },
  ): Promise<InAppNotificationData[]> {
    const query: any = { userId };
    if (options?.unreadOnly) {
      query.read = false;
    }

    const docs = await this.db
      .db('app')
      .collection('notifications')
      .find(query)
      .sort({ createdAt: -1 })
      .skip(options?.offset || 0)
      .limit(options?.limit || 50)
      .toArray();

    return docs.map((doc) => ({
      id: doc._id.toString(),
      userId: doc.userId,
      title: doc.title,
      message: doc.message,
      type: doc.type,
      actionUrl: doc.actionUrl,
      icon: doc.icon,
      data: doc.data,
      read: doc.read,
      readAt: doc.readAt,
      createdAt: doc.createdAt,
      metadata: doc.metadata,
    }));
  }

  async markAsRead(id: string): Promise<void> {
    await this.db
      .db('app')
      .collection('notifications')
      .updateOne({ _id: new ObjectId(id) }, { $set: { read: true, readAt: new Date() } });
  }

  async markAllAsRead(userId: string): Promise<void> {
    await this.db
      .db('app')
      .collection('notifications')
      .updateMany({ userId, read: false }, { $set: { read: true, readAt: new Date() } });
  }

  async delete(id: string): Promise<void> {
    await this.db
      .db('app')
      .collection('notifications')
      .deleteOne({ _id: new ObjectId(id) });
  }

  async countUnread(userId: string): Promise<number> {
    return await this.db
      .db('app')
      .collection('notifications')
      .countDocuments({ userId, read: false });
  }
}

API Integration Example

import express from 'express';
import { NotificationManager, NotificationChannel } from 'townkrier-core';
import { createInAppChannel, DatabaseInAppChannel } from 'townkrier-in-app';

const app = express();
app.use(express.json());

// Setup notification manager (see above)
const notificationManager = new NotificationManager(/* ... */);
const inAppChannel = notificationManager.getChannel('in-app') as DatabaseInAppChannel;

// Get notifications for current user
app.get('/api/notifications', async (req, res) => {
  const userId = req.user.id; // From your auth middleware
  const notifications = await inAppChannel.getNotificationsForUser(userId, {
    limit: parseInt(req.query.limit as string) || 20,
    offset: parseInt(req.query.offset as string) || 0,
    unreadOnly: req.query.unread === 'true',
  });

  const unreadCount = await inAppChannel.countUnread(userId);

  res.json({ notifications, unreadCount });
});

// Mark notification as read
app.post('/api/notifications/:id/read', async (req, res) => {
  try {
    await inAppChannel.markAsRead(req.params.id);
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: 'Failed to mark as read' });
  }
});

// Mark all as read
app.post('/api/notifications/read-all', async (req, res) => {
  const userId = req.user.id;
  try {
    await inAppChannel.markAllAsRead(userId);
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: 'Failed to mark all as read' });
  }
});

// Delete notification
app.delete('/api/notifications/:id', async (req, res) => {
  try {
    await inAppChannel.deleteNotification(req.params.id);
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: 'Failed to delete notification' });
  }
});

TypeScript Support

This package is written in TypeScript and provides full type definitions.

License

MIT