# How to Integrate Bolt.new with Notion

- Tool: Bolt.new
- Difficulty: Intermediate
- Time required: 25 minutes
- Last updated: April 2026

## TL;DR

Connect Bolt.new to Notion using the Notion REST API with an internal integration token. Use Notion as a content backend — pull database items as blog posts, wiki articles, or project data into your Bolt app. All outbound Notion API calls work in Bolt's WebContainer preview. Share each database or page with your integration before querying it. Notion's block-based content requires extra parsing for rich text rendering.

## Use Notion as a Headless CMS and Content Backend for Your Bolt.new App

Notion has become one of the most popular tools for teams to organize knowledge, manage projects, and create content. Its combination of structured databases (with filtering, sorting, and relational fields) and rich document pages makes it uniquely flexible as a backend. For Bolt-built applications, this means you can let non-technical team members create and edit content in Notion's familiar interface while your app queries and displays that content dynamically — essentially using Notion as a headless CMS without paying for a dedicated CMS subscription.

The Notion API v1 is a clean, well-documented REST API using bearer token authentication. Every workspace member can create internal integrations that receive an integration token — no OAuth flow needed for single-workspace apps. The main design decision in Notion's API is the separation between database queries (structured, filterable) and page content (block-based, recursive). Database items are easy to work with: query returns an array of page objects with typed properties matching your database columns. Page bodies are more complex: content is represented as a tree of blocks, each with a type (paragraph, heading_1, bulleted_list_item, code, image, etc.) and content, requiring recursive fetching to get full page trees.

For most Bolt integration use cases — blog backends, documentation portals, project trackers, content management — reading database properties is sufficient and avoids the complexity of block rendering. If you need to display full page bodies (articles, documentation), the @notionhq/client SDK's block-to-HTML conversion utilities simplify the process considerably. The combination of Notion's editing experience and Bolt's rapid UI generation makes this pairing particularly effective for content-driven internal tools.

## Before you start

- A Bolt.new account with a Next.js project
- A Notion account at notion.so (free plan works)
- A Notion database or page you want to connect — create one if you do not have one
- A Notion internal integration token from notion.so/my-integrations
- The database or page shared with your integration (connection added in Notion)

## Step-by-step guide

### 1. Create a Notion Internal Integration and Get the Token

Notion integrations authenticate using a secret token tied to a specific workspace. Internal integrations are the simplest type — no OAuth flow, no redirect URIs, just a token you create and use immediately. They are appropriate for any app where you control both the Notion workspace and the application.

To create an integration, go to notion.so/my-integrations and click New integration. Give it a descriptive name like 'Bolt App' or 'My Website Backend.' Select the workspace it should access from the dropdown — if you have multiple workspaces, choose the one containing your databases. Set the logo if you like (optional). Under Capabilities, review the permissions: Read content, Update content, and Insert content cover most use cases. Read user information without email is sufficient for fetching user details on pages; you rarely need email addresses from the API.

Click Submit and you will see your integration's Internal Integration Secret, which starts with 'secret_'. Copy this immediately and add it to your Bolt project's .env file as NOTION_TOKEN.

The critical step that many developers miss: you must explicitly share each Notion database or page with your integration before the API can access it. Open the Notion database you want to query, click the three-dot menu (or the Share button at the top right), then click Connections and search for your integration name. Click the integration to add the connection. Without this sharing step, every API call to that database will return a 404 error with 'Could not find database with id' — even though the database exists and your token is valid.

Also note your Database ID. Open the Notion database in your browser. The URL has the format notion.so/{workspaceName}/{databaseId}?v={viewId}. The database ID is the 32-character string before the query mark, formatted as xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. Add this to .env as NOTION_DATABASE_ID.

