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

@notilane/notif-sdk

v1.2.0

Published

NotiLane Notification Widget for external applications

Readme

NotiLane Notification Widget

A production-ready notification widget for displaying NotiLane posts in your web applications. Features a notification bell with badge, dropdown panel, "Load more" button for pagination, real-time polling, and theme support.

Note: The widget automatically filters to display only Notification type posts (not Article posts). This ensures the bell icon dropdown shows notifications intended for end-users, while Article posts are used separately for contextual help features.

Features

  • 🔐 OAuth 2.0-style authentication with client credentials
  • 🔔 Notification bell with unread badge count
  • 📋 Dropdown panel with post list
  • 📜 "Load more" button for pagination (loads posts on demand)
  • 🔄 Real-time polling for new posts
  • ✅ Read/unread status tracking (persisted in IndexedDB with localStorage fallback)
  • 🌓 Light and dark themes
  • 🌍 Internationalization (15 languages supported)
  • ♿ Keyboard accessibility
  • 📨 Send Smart Message - Programmatic dialog to send messages across multiple channels (Email, SMS, Teams, Push, WebPush)
  • 📱 Responsive design
  • 🎨 Contextual article help icons
  • 🚀 Zero external dependencies

Installation

Via NPM

npm install @notilane/notif-sdk

Then import in your JavaScript/TypeScript:

import '@notilane/widget';

Via Script Tag (CDN)

<!-- Production CDN -->
<script src="https://cdn.notilane.com/widget/v1/notilane-widget.umd.js"></script>

<!-- Or specific version -->
<script src="https://cdn.notilane.com/widget/v1.0.0/notilane-widget.umd.js"></script>

Self-Hosted

Download the latest release and include it in your project:

<script src="/path/to/notilane-widget.umd.js"></script>

Quick Start

<!DOCTYPE html>
<html>
<head>
  <title>My Application</title>
</head>
<body>
  <!-- Notification Bell Icon -->
  <div id="notification-bell" style="position: relative; cursor: pointer;">
    <span style="font-size: 24px;">🔔</span>
  </div>

  <!-- NotiLane Widget Script -->
  <script src="https://cdn.notilane.com/widget/v1/notilane-widget.umd.js"></script>
  <script>
    window.NotiLaneWidget = window.NotiLaneWidget || {};
    window.NotiLaneWidget.config = {
      // Required configuration
      apiBaseUrl: "https://api.notilane.com/api/v1",
      clientId: "your-client-id",
      clientSecret: "your-client-secret",
      laneId: "your-lane-id",
      bellElementId: "notification-bell",
      
      // Optional configuration
      theme: "light",                    // "light" or "dark"
      maxNotifications: 200,             // Max notifications in memory (default: 200)
      postsPerPage: 10,                  // Posts per API request
      autoFetch: true,                   // Fetch posts on init (default: true)
      pollingIntervalInSeconds: 60,      // Poll interval in seconds (min: 60, 0 to disable)
      maxPostsAgeDays: 30,               // Max age of posts to fetch (1-30 days)
      dropdownPosition: "bottom-right",  // Dropdown position
      showTimestamp: true,               // Show relative time
      showReadStatus: true,              // Track read/unread
      locale: "en"                       // Auto-detected from browser, or set explicitly
    };
  </script>
</body>
</html>

