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 🙏

© 2025 – Pkg Stats / Ryan Hefner

notion-upstash-cms

v0.4.0

Published

Sync Notion data sources to Upstash Redis with webhook-based real-time updates

Readme

notion-upstash-cms

npm version License: MIT

Sync Notion data sources to Upstash Redis with webhook-based real-time updates. Keep your Notion CMS in sync with your application's cache automatically.

Features

  • 🔄 Real-time sync via Notion webhooks
  • 📦 Option B storage - one key per page + indexes for fast queries
  • 🎯 Config-based - define your field mappings once
  • 🔌 Framework agnostic - works with Next.js, Express, or any Node.js runtime
  • 📝 Markdown support - automatic content extraction from Notion blocks
  • 🚀 Edge-ready - works in Edge runtimes (Vercel Edge, Cloudflare Workers)
  • 🔒 Type-safe - full TypeScript support

Installation

npm install notion-upstash-cms
# or
pnpm add notion-upstash-cms
# or
yarn add notion-upstash-cms

Quick Start

1. Create a config file

Create notion-upstash.config.ts:

import { defineConfig } from 'notion-upstash-cms';

export default defineConfig({
  dataSources: [
    {
      id: 'your-data-source-id',
      name: 'blog',
      content: {
        mode: 'markdown',
        field: 'content',
      },
      storage: {
        primaryIndex: { field: 'publishedAt', type: 'timestamp' },
      },
      fields: {
        id: { type: 'meta', source: 'page.id' },
        title: { type: 'title' },
        slug: {
          type: 'rich_text',
          propertyId: 'PBBh',
          transforms: ['slugify'],
        },
        excerpt: { type: 'rich_text', propertyId: '%7B%40V%3C' },
        publishedAt: { type: 'date', propertyId: 'kdM%7C' },
        tags: {
          type: 'multi_select',
          propertyId: 'BzMr',
          value: 'name',
        },
        featured: {
          type: 'checkbox',
          propertyId: 'Izfp',
          default: false,
        },
        image: { type: 'url', propertyId: '%3D_%5E%5C' },
        lastEdited: { type: 'meta', source: 'page.last_edited_time' },
        created: { type: 'meta', source: 'page.created_time' },
      },
    },
  ],
});

2. Set up webhook handler

Next.js (Edge Runtime)

// app/api/notion-webhook/route.ts
import rawConfig from '../../../notion-upstash.config';
import { withEnv } from 'notion-upstash-cms';
import { createNextRouteHandler } from 'notion-upstash-cms/next';

const config = withEnv(rawConfig, {
  notionToken: process.env.NOTION_TOKEN!,
  redis: {
    url: process.env.KV_REST_API_URL!,
    token: process.env.KV_REST_API_TOKEN!,
    prefix: 'notioncms',
  },
});

export const runtime = 'edge';
const routeHandler = createNextRouteHandler(config);
export const POST = routeHandler;

Express

import express from 'express';
import rawConfig from './notion-upstash.config';
import { withEnv } from 'notion-upstash-cms';
import { createExpressMiddleware } from 'notion-upstash-cms/express';

const config = withEnv(rawConfig, {
  notionToken: process.env.NOTION_TOKEN!,
  redis: {
    url: process.env.KV_REST_API_URL!,
    token: process.env.KV_REST_API_TOKEN!,
    prefix: 'notioncms',
  },
});

const app = express();
app.use(express.text({ type: 'application/json' }));
app.post('/notion-webhook', createExpressMiddleware(config));

3. Read data in your app

import { createReader, withEnv } from 'notion-upstash-cms';
import rawConfig from './notion-upstash.config';

const config = withEnv(rawConfig, {
  notionToken: process.env.NOTION_TOKEN!,
  redis: {
    url: process.env.KV_REST_API_URL!,
    token: process.env.KV_REST_API_TOKEN!,
  },
});

const reader = createReader(config);

// Get all blog posts
const posts = await reader.getAll('blog');

// Get post by slug
const post = await reader.getBySlug('blog', 'my-post-slug');

// Get post by ID
const postById = await reader.getById('blog', 'page-id');

// List posts by index (e.g., publishedAt)
const recentPosts = await reader.listByIndex('blog', 'publishedAt', {
  limit: 10,
  order: 'desc',
});

4. Manual sync (optional)

import { syncDataSource, withEnv } from 'notion-upstash-cms';
import rawConfig from './notion-upstash.config';

const config = withEnv(rawConfig, {
  notionToken: process.env.NOTION_TOKEN!,
  redis: {
    url: process.env.KV_REST_API_URL!,
    token: process.env.KV_REST_API_TOKEN!,
  },
});