```
// lib/notion.ts
const NOTION_BASE_URL = 'https://api.notion.com/v1';
const NOTION_VERSION = '2022-06-28';

function getHeaders() {
  const token = process.env.NOTION_TOKEN;
  if (!token) throw new Error('NOTION_TOKEN must be set in .env');

  return {
    Authorization: `Bearer ${token}`,
    'Notion-Version': NOTION_VERSION,
    'Content-Type': 'application/json',
  };
}

export async function notionGet<T = unknown>(path: string): Promise<T> {
  const response = await fetch(`${NOTION_BASE_URL}${path}`, {
    headers: getHeaders(),
  });
  if (!response.ok) {
    const err = await response.json().catch(() => ({})) as { message?: string };
    throw new Error(err.message || `Notion API error: ${response.status}`);
  }
  return response.json() as Promise<T>;
}

export async function notionPost<T = unknown>(path: string, body?: unknown): Promise<T> {
  const response = await fetch(`${NOTION_BASE_URL}${path}`, {
    method: 'POST',
    headers: getHeaders(),
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!response.ok) {
    const err = await response.json().catch(() => ({})) as { message?: string };
    throw new Error(err.message || `Notion API error: ${response.status}`);
  }
  return response.json() as Promise<T>;
}

export async function notionPatch<T = unknown>(path: string, body: unknown): Promise<T> {
  const response = await fetch(`${NOTION_BASE_URL}${path}`, {
    method: 'PATCH',
    headers: getHeaders(),
    body: JSON.stringify(body),
  });
  if (!response.ok) {
    const err = await response.json().catch(() => ({})) as { message?: string };
    throw new Error(err.message || `Notion API error: ${response.status}`);
  }
  return response.json() as Promise<T>;
}
```

**Expected result:** The lib/notion.ts helper is configured. Test the connection by calling notionGet('/users/me') from an API route — you should see the bot user profile for your integration, confirming the token is working.

### 2. Query a Notion Database via API Route

Querying a Notion database returns an array of page objects, each representing one row in the database. Each page object has a properties field containing all the column values for that row, with the structure varying by property type: title properties contain rich_text arrays; select properties contain a select object with name and color; date properties contain a date object with start and optionally end values; checkbox properties contain a boolean; number properties contain a number; relation properties contain an array of page IDs.

The database query endpoint is POST /v1/databases/{database_id}/query. Despite being a data retrieval operation, Notion uses POST (not GET) for queries so you can include a complex filter and sort body. The filter object supports compound conditions using and and or arrays, individual property filters with field-specific filter types, and nested conditions. The sort array accepts property name plus direction pairs.

For a simple published articles filter: pass a filter object with a property matching your status column name, a select filter type, and an equals condition for 'Published'. For date-sorted results, pass a sort array with the date property name and 'descending' direction.

Notion returns a maximum of 100 results per query by default. For pagination, check the has_more boolean in the response. If true, use the next_cursor value as the start_cursor in the next query. For most content management use cases, 100 results are sufficient without pagination.

The response structure from Notion can be verbose — each property value is wrapped in type-specific nested objects. Write a helper function that extracts the plain text value from each property type, converting Notion's property format to simple strings and primitives that are easier to work with in your React components. This mapping function dramatically simplifies your component code.