Configuration Options

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | apiBaseUrl | string | ✅ | - | NotiLane API base URL | | clientId | string | ✅ | - | External app client ID | | clientSecret | string | ✅ | - | External app client secret | | laneId | string | ✅ | - | Lane ID (MongoDB ObjectId) | | bellElementId | string | ✅ | - | HTML element ID for bell icon | | theme | "light" | "dark" | ❌ | "light" | Visual theme | | maxNotifications | number | ❌ | 200 | Max notifications in memory | | postsPerPage | number | ❌ | 10 | Posts per API request | | autoFetch | boolean | ❌ | true | Fetch posts on init (false = lazy load on dropdown open) | | pollingIntervalInSeconds | number | ❌ | 60 | Poll interval in seconds (min: 60, 0 to disable) | | maxPostsAgeDays | number | ❌ | 30 | Max age of posts to fetch in days (1-30) | | dropdownPosition | string | ❌ | "bottom-right" | Dropdown position | | showTimestamp | boolean | ❌ | true | Show relative timestamps | | showReadStatus | boolean | ❌ | true | Track read/unread status | | locale | string | ❌ | auto | Interface language (auto-detected). Supported: en, fr, cs, de, es, es-PE, es-CL, fr-BE, hr, hu, nl, nl-BE, pl, sk, sl | | customClass | string | ❌ | - | Custom CSS class | | cssClasses | object | ❌ | - | Override CSS classes (supports light/dark themes) | | enableArticleHelp | boolean | ❌ | false | Enable article help feature | | helpIconClass | string | ❌ | - | Custom CSS class for article help icons | | onPostClick | function | ❌ | - | Callback when post is clicked | | onAuthError | function | ❌ | - | Callback on auth error |

Pagination

The widget uses a "Load more" button for pagination instead of infinite scroll.

How It Works

  1. Initial load fetches the first page of posts (default: 10 posts)
  2. A "Load more" button appears at the bottom if more posts are available
  3. Clicking the button loads the next page and appends posts to the list
  4. The button automatically hides when all posts are loaded

Configuration

window.NotiLaneWidget.config = {
  // ...
  postsPerPage: 10,        // Number of posts per page (default: 10)
  maxNotifications: 200    // Maximum posts to load in memory (default: 200)
};

Mandatory Posts Popup

Posts with isMandatory: true are automatically displayed in a modal popup that requires user acknowledgment.

Behavior

  • Auto-display on load: Unread mandatory posts are shown immediately when the widget initializes
  • Modal popup: The popup blocks interaction with the page until dismissed
  • Queue navigation: If multiple mandatory posts exist, users can navigate using Previous/Next buttons
  • Acknowledgment required: Posts must be explicitly marked as read
  • Persistence: Read status is stored in IndexedDB

Events

const widget = window.NotiLaneWidget.instance;

// Popup opened
widget.on('mandatory-popup-open', () => {
  console.log('Mandatory popup opened');
});

// Popup closed (all mandatory posts read)
widget.on('mandatory-popup-close', () => {
  console.log('Mandatory popup closed');
});

// Individual post marked as read
widget.on('mandatory-post-read', (event) => {
  console.log('Mandatory post read:', event.data.postId);
});

CSS Class Customization

All CSS classes can be overridden via the cssClasses configuration option.

Theme-Aware CSS Classes

window.NotiLaneWidget.config = {
  theme: "dark",
  
  cssClasses: {
    // Light theme classes
    light: {
      dropdown: 'bg-white border shadow-lg',
      postItem: 'bg-white hover:bg-gray-50',
      postTitle: 'text-gray-900',
      postBody: 'text-gray-600'
    },
    
    // Dark theme classes (override light)
    dark: {
      dropdown: 'bg-gray-800 border-gray-700 shadow-lg',
      postItem: 'bg-gray-800 hover:bg-gray-700',
      postTitle: 'text-white',
      postBody: 'text-gray-300'
    }
  }
};

Available Class Overrides

| Class Key | Default Class | Description | |-----------|---------------|-------------| | widget | notilane-widget | Main widget container | | badge | notilane-badge | Unread count badge | | dropdown | notilane-dropdown | Dropdown container | | header | notilane-header | Header container | | headerTitle | notilane-header-title | Header title | | postsList | notilane-posts-list | Posts list container | | postItem | notilane-post-item | Post item container | | postItemUnread | unread | Unread post modifier | | postTitle | notilane-post-title | Post title | | postBody | notilane-post-body | Post body text | | postTags | notilane-post-tags | Tags container | | tag | notilane-tag | Individual tag | | postTime | notilane-post-time | Timestamp | | loading | notilane-loading | Loading container | | spinner | notilane-spinner | Loading spinner | | error | notilane-error | Error container | | empty | notilane-empty | Empty state container | | loadMoreButton | notilane-load-more | Load more button |

Example: Tailwind CSS Integration

window.NotiLaneWidget.config = {
  cssClasses: {
    light: {
      dropdown: 'absolute right-0 mt-2 w-80 bg-white rounded-lg shadow-xl border',
      header: 'flex justify-between items-center px-4 py-3 border-b',
      headerTitle: 'text-lg font-semibold text-gray-900',
      postItem: 'px-4 py-3 border-b hover:bg-gray-50 cursor-pointer',
      postItemUnread: 'bg-blue-50',
      postTitle: 'font-medium text-gray-900',
      postBody: 'text-sm text-gray-600 mt-1',
      tag: 'inline-block px-2 py-1 text-xs bg-gray-100 rounded mr-1'
    }
  }
};

Article Help Icons

Display contextual help icons next to any element on your page that fetch and display article content from NotiLane.

Enabling the Feature

window.NotiLaneWidget.config = {
  // ... required config ...
  
  enableArticleHelp: true,  // Enable article help feature
  helpIconClass: 'fa fa-question-circle'  // Optional: Custom icon class
};

HTML Markup

Add the nl-article-aid attribute to any element. The value should be the External ID of the article in NotiLane:

<!-- The External ID should match the externalId in NotiLane -->
<label nl-article-aid="help-password-requirements">Password</label>
<input type="password" />

<div nl-article-aid="feature-explanation" class="my-feature">
  Feature Title
</div>

<span nl-article-aid="tooltip-123">Click for help</span>

How It Works

  1. Initialization: Widget scans DOM for [nl-article-aid] elements
  2. Icon Display: A help icon appears next to each element
  3. Click Handler: Clicking fetches the article from the API
  4. Popover Display: Article content is displayed in a styled popover
  5. Error Handling: Errors are shown in a toast notification

Custom Icon Styling

// FontAwesome
window.NotiLaneWidget.config = {
  enableArticleHelp: true,
  helpIconClass: 'fa-solid fa-circle-question'
};

// Material Icons
window.NotiLaneWidget.config = {
  enableArticleHelp: true,
  helpIconClass: 'material-icons'
};

Showing/Hiding Help Icons

Control help icon visibility programmatically:

Via Instance Methods

const widget = window.NotiLaneWidget.instance;

widget.hideArticleHelp();     // Hide all help icons
widget.showArticleHelp();     // Show all help icons
widget.toggleArticleHelp();   // Toggle visibility

// Check current state
const isVisible = widget.isArticleHelpVisible;

Via DOM Events

// Hide all help icons
document.dispatchEvent(new CustomEvent('notilane:help:hide'));

// Show all help icons
document.dispatchEvent(new CustomEvent('notilane:help:show'));

// Toggle visibility
document.dispatchEvent(new CustomEvent('notilane:help:toggle'));

Listening for Visibility Changes

document.addEventListener('notilane:help:visibility-changed', (event) => {
  console.log('Help icons visible:', event.detail.visible);
});

Complete Example

<button id="helpToggle">👁️ Toggle Help Icons</button>

<script>
  document.getElementById('helpToggle').addEventListener('click', () => {
    window.NotiLaneWidget.instance.toggleArticleHelp();
  });

  document.addEventListener('notilane:help:visibility-changed', (e) => {
    const btn = document.getElementById('helpToggle');
    btn.textContent = e.detail.visible ? '👁️ Hide Help' : '👁️ Show Help';
  });
</script>

Programmatic API

Widget Methods

const widget = window.NotiLaneWidget.instance;

// Open/close dropdown
widget.open();
widget.close();
widget.toggle();