// Sync all pages from 'blog' data source
await syncDataSource('blog', config);

Configuration

Field Types

  • meta - Page metadata (page.id, page.last_edited_time, page.created_time)
  • title - Title property
  • rich_text - Rich text property
  • date - Date property
  • checkbox - Checkbox property
  • multi_select - Multi-select property
  • select - Select property
  • url - URL property
  • email - Email property
  • phone - Phone property
  • number - Number property

Transforms

Built-in transforms:

  • slugify - Convert text to URL-safe slug
  • optimizeSeoTitle - Truncate title at word boundary
  • truncate(length) - Truncate text to specified length

Example:

slug: {
  type: 'rich_text',
  propertyId: 'PBBh',
  transforms: ['slugify'],
}

Lifecycle Hooks

Optional hooks for advanced use cases:

{
  name: 'blog',
  // ... fields
  hooks: {
    beforeUpsert: async (page, transformed) => {
      // Modify transformed data before storing
      return transformed;
    },
    afterUpsert: async (page, transformed) => {
      // Side effects after storing (e.g., invalidate cache)
    },
    beforeDelete: async (pageId) => {
      // Side effects before deleting
    },
  },
}

Storage Strategy (Option B)

This package uses Option B storage strategy:

  • One key per page: notioncms:{name}:page:{id}
  • Indexes: Sorted sets for fast queries
    • notioncms:{name}:index:all - All pages indexed by created time
    • notioncms:{name}:index:{field} - Custom index (if primaryIndex configured)

This provides:

  • Fast individual page lookups
  • Efficient pagination and sorting
  • Better scalability than single JSON blob

API Reference

defineConfig(config)

Defines a static configuration (no secrets). Returns the config with TypeScript autocomplete.

withEnv(staticConfig, env)

Combines static config with runtime secrets.

Parameters:

  • staticConfig - Static config from defineConfig
  • env - Runtime environment:
    • notionToken - Notion API token
    • redis.url - Upstash Redis REST API URL
    • redis.token - Upstash Redis REST API token
    • redis.prefix - Optional Redis key prefix (default: 'notioncms')
    • notionVersion - Optional Notion API version (default: '2025-09-03')

createNotionWebhookHandler(config)

Creates a webhook handler function.

Returns: (rawBody: string | object) => Promise<void>

createNextRouteHandler(config)

Creates a Next.js route handler.

Returns: (req: Request) => Promise<Response>

createExpressMiddleware(config)

Creates an Express middleware.

Returns: Express middleware function

syncDataSource(dsNameOrId, config)

Manually syncs a data source from Notion.

Parameters:

  • dsNameOrId - Data source name or ID
  • config - Complete configuration

createReader(config)

Creates a reader instance for querying cached data.

Returns: Reader object with methods:

  • getAll(dsName) - Get all pages
  • getById(dsName, id) - Get page by ID
  • getBySlug(dsName, slug) - Get page by slug
  • listByIndex(dsName, indexField, options) - List pages by index

Notion Setup

  1. Create a Notion integration at https://www.notion.so/my-integrations
  2. Get your integration token
  3. Share your database/data source with the integration
  4. Get your data source ID from the database URL or API
  5. Set up a webhook in Notion pointing to your webhook endpoint

Environment Variables

NOTION_TOKEN=secret_xxx
KV_REST_API_URL=https://xxx.upstash.io
KV_REST_API_TOKEN=xxx

Migration from Option A

If you're migrating from single-key storage (Option A):

  1. Your old data was stored as: blog-database{ posts: [...], lastUpdated: "..." }
  2. New storage uses: notioncms:blog:page:{id}{ ...page data }
  3. Run a full sync to migrate: await syncDataSource('blog', config)
  4. Update your read code to use createReader instead of direct Redis access

Examples

See the examples/ directory for complete examples:

  • nextjs-edge/ - Next.js Edge runtime example
  • express/ - Express.js example
  • basic/ - Minimal setup example

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

License

MIT

Troubleshooting

Webhook not receiving events

  • Verify your webhook URL is accessible
  • Check Notion integration has access to the database
  • Ensure webhook is configured in Notion settings

Pages not syncing

  • Check data source ID matches your Notion data source
  • Verify property IDs are correct (use inspect helper if available)
  • Check webhook logs for errors

Property not found

  • Ensure property ID is URL-encoded correctly
  • Fallback to propertyName if property ID is unavailable
  • Verify property type matches field mapping type

Support