```
// app/api/notion/database/route.ts
import { NextResponse } from 'next/server';
import { notionPost } from '@/lib/notion';

type NotionPropertyValue =
  | { type: 'title'; title: Array<{ plain_text: string }> }
  | { type: 'rich_text'; rich_text: Array<{ plain_text: string }> }
  | { type: 'select'; select: { name: string; color: string } | null }
  | { type: 'multi_select'; multi_select: Array<{ name: string }> }
  | { type: 'date'; date: { start: string; end: string | null } | null }
  | { type: 'checkbox'; checkbox: boolean }
  | { type: 'number'; number: number | null }
  | { type: 'url'; url: string | null }
  | { type: 'email'; email: string | null };

function extractProperty(value: NotionPropertyValue): string | boolean | number | null {
  if (!value) return null;
  switch (value.type) {
    case 'title': return value.title.map(t => t.plain_text).join('');
    case 'rich_text': return value.rich_text.map(t => t.plain_text).join('');
    case 'select': return value.select?.name ?? null;
    case 'multi_select': return value.multi_select.map(s => s.name).join(', ');
    case 'date': return value.date?.start ?? null;
    case 'checkbox': return value.checkbox;
    case 'number': return value.number;
    case 'url': return value.url;
    case 'email': return value.email;
    default: return null;
  }
}

const cache = new Map<string, { data: unknown; expiresAt: number }>();

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const databaseId = searchParams.get('databaseId') || process.env.NOTION_DATABASE_ID;
  const sortProperty = searchParams.get('sortProperty');
  const sortDirection = searchParams.get('sortDirection') || 'descending';

  if (!databaseId) {
    return NextResponse.json({ error: 'databaseId is required' }, { status: 400 });
  }

  const cacheKey = `${databaseId}-${sortProperty}-${sortDirection}`;
  const cached = cache.get(cacheKey);
  if (cached && Date.now() < cached.expiresAt) {
    return NextResponse.json(cached.data);
  }

  try {
    const body: Record<string, unknown> = {};
    if (sortProperty) {
      body.sorts = [{ property: sortProperty, direction: sortDirection }];
    }

    const data = await notionPost<{ results: Array<{ id: string; properties: Record<string, NotionPropertyValue> }>; has_more: boolean }>(
      `/databases/${databaseId}/query`,
      body
    );

    const items = data.results.map((page) => ({
      id: page.id,
      ...Object.fromEntries(
        Object.entries(page.properties).map(([key, val]) => [key, extractProperty(val)])
      ),
    }));

    const result = { items, total: items.length };
    cache.set(cacheKey, { data: result, expiresAt: Date.now() + 60_000 });
    return NextResponse.json(result);
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Failed to query Notion database';
    return NextResponse.json({ error: message }, { status: 500 });
  }
}
```

**Expected result:** The API route queries your Notion database and returns a flat array of items with extracted property values. Test it in the Bolt preview by hitting the route URL and verifying the items array matches your Notion database rows.

### 3. Fetch and Render Notion Page Block Content

Notion pages are composed of blocks arranged in a tree structure. To display the body of a Notion page — an article, documentation page, or wiki entry — you need to fetch the page's child blocks recursively and convert them to renderable HTML or React elements.

The blocks endpoint is GET /v1/blocks/{block_id}/children. Pass the page ID as the block ID to get top-level blocks. Each block object has a type field and a corresponding data field with the same name (a paragraph block has a paragraph field, a heading_1 block has a heading_1 field, etc.). Text content within blocks uses Notion's rich_text format — an array of text objects with the actual content in plain_text and optional annotations for bold, italic, underline, strikethrough, and code formatting.

Some block types contain child blocks (toggle blocks, bulleted lists, numbered lists, columns). For full page rendering, you need to recursively fetch child blocks for any block with has_children set to true. This can require multiple API calls for complex pages. For simple content pages (primarily paragraphs and headings without nested structures), a single blocks fetch is sufficient.

For a practical rendering implementation, iterate the blocks array and convert each block type to an HTML string or React element. Handle the essential types: paragraph, heading_1, heading_2, heading_3, bulleted_list_item, numbered_list_item, to_do, code, image, callout, quote, and divider. Rich text annotations (bold, italic, code inline) require wrapping the text segments in appropriate HTML tags like strong, em, and code.

For code blocks, Notion includes a language field — use this to add the appropriate language class for syntax highlighting with Prism.js or Highlight.js. Image blocks contain a file object with a url that expires after one hour — do not cache image URLs for longer than that.