// Refresh posts
await widget.refresh();

// Get data
const posts = widget.getPosts();
const unreadCount = widget.getUnreadCount();

// Check status
const isReady = widget.isReady();

// Event subscription
const unsubscribe = widget.on('new-posts', (event) => {
  console.log('New posts:', event.data.posts);
});

// Clean up
widget.destroy();

Dynamic Theme Switching

Switch between light and dark themes at runtime when your application's theme changes:

const widget = window.NotiLaneWidget.instance;

// Get current theme
console.log(widget.theme); // 'light' or 'dark'

// Set theme dynamically
widget.setTheme('dark');
widget.setTheme('light');

// Example: Sync with your app's theme toggle
document.getElementById('theme-toggle').addEventListener('click', () => {
  const isDark = document.body.classList.toggle('dark-mode');
  widget.setTheme(isDark ? 'dark' : 'light');
});

// Example: Watch for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
  widget.setTheme(e.matches ? 'dark' : 'light');
});

// Listen for theme changes
widget.on('theme-changed', (event) => {
  console.log('Theme changed to:', event.data.theme);
});

User Context (Read Tracking)

The widget always calls the read tracking API (POST /lanes/{laneId}/posts/{postId}/read) when a user clicks "Read more" or "Mark as read". This allows counting total read clicks regardless of user identification.

Optionally, set user context to associate reads with specific users for analytics:

const widget = window.NotiLaneWidget.instance;

// Set user context (userId is mandatory)
widget.setUserContext({
  userId: 'user_12345',       // Required: Unique ID in your system
  name: 'John Doe',           // Optional: Display name
  email: '[email protected]',  // Optional: Email
  plan: 'premium',            // Optional: Custom properties allowed
  department: 'Engineering'   // Optional: Any flat key-value pairs
});

// Clear context on logout
widget.clearUserContext();

// Get current context
const ctx = widget.getUserContext();

Behavior:

  • Always: API call POST /lanes/{laneId}/posts/{postId}/read is sent on every read action
  • With userContext: Request includes X-NotiLane-User-Context header for user-level tracking
  • Without userContext: Request is sent without user header (anonymous read count)

Constraints (when setting userContext):

  • userId is mandatory - context is rejected without it
  • Maximum context size: 2KB
  • Values truncated to 256 characters
  • Only flat properties (no nested objects/arrays)

Events

| Event | Data | Description | |-------|------|-------------| | initialized | { success: boolean } | Widget initialized | | authenticated | { success: boolean } | Authentication completed | | posts-loaded | { posts: PostDto[], count: number } | Posts loaded | | new-posts | { posts: PostDto[], count: number } | New posts from polling | | error | { message: string, error: any } | Error occurred | | dropdown-open | {} | Dropdown opened | | dropdown-close | {} | Dropdown closed | | mandatory-popup-open | {} | Mandatory popup opened | | mandatory-popup-close | {} | Mandatory popup closed | | mandatory-post-read | { postId: string } | Mandatory post marked as read | | article-help-open | { externalId: string } | Article help popover opened | | article-help-close | {} | Article help popover closed | | article-help-error | { error: string, externalId: string } | Error loading article | | theme-changed | { theme: 'light' \| 'dark' } | Theme changed via setTheme() | | user-context-changed | { userId: string \| null } | User context set or cleared | | smart-message-open | {} | Send smart message dialog opened | | smart-message-close | {} | Send smart message dialog closed | | smart-message-success | { result: SendSmartMessageResult } | Message sent successfully | | smart-message-error | { error: SendSmartMessageError } | Send message failed |

DOM Events (Article Help Visibility)

| Event (dispatch) | Description | |------------------|-------------| | notilane:help:show | Show all help icons | | notilane:help:hide | Hide all help icons | | notilane:help:toggle | Toggle help icon visibility |

| Event (listen) | Detail | Description | |----------------|--------|-------------| | notilane:help:visibility-changed | { visible: boolean } | Emitted when visibility changes |

Storage

The widget uses IndexedDB for storing read post IDs (with localStorage fallback):

  • Read post IDs are stored locally per lane
  • Automatic cleanup removes oldest entries when exceeding 5,000 posts per lane
  • Data persists across browser sessions

Clearing Read Posts

const widget = window.NotiLaneWidget.instance;
const storage = widget.postsManager.getReadPostsStorage();
await storage.clear();

Send Smart Message

The Send Smart Message feature allows host applications to open a dialog for sending messages to selected recipients across multiple notification channels (Email, SMS, Teams, Push, WebPush).

Opening the Dialog

const widget = window.NotiLaneWidget.instance;

widget.openSendSmartMessageDialog({
  // Optional: Recipients provided by your application
  recipients: [
    {
      id: 'user-123',
      firstName: 'John',
      lastName: 'Doe',
      email: '[email protected]',
      phoneNumber: '+15551234567',
      // Optional: Custom data sent in the API payload for this recipient
      customData: {
        department: 'Engineering',
        employeeId: 'EMP-001'
      }
    },
    {
      id: 'user-456',
      fullName: 'Jane Smith',
      email: '[email protected]'
    }
  ],
  
  // Optional: Restrict which channels are shown (default: all)
  // Use this to hide channels not relevant to your application
  allowedChannels: ['Email', 'Sms', 'Teams', 'Push', 'WebPush'],
  
  // Optional: Pre-select channels
  defaultChannels: ['Email'],
  
  // Optional: Pre-select recipients by ID
  preselectedRecipientIds: ['user-123'],
  
  // Optional: Default selection mode
  defaultSelectionMode: 'TryAllChannels', // or 'StopOnFirstSuccess'
  
  // Optional: Pre-fill message content
  defaultTitle: 'Important Update',
  defaultBody: '',
  
  // Optional: Teams card customization
  teamsOptions: {
    format: 'Hero',        // 'Simple' | 'Hero' | 'Fact' | 'Alert' | 'Announcement'
    themeColor: '0078D4',  // Hex color without #
    headerImageUrl: 'https://example.com/image.png'
  },
  
  // Optional: Advanced options (debugging)
  showAdvancedOptions: false,
  defaultAdvancedOptions: {
    actionUrl: 'https://example.com/action',
    externalId: 'tracking-123'
  },
  
  // Optional: External data for tracking
  externalData: {
    externalId: 'msg-123',
    payload: { customField: 'value' }
  },
  
  // Callbacks
  onSuccess: (result) => {
    console.log('Message sent successfully:', result);
    // result.success - true if at least one message sent
    // result.deliveredVia - channels used for single recipient
    // result.successCount / result.failureCount - for bulk
  },
  onError: (error) => {
    console.error('Failed to send message:', error);
  },
  onCancel: () => {
    console.log('User cancelled the dialog');
  }
});

Closing the Dialog

widget.closeSendSmartMessageDialog();

Recipient Interface

Each recipient object should conform to the WidgetRecipient interface:

interface WidgetRecipient {
  id: string;                   // Unique identifier (used as externalId in API)
  firstName?: string;           // First name
  lastName?: string;            // Last name
  fullName?: string;            // Full name (used if firstName/lastName not set)
  email?: string;               // Required for Email channel
  phoneNumber?: string;         // Required for SMS channel (E.164 format)
  customData?: Record<string, string>; // Per-recipient custom data (see below)
}

type NotificationChannelName = 'Email' | 'Sms' | 'Teams' | 'Push' | 'WebPush' | 'WhatsApp' | 'Telegram';

Per-Recipient Custom Data

The customData field allows you to attach recipient-specific metadata that will be included in the API payload:

recipients: [
  {
    id: 'user-123',
    firstName: 'John',
    lastName: 'Doe',
    email: '[email protected]',
    customData: {
      department: 'Engineering',
      employeeId: 'EMP-001',
      region: 'EU'
    }
  }
]

How it works:

  • Each recipient's customData is sent as externalData.payload in the API request
  • For single recipient: merged with dialog-level externalData.payload (recipient data overrides)
  • For bulk recipients: each recipient DTO includes their own externalData with payload
  • The recipient's id is automatically used as externalData.externalId for tracking

Dialog Options

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | recipients | WidgetRecipient[] | ❌ | [] | Initial recipients to display (more can be added dynamically) | | allowedChannels | NotificationChannelName[] | ❌ | All channels | Restricts which channels are shown in the dialog. Hidden channels cannot be selected. | | defaultChannels | NotificationChannelName[] | ❌ | [] | Pre-selected channels | | preselectedRecipientIds | string[] | ❌ | [] | Pre-selected recipient IDs | | defaultSelectionMode | 'TryAllChannels' \| 'StopOnFirstSuccess' | ❌ | 'TryAllChannels' | Default channel selection mode | | defaultTitle | string | ❌ | '' | Pre-filled message title | | defaultBody | string | ❌ | '' | Pre-filled message body | | teamsOptions | TeamsCardOptions | ❌ | - | Teams card customization (format, theme color, header image, facts, actions) | | showAdvancedOptions | boolean | ❌ | false | Show advanced options panel (Action URL, External ID) | | defaultAdvancedOptions | object | ❌ | - | Pre-fill advanced options { actionUrl, externalId } | | externalData | object | ❌ | - | External data to include with messages { externalId, payload } | | laneId | string | ❌ | widget's laneId | Override lane ID for lane-specific configuration | | onSuccess | function | ❌ | - | Called on successful send | | onError | function | ❌ | - | Called on send error | | onCancel | function | ❌ | - | Called when user cancels |

TeamsCardOptions Interface

interface TeamsCardOptions {
  format?: 'Simple' | 'Hero' | 'Fact' | 'Alert' | 'Announcement';
  headerImageUrl?: string;   // URL for header image (Hero/Announcement formats)
  themeColor?: string;       // Hex color without # (e.g., "0078D4")
  facts?: Array<{ name: string; value: string }>;  // Key-value pairs (Fact format)
  additionalActions?: Array<{ type: 'OpenUrl'; name: string; url: string }>;
  footerText?: string;       // Footer text
}

Channel Selection Modes

| Mode | Description | |------|-------------| | TryAllChannels | Attempt delivery on all selected channels simultaneously | | StopOnFirstSuccess | Stop after the first successful delivery (ordered by channel priority) |

Features

  • Multi-recipient selection: Select all or individual recipients with checkboxes
  • Dynamic recipients: Add new recipients directly in the dialog
  • Channel filtering: Use allowedChannels to show only relevant channels for your app
  • Channel selection: Choose delivery channels with sortable order (drag & drop)
  • Selection mode: "Try all channels" or "Stop on first success"
  • Teams customization: Card format, theme color, header image, facts, action buttons
  • Advanced options: Action URL, External ID for tracking (optional panel)
  • Validation: Real-time form validation with clear error messages
  • Localization: Supports all 15 widget locales
  • Theming: Inherits light/dark theme from widget configuration
  • Accessibility: Full keyboard navigation and ARIA support
  • Result dialog: Shows detailed success/failure results

Events

// Listen for send smart message events
widget.on('smart-message-open', () => {
  console.log('Send smart message dialog opened');
});

widget.on('smart-message-close', () => {
  console.log('Send smart message dialog closed');
});

widget.on('smart-message-success', (event) => {
  console.log('Message sent:', event.data);
});

widget.on('smart-message-error', (event) => {
  console.error('Send failed:', event.data.error);
});

CSS Customization

Override send message dialog styles via cssClasses:

