@notilane/notif-sdk
v1.2.0
Published
NotiLane Notification Widget for external applications
Maintainers
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-sdkThen 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
- Initial load fetches the first page of posts (default: 10 posts)
- A "Load more" button appears at the bottom if more posts are available
- Clicking the button loads the next page and appends posts to the list
- 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
- Initialization: Widget scans DOM for
[nl-article-aid]elements - Icon Display: A help icon appears next to each element
- Click Handler: Clicking fetches the article from the API
- Popover Display: Article content is displayed in a styled popover
- 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}/readis sent on every read action - With userContext: Request includes
X-NotiLane-User-Contextheader for user-level tracking - Without userContext: Request is sent without user header (anonymous read count)
Constraints (when setting userContext):
userIdis 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
customDatais sent asexternalData.payloadin the API request - For single recipient: merged with dialog-level
externalData.payload(recipient data overrides) - For bulk recipients: each recipient DTO includes their own
externalDatawithpayload - The recipient's
idis automatically used asexternalData.externalIdfor 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
allowedChannelsto 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.jsConfiguration
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 --buildUsing 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 `
-PushUsing 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 -pWidget 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.0Kubernetes
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 domainAccess-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:latestBrowser Support
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
Getting Your Credentials
To use the NotiLane widget, you need:
- Client ID and Client Secret: Obtained from your NotiLane dashboard under Settings > External Apps
- Lane ID: Found in your NotiLane dashboard under Lanes > [Your Lane] > Settings
- API Base URL:
https://api.notilane.com/api/v1
Support
- 📧 Email: [email protected]
License
MIT