```
// app/api/notion/page/[id]/route.ts
import { NextResponse } from 'next/server';
import { notionGet } from '@/lib/notion';

interface RichText {
  plain_text: string;
  annotations: { bold: boolean; italic: boolean; code: boolean; strikethrough: boolean };
  href: string | null;
}

function richTextToHtml(richText: RichText[]): string {
  return richText
    .map((t) => {
      let text = t.plain_text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
      if (t.annotations.code) text = `<code>${text}</code>`;
      if (t.annotations.bold) text = `<strong>${text}</strong>`;
      if (t.annotations.italic) text = `<em>${text}</em>`;
      if (t.annotations.strikethrough) text = `<del>${text}</del>`;
      if (t.href) text = `<a href="${t.href}">${text}</a>`;
      return text;
    })
    .join('');
}

type Block = {
  type: string;
  id: string;
  has_children: boolean;
  [key: string]: unknown;
};

function blockToHtml(block: Block): string {
  const type = block.type;
  const data = block[type] as Record<string, unknown>;
  const rt = (data?.rich_text as RichText[]) || [];
  const text = richTextToHtml(rt);

  switch (type) {
    case 'paragraph': return `<p>${text || '&nbsp;'}</p>`;
    case 'heading_1': return `<h1>${text}</h1>`;
    case 'heading_2': return `<h2>${text}</h2>`;
    case 'heading_3': return `<h3>${text}</h3>`;
    case 'bulleted_list_item': return `<li>${text}</li>`;
    case 'numbered_list_item': return `<li>${text}</li>`;
    case 'code': {
      const lang = (data?.language as string) || 'text';
      return `<pre><code class="language-${lang}">${(data?.rich_text as RichText[]).map(t => t.plain_text).join('')}</code></pre>`;
    }
    case 'image': {
      const imgData = data as { type: string; file?: { url: string }; external?: { url: string } };
      const url = imgData.type === 'external' ? imgData.external?.url : imgData.file?.url;
      return `<img src="${url}" alt="" style="max-width:100%" />`;
    }
    case 'callout': return `<div class="callout">${text}</div>`;
    case 'quote': return `<blockquote>${text}</blockquote>`;
    case 'divider': return '<hr />';
    case 'to_do': {
      const checked = (data?.checked as boolean) ? 'checked' : '';
      return `<label><input type="checkbox" ${checked} disabled /> ${text}</label>`;
    }
    default: return text ? `<p>${text}</p>` : '';
  }
}

export async function GET(
  _request: Request,
  { params }: { params: { id: string } }
) {
  try {
    const [pageData, blocksData] = await Promise.all([
      notionGet<{ properties: Record<string, { title?: Array<{ plain_text: string }> }> }>(`/pages/${params.id}`),
      notionGet<{ results: Block[] }>(`/blocks/${params.id}/children`),
    ]);

    const titleProp = Object.values(pageData.properties).find(p => p.title);
    const title = titleProp?.title?.map(t => t.plain_text).join('') || 'Untitled';

    const blocks = blocksData.results;
    let html = '';
    let inUl = false;
    let inOl = false;

    for (const block of blocks) {
      if (block.type === 'bulleted_list_item') {
        if (!inUl) { html += '<ul>'; inUl = true; }
        html += blockToHtml(block);
      } else {
        if (inUl) { html += '</ul>'; inUl = false; }
        if (block.type === 'numbered_list_item') {
          if (!inOl) { html += '<ol>'; inOl = true; }
          html += blockToHtml(block);
        } else {
          if (inOl) { html += '</ol>'; inOl = false; }
          html += blockToHtml(block);
        }
      }
    }
    if (inUl) html += '</ul>';
    if (inOl) html += '</ol>';

    return NextResponse.json({ title, html });
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Failed to fetch page';
    return NextResponse.json({ error: message }, { status: 500 });
  }
}
```

**Expected result:** The page route fetches a Notion page's block content and returns it as an HTML string. Test by passing a page ID from your database results — the response should include the page title and HTML-formatted body content ready for rendering with dangerouslySetInnerHTML.

### 4. Create Notion Pages from Bolt Form Submissions