window.NotiLaneWidget.config = {
  cssClasses: {
    light: {
      sendDialogOverlay: 'my-overlay',
      sendDialog: 'my-dialog',
      sendDialogHeader: 'my-header',
      sendDialogContent: 'my-content',
      sendDialogFooter: 'my-footer',
      sendRecipientList: 'my-recipients',
      sendChannelChip: 'my-channel-chip',
      sendSubmitButton: 'my-submit-btn'
    }
  }
};

Security Considerations

⚠️ Important: Exposing client secrets in browser JavaScript is not recommended for production.

Recommended: Backend Proxy

For production applications, use a backend proxy to keep credentials secure:

// Frontend
window.NotiLaneWidget.config = {
  apiBaseUrl: "https://yourapp.com/api/notilane-proxy",
  laneId: "your-lane-id",
  bellElementId: "notification-bell"
  // No clientId or clientSecret needed
};
// Backend (Node.js/Express example)
app.post('/api/notilane-proxy/authentications/external-app-token', async (req, res) => {
  const response = await fetch('https://api.notilane.com/api/v1/authentications/external-app-token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      clientId: process.env.NOTILANE_CLIENT_ID,
      clientSecret: process.env.NOTILANE_CLIENT_SECRET
    })
  });
  res.json(await response.json());
});

Custom Styling

All CSS classes are prefixed with notilane- for easy targeting:

/* Custom badge color */
.notilane-badge {
  background-color: #ff6b6b;
}

/* Custom post item style */
.notilane-post-item.unread {
  background-color: #e8f4fd;
}