Creating new pages in a Notion database via the API allows your Bolt app to act as an intake form for Notion workflows. Form submissions, user requests, lead captures, and content submissions can all create structured records in your Notion database, ready for team review in Notion's native interface.

The create page endpoint is POST /v1/pages. The request body requires a parent object specifying the database ID, a properties object containing the new page's property values (matching your database schema exactly), and optionally a children array for the page body content.

Properties in the create payload use a typed structure that mirrors the read format. Title properties require a title array of rich_text objects. Select properties require a select object with the option name. Date properties require a date object with a start string. Number properties require a number value directly. Checkbox properties require a boolean.

Validation before the API call is important because Notion returns 400 errors for invalid property values — attempting to set a select option that does not exist in the database's defined options list returns an error. Fetch the database schema first (GET /v1/databases/{id}) to get the list of valid select options if you are rendering option choices dynamically.

For the page body content (optional but useful for notes or descriptions), include a children array with block objects. The create and blocks API use the same block format, so you can create a page with pre-populated content paragraphs, checklists, or any other supported block type in a single request. This is more efficient than creating the page first and then appending blocks separately.

```
// app/api/notion/create/route.ts
import { NextResponse } from 'next/server';
import { notionPost } from '@/lib/notion';

interface CreatePageBody {
  title: string;
  status?: string;
  priority?: string;
  notes?: string;
  dueDate?: string;
}

export async function POST(request: Request) {
  const body = await request.json() as CreatePageBody;
  const databaseId = process.env.NOTION_DATABASE_ID;

  if (!databaseId) {
    return NextResponse.json({ error: 'NOTION_DATABASE_ID is not configured' }, { status: 500 });
  }

  if (!body.title?.trim()) {
    return NextResponse.json({ error: 'title is required' }, { status: 400 });
  }

  const properties: Record<string, unknown> = {
    Name: {
      title: [{ text: { content: body.title } }],
    },
  };

  if (body.status) {
    properties.Status = { select: { name: body.status } };
  }
  if (body.priority) {
    properties.Priority = { select: { name: body.priority } };
  }
  if (body.dueDate) {
    properties['Due Date'] = { date: { start: body.dueDate } };
  }

  const pageBody: Record<string, unknown> = {
    parent: { database_id: databaseId },
    properties,
  };

  if (body.notes?.trim()) {
    pageBody.children = [
      {
        object: 'block',
        type: 'paragraph',
        paragraph: {
          rich_text: [{ type: 'text', text: { content: body.notes } }],
        },
      },
    ];
  }

  try {
    const page = await notionPost<{ id: string; url: string }>('/pages', pageBody);
    return NextResponse.json({ success: true, id: page.id, url: page.url });
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Failed to create page';
    return NextResponse.json({ error: message }, { status: 500 });
  }
}
```

**Expected result:** Submitting the form creates a new page in your Notion database. Open Notion and refresh the database view — the new entry should appear with all the properties you submitted. The API route returns the new page's ID and the direct Notion URL.

## Best practices

- Always share each Notion database or page with your integration explicitly — the API does not discover databases automatically, and missing this step is the single most common setup error.
- Never expose NOTION_TOKEN with a NEXT_PUBLIC_ prefix — integration tokens grant full write access to your workspace and must only be used server-side in API routes.
- Cache Notion API responses for 60 seconds or more — the API has rate limits of 3 requests per second and most content (blog posts, documentation) does not change in real time.
- Define TypeScript interfaces for your specific Notion database schema to get type safety when extracting property values — the generic response types are useful but database-specific types catch errors early.
- Use Notion Views to pre-filter and pre-sort database content — the API supports a filter_properties query param and you can build filters that mirror your saved views, reducing query complexity in code.
- Handle Notion's pagination — databases with more than 100 entries return a next_cursor; always check has_more in responses and implement cursor-based pagination for complete data access.
- Avoid storing Notion image URLs longer than one hour — file URLs (not external URLs) expire and must be freshly fetched each session.
- Test page creation with a dedicated test database before connecting to production data — Notion has no undo for API-created pages, and bulk creation errors can pollute a real workspace database.

## Use cases

### Notion-Powered Blog or Documentation Site

Use a Notion database as the content management system for a blog or documentation site. Team members write posts and docs in Notion, setting properties like title, category, publish date, and published status in the database columns. Your Bolt app queries only published items, renders the list view from database properties, and fetches full page block content for individual article pages.

Prompt example:

```
Build a blog powered by Notion. Create /api/notion/posts that queries a Notion database using POST to https://api.notion.com/v1/databases/{DATABASE_ID}/query with a filter for Status = 'Published', sorted by PublishDate descending. Return title, slug, category, publishDate, excerpt, and coverImage from page properties. Create /api/notion/posts/[id] that fetches page blocks from /v1/blocks/{id}/children and converts them to a simple HTML string (handle paragraph, heading_1-3, bulleted_list_item, numbered_list_item, code, and image block types). Build a blog index page and a post detail page. Store NOTION_TOKEN and NOTION_DATABASE_ID in process.env.
```

### Project Tracker from Notion Database

Build a custom project tracker view that reads project data from a Notion database and presents it with a more focused interface than Notion's native views. Filter and sort projects by status, owner, or deadline. Display a Kanban-style board or a priority-sorted list. Let team members update project status directly from the Bolt interface via the Notion API's page update endpoint.

Prompt example:

```
Create a project tracker dashboard using a Notion database as the backend. Build /api/notion/projects that queries the database with sorting by Priority (High/Medium/Low) and filtering to exclude Archived projects. Map Notion properties to: name, status, owner, dueDate, priority, description. Build a React Kanban board with columns for each status value (Planning, In Progress, Review, Done). Each card shows the project name, owner avatar initials, priority badge, and due date. Clicking a card opens a detail panel with the full description. Add a status dropdown on each card that calls PATCH /api/notion/projects/[id]/status to update the Status property.
```

### Content Submission Form that Creates Notion Pages

Build a form that creates new entries in a Notion database on submission — useful for content submission workflows, team request trackers, or lead capture systems. The form collects structured data that maps to Notion database properties, and the API route creates a new page in the database with those property values set.

Prompt example:

```
Build a content submission form that creates Notion database entries. Create /api/notion/submit that accepts POST with { title, category, content, authorName, authorEmail } and calls POST to https://api.notion.com/v1/pages with parent: { database_id: NOTION_DATABASE_ID }, properties for Title, Category (select), Author, Email, and Status set to 'Pending', plus the content as a paragraph block in the page body. Build a React form with title text input, category selector, content textarea, author name, and email fields. Show a success message with a link to view the submission in Notion after creation.
```

## Troubleshooting

### API returns 404 with 'Could not find database with id' even though the database exists

Cause: The Notion integration has not been given access to the database. Every database must be explicitly connected to the integration — Notion does not automatically grant integrations access to all databases in the workspace.

Solution: Open the Notion database, click the three-dot menu at the top right (or the Share button), select Connections, and search for your integration name. Click it to add the connection. If you created the integration recently, you may need to refresh the Connections list. After adding the connection, the API should be able to query the database immediately.

### Properties in API response have unexpected structure or missing values

Cause: Notion property values are wrapped in type-specific nested objects. Reading a title property as a plain string fails — the actual text is nested inside page.properties.Name.title[0].plain_text. Different property types have completely different structures.

Solution: Use the extractProperty helper function to normalize all property types to simple values. Log the raw API response first to understand the exact structure of your specific database columns, then write type-specific extraction logic for each property type you need.

```
// For a title property named 'Name':
const name = page.properties.Name.title.map(t => t.plain_text).join('');
// For a select property named 'Status':
const status = page.properties.Status.select?.name;
// For a date property named 'Due Date':
const dueDate = page.properties['Due Date'].date?.start;
```