/* Custom header */
.notilane-header {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

Adding a Custom Class

window.NotiLaneWidget.config = {
  customClass: 'my-notifications'
};

Self-Hosted Deployment (Docker)

For organizations that need to host the widget CDN themselves, we provide Docker support for easy deployment.

Prerequisites

  • Docker 20.10+ and Docker Compose v2+
  • Node.js 20+ (for local development only)

Quick Start

# Clone and build
git clone https://github.com/your-org/notilane-widget.git
cd notilane-widget

# Build and run with Docker Compose
docker-compose up --build

# Widget is now available at:
# http://localhost:8080/widget/latest/notilane-widget.es.js
# http://localhost:8080/widget/latest/notilane-widget.umd.js

Configuration

Environment Variables

Create a .env file (copy from .env.example):

# Widget version (auto-detected from package.json if not set)
WIDGET_VERSION=1.1.2

# Default API URL baked into the widget build
DEFAULT_API_URL=https://api.notilane.app

# Local CDN port
CDN_PORT=8080

| Variable | Required | Default | Description | |----------|----------|---------|-------------| | WIDGET_VERSION | No | From package.json | Version tag for the build | | DEFAULT_API_URL | No | https://api.notilane.app | API URL embedded in widget | | CDN_PORT | No | 8080 | Port for local CDN server |

Building the Docker Image

Using Docker Compose (Recommended)

# Build with default settings
docker-compose up --build

# Build with custom version
WIDGET_VERSION=2.0.0 docker-compose up --build

# Build with custom API URL
DEFAULT_API_URL=https://api.mycompany.com docker-compose up --build

Using the Build Script (Windows)

# Build with version from package.json
.\scripts\docker-build.ps1

# Build specific version
.\scripts\docker-build.ps1 -Version 1.2.0

# Build and push to registry
.\scripts\docker-build.ps1 -Version 1.2.0 -Registry ghcr.io -Push

# Full options
.\scripts\docker-build.ps1 `
  -Version 1.2.0 `
  -Registry docker.io `
  -ImageName myorg/widget-cdn `
  -ApiUrl https://api.mycompany.com `
  -Push

Using the Build Script (Linux/macOS)

# Make executable
chmod +x scripts/docker-build.sh

# Build with version from package.json
./scripts/docker-build.sh

# Build specific version
./scripts/docker-build.sh -v 1.2.0

# Build and push to registry
./scripts/docker-build.sh -v 1.2.0 -r ghcr.io -p

Widget URL Structure

Once deployed, the widget is available at these URLs:

| URL Pattern | Cache Duration | Use Case | |-------------|----------------|----------| | /widget/latest/notilane-widget.es.js | 5 minutes | Always get latest version | | /widget/latest/notilane-widget.umd.js | 5 minutes | Always get latest version | | /widget/v1.2.0/notilane-widget.es.js | 1 year (immutable) | Lock to specific version | | /widget/v1.2.0/notilane-widget.umd.js | 1 year (immutable) | Lock to specific version | | /health | No cache | Health check endpoint | | /widget/version.json | No cache | Current version info |

Using Your Self-Hosted Widget

Update your HTML to point to your CDN:

<!-- ES Module (recommended for modern browsers) -->
<script type="module">
  import 'https://cdn.yourcompany.com/widget/latest/notilane-widget.es.js';
</script>

<!-- UMD (for broader compatibility) -->
<script src="https://cdn.yourcompany.com/widget/v1.2.0/notilane-widget.umd.js"></script>

<script>
  window.NotiLaneWidget.config = {
    apiBaseUrl: "https://api.yourcompany.com/api/v1",
    clientId: "your-client-id",
    clientSecret: "your-client-secret",
    laneId: "your-lane-id",
    bellElementId: "notification-bell"
  };
</script>

Health Check

The container exposes a health endpoint for orchestration:

curl http://localhost:8080/health
# Response: {"status":"healthy","service":"notilane-widget-cdn"}

Production Deployment

Docker Run

docker run -d \
  --name notilane-widget-cdn \
  -p 80:80 \
  --restart unless-stopped \
  notilane/widget-cdn:1.2.0

Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: notilane-widget-cdn
spec:
  replicas: 2
  selector:
    matchLabels:
      app: notilane-widget-cdn
  template:
    metadata:
      labels:
        app: notilane-widget-cdn
    spec:
      containers:
      - name: widget-cdn
        image: notilane/widget-cdn:1.2.0
        ports:
        - containerPort: 80
        livenessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /health
            port: 80
          initialDelaySeconds: 5
          periodSeconds: 10
        resources:
          requests:
            memory: "64Mi"
            cpu: "50m"
          limits:
            memory: "128Mi"
            cpu: "100m"

CORS Configuration

The NGINX configuration includes permissive CORS headers for widget embedding:

  • Access-Control-Allow-Origin: * - Allows embedding from any domain
  • Access-Control-Allow-Methods: GET, HEAD, OPTIONS
  • Preflight requests are handled automatically

If you need to restrict origins, edit the nginx.conf file:

# Replace this:
add_header Access-Control-Allow-Origin "*" always;

# With specific origins:
add_header Access-Control-Allow-Origin "https://app.yourcompany.com" always;

Serving Multiple Versions

To serve multiple widget versions simultaneously, rebuild the image for each version and update the volume mappings:

# Build version 1.0.0
WIDGET_VERSION=1.0.0 docker-compose up --build
docker cp notilane-widget-cdn:/usr/share/nginx/html/widget/v1.0.0 ./versions/

# Build version 1.1.0
WIDGET_VERSION=1.1.0 docker-compose up --build
docker cp notilane-widget-cdn:/usr/share/nginx/html/widget/v1.1.0 ./versions/

# Mount all versions
docker run -d \
  -v ./versions:/usr/share/nginx/html/widget:ro \
  -p 80:80 \
  notilane/widget-cdn:latest

Browser Support

  • Chrome 90+
  • Firefox 88+
  • Safari 14+
  • Edge 90+

Getting Your Credentials

To use the NotiLane widget, you need:

  1. Client ID and Client Secret: Obtained from your NotiLane dashboard under Settings > External Apps
  2. Lane ID: Found in your NotiLane dashboard under Lanes > [Your Lane] > Settings
  3. API Base URL: https://api.notilane.com/api/v1

Support

License

MIT