### Creating a page fails with 400 and 'body.properties.Status.select.name should be a valid select option name'

Cause: The select option name in the create request does not match any existing option in the database column. Notion select columns have a predefined list of options, and the API rejects any option name not in that list.

Solution: Fetch the database schema with GET /v1/databases/{id} and read the properties.Status.select.options array to get the exact option names. Use these names in your form's dropdown and in the API payload. Alternatively, prompt Bolt to add a new option to the Notion database column directly.

```
// Fetch valid select options before creating:
const db = await notionGet(`/databases/${databaseId}`);
const statusOptions = db.properties.Status.select.options.map(o => o.name);
// Use statusOptions to populate your form dropdown
```

### Page content (blocks) not rendering correctly — blocks appear as empty paragraphs

Cause: Nested blocks (list items inside a toggle, text inside a column) require recursive fetching. The initial /blocks/{id}/children call only returns the top-level blocks — blocks with has_children: true contain additional child blocks that must be fetched with a second API call.

Solution: For complex pages with toggles, columns, or nested lists, recursively fetch child blocks for any block where has_children is true. For simple pages with only paragraphs and headings, the single fetch is sufficient. Check the has_children field on each block in your initial response to determine if additional fetching is needed.

```
// Recursive block fetching for complex pages:
async function fetchBlocksRecursively(blockId: string): Promise<Block[]> {
  const data = await notionGet<{ results: Block[] }>(`/blocks/${blockId}/children`);
  const blocks = data.results;
  for (const block of blocks) {
    if (block.has_children) {
      block.children = await fetchBlocksRecursively(block.id);
    }
  }
  return blocks;
}
```

## Frequently asked questions

### Can I use the Notion API in Bolt's WebContainer preview without deploying?

Yes — all outbound Notion API calls work from Next.js API routes in the Bolt WebContainer. You can query databases, fetch page content, and create new pages during development. Notion does not have a webhook feature that requires incoming connections, so there is no deployment requirement for standard read/write operations.

### Does Bolt.new have a native Notion integration?

No — Bolt.new does not include a built-in Notion connector. The integration uses Notion's REST API directly from Next.js API routes with an internal integration token. Bolt's AI can generate all the integration code from a description of your use case, making setup fast even without a native connector.

### How do I access a Notion database I did not create?

Ask a workspace member with edit access to the database to add your integration as a connection. Open the database, click Share, go to Connections, and add the integration. Integration access to Notion is database-specific, not workspace-wide, so each database must be shared individually. You cannot access a database that has not been explicitly connected to your integration.

### What is the Notion API rate limit?

The Notion API allows up to 3 requests per second per integration. For dashboard pages that make multiple parallel requests on load, this limit can be reached quickly. Add a 60-second in-memory cache on your API routes to reduce Notion API calls significantly. For high-traffic apps, consider scheduling periodic data syncs to a local database rather than querying Notion on every user request.

### Can I use Notion as a CMS for a public website built with Bolt?

Yes — this is one of the most common Notion integration patterns. Create a Notion database with your content (blog posts, portfolio items, product listings), write your Bolt app to fetch and display it, and have non-technical team members manage all content in Notion. The main consideration is caching: cache Notion responses for at least 60 seconds to stay within rate limits and reduce latency for public site visitors.

### How do I render Notion page content with proper formatting in React?

Fetch the page blocks from /v1/blocks/{pageId}/children and convert each block type to HTML or React elements. The block-to-HTML approach returns an HTML string you render with dangerouslySetInnerHTML (safe since you control the Notion content). Alternatively, use the @notionhq/client SDK's page rendering utilities, or the notion-to-md npm package which converts Notion blocks to Markdown for display with a Markdown renderer like react-markdown.

---

Source: https://www.rapidevelopers.com/bolt-ai-integrations/notion
© RapidDev — https://www.rapidevelopers.com/bolt-ai-integrations/notion
