---
title: "Runtime Sync (Optional)"
description: "Opt-in runtime page indexing for sites with dynamic content."
canonical_url: "https://nuxtseo.com/docs/ai-ready/guides/runtime-indexing"
last_updated: "2026-09-26T04:04:49.927Z"
---

Enable runtime sync to discover new sitemap routes and control the stored pages used by MCP and other database-backed features.
If each deployment prerenders your content, start with that build output.

## When You Need Runtime Sync

Enable runtime sync if your site has:

- **Dynamic pages** generated at runtime (e.g., user-generated content)
- **Frequently updated content** that changes between deploys
- **API-driven pages** where content comes from external sources

If content changes only on deploy, prerender the pages during that deployment.

## How It Works

1. Prerendering writes page data and a dump for the deployed server.
2. The server restores that dump when its database is empty.
3. Runtime sync seeds routes from the sitemap. Poll requests or cron fetch and index pending pages.

Seeding records routes. Poll and cron only process pending pages.
TTL expiry does not mark an already indexed page pending.
When an existing page changes between builds, call the authenticated reindex endpoint or a manual indexing utility.

## Enabling Runtime Sync

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  aiReady: {
    runtimeSync: {
      ttl: 3600, // Manual indexing freshness and sitemap refresh interval
      batchSize: 50, // Pages per batch
      pruneTtl: 0 // Prune routes not in sitemap (0 = never)
    },
    cron: true // Optional: scheduled background indexing (every 5 minutes)
  }
})
```

::note
The module generates a secret when you enable `runtimeSync` or `cron`.
Set `NUXT_AI_READY_RUNTIME_SYNC_SECRET` during setup to use a stable secret across deployments.
Send it in the `Authorization: Bearer <token>` header, including for status requests.
::

## Control Endpoints

When you enable `runtimeSync`, these endpoints become available.
For example, poll your deployed site with its configured secret:

```bash
curl --fail-with-body -X POST 'https://example.com/__ai-ready/poll' \
  -H "Authorization: Bearer $NUXT_AI_READY_RUNTIME_SYNC_SECRET"
```

The following request paths and responses are illustrative:

```text
# Check indexing progress (requires the Bearer header)
GET /__ai-ready/status
# Returns: { total: 50, indexed: 45, pending: 5 }

# Trigger batch indexing (requires the Bearer header)
POST /__ai-ready/poll
# Returns: { indexed: 20, remaining: 25, errors: [], duration: 1234, complete: false }

# Process one bounded batch with a timeout check
POST /__ai-ready/poll?all=true&timeout=30000
# Returns: { indexed: 45, remaining: 0, errors: [], duration: 28500, complete: true }

# Prune stale routes (dry run, preview what would be pruned)
POST /__ai-ready/prune?dry=true&ttl=604800
# Returns: { routes: ["/old-page"], count: 1, ttl: 604800, dry: true }

# Prune stale routes (execute, requires Authorization: Bearer <token> header)
POST /__ai-ready/prune?ttl=604800
# Returns: { pruned: 1, ttl: 604800, dry: false }

# Reindex a single route, e.g. after a CMS publish (requires Authorization: Bearer <token> header)
POST /__ai-ready/reindex?route=/about
# Returns: { route: "/about", indexed: true, contentChanged: true }

# Reindex only when the page is older than the TTL
POST /__ai-ready/reindex?route=/about&force=false
# Returns: { route: "/about", indexed: false, skipped: true }

# A failed fetch or convert answers 502
POST /__ai-ready/reindex?route=/broken
# Returns: { route: "/broken", indexed: false, error: "Failed to fetch HTML for /broken" }
```

Status and write requests require the configured Bearer token. A prune dry run does not require authentication.

### Poll Endpoint Options

Each request processes at most 50 pages, including `all=true`. If `remaining` is positive, make another request.

| Param     | Type      | Default                        | Description                                 |
| --------- | --------- | ------------------------------ | ------------------------------------------- |
| `limit`   | `number`  | `runtimeSync.batchSize` (`50`) | Max pages per batch (max: 50)               |
| `all`     | `boolean` | `false`                        | Use timeout checks within the bounded batch |
| `timeout` | `number`  | `30000`                        | Max ms for `all` mode                       |

### Prune Endpoint Options

| Param | Type      | Default           | Description                            |
| ----- | --------- | ----------------- | -------------------------------------- |
| `dry` | `boolean` | `false`           | Preview stale routes without deleting  |
| `ttl` | `number`  | `pruneTtl` config | Prune routes older than this (seconds) |

### Reindex Endpoint Options

| Param   | Type      | Default | Description                                                       |
| ------- | --------- | ------- | ----------------------------------------------------------------- |
| `route` | `string`  | -       | Route to index. Required. Must start with `/`                     |
| `force` | `boolean` | `true`  | Index even when the page is fresh. Set `false` to respect the TTL |

A route does not need to be in the sitemap or the database seed. Reindex is an authenticated admin action, so it may index any live route. Sitemap discovery supplies the normal pending routes. Poll and cron process pending database rows, including rows restored from build data.

## Scheduled Indexing

Cron discovers sitemap routes and processes pending pages. Use reindex for edits to existing pages between deployments.

When you set `cron: true`, Nitro scheduled tasks enable automatic background indexing (runs every 5 minutes):

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  aiReady: {
    cron: true // Runs every 5 minutes, auto-enables runtimeSync
  }
})
```

The module auto-enables `nitro.experimental.tasks` when you configure cron.

::warning
**Cloudflare Workers**: The module adds cron triggers to Nitro’s generated Wrangler configuration.

**Cloudflare Pages**: Keep `cron: true` to register the production HTTP endpoint. Use an external scheduler to call `GET /__ai-ready/cron` with `Authorization: Bearer <token>`{lang="html"} header. See the [Cloudflare guide](/docs/ai-ready/guides/cloudflare#scheduled-tasks-cron-cloudflare-pages) for details.
::

## TTL Configuration

| Option     | Default | Description                                                      |
| ---------- | ------- | ---------------------------------------------------------------- |
| `ttl`      | `3600`  | Manual indexing freshness and sitemap refresh interval (seconds) |
| `pruneTtl` | `0`     | Delete routes not in sitemap for this long (0 = never)           |

Force re-index regardless of TTL:

```ts
await indexPage('/about', html, { force: true })
```

## Sitemap Lastmod

A page's sitemap `lastmod` comes from a date it declares, such as an `article:modified_time` meta tag. If a page declares no date, runtime sync dates it from detected content changes:

- The first index gives the page no date.
- If a later runtime index finds a different body, the page gets the time of that index.
- If the body is the same, the stored date stays.

Rows restored from the prerendered dump never count as changed. Their hash comes from the build pipeline, so a difference proves nothing. Without runtime sync, a page with no declared date has no `lastmod`.

## Database Configuration

Runtime sync enables database storage. The module selects a driver from the deployment preset and your configuration:

| Platform                                           | Driver           | Extra dependency |
| -------------------------------------------------- | ---------------- | ---------------- |
| [Node.js](https://nodejs.org) 22.13+               | `node:sqlite`    | None             |
| Earlier [Node.js](https://nodejs.org)              | `better-sqlite3` | `better-sqlite3` |
| [Bun](https://bun.sh)                              | `bun:sqlite`     | None             |
| [Cloudflare](https://cloudflare.com)               | D1               | None             |
| [Vercel](https://vercel.com) (with `POSTGRES_URL`) | Neon Postgres    | None             |

On Node.js before 22.13, install the optional [`better-sqlite3`](https://github.com/WiseLibs/better-sqlite3) peer dependency yourself:

```bash
pnpm add better-sqlite3
```

For edge deployments, configure D1 or LibSQL:

::code-group
```ts [Cloudflare D1]
export default defineNuxtConfig({
  aiReady: {
    database: {
      type: 'd1',
      bindingName: 'DB'
    }
  }
})
```

```ts [Turso/LibSQL]
export default defineNuxtConfig({
  aiReady: {
    database: {
      type: 'libsql',
      url: process.env.TURSO_URL,
      authToken: process.env.TURSO_AUTH_TOKEN
    }
  }
})
```
::

## Serverless Cold Starts

A local [SQLite](https://sqlite.org) database may be temporary on a serverless deployment. D1, LibSQL, Neon, and [PostgreSQL](https://postgresql.org) provide external storage.

When a runtime database is empty, the module tries to restore `__ai-ready/pages.dump` from the build output.
If the build ID changes, it compares content hashes, adds new pages, and marks changed pages for indexing.

The dump must exist and remain accessible to the server. Prerender the pages you need in the initial index.

## Sync with External Systems

Use `ai-ready:page:indexed` after runtime indexing. Check `contentChanged` before sending Markdown to another service.

```ts [server/plugins/indexed-pages.ts]
export default defineNitroPlugin((nitro) => {
  nitro.hooks.hook('ai-ready:page:indexed', (ctx) => {
    if (!ctx.contentChanged)
      return

    console.info(`Changed page: ${ctx.route}`)
  })
})
```

Replace the log with your integration. The database write happens before this hook.
If an external request fails, retry that request separately; indexing unchanged content again produces `contentChanged: false`.
See the [IndexNow recipe](/docs/ai-ready/advanced/indexnow) for a complete changed-URL example.

## Manual Indexing

For a CMS publish, use the built-in authenticated reindex endpoint:

```bash
curl --fail-with-body -X POST 'https://example.com/__ai-ready/reindex?route=/about' \
  -H "Authorization: Bearer $NUXT_AI_READY_RUNTIME_SYNC_SECRET"
```

For trusted server code that already has HTML, use [`indexPage`](/docs/ai-ready/nitro-api/composables#indexpage).
Do not expose an unauthenticated endpoint that accepts arbitrary URLs to fetch and index.

## Direct Database Access

Use the [server composables](/docs/ai-ready/nitro-api/composables) for page queries.

| Function                                     | Description                                                     |
| -------------------------------------------- | --------------------------------------------------------------- |
| `queryPages(event, opts)`{lang="ts"}         | Query pages with filters and pagination                         |
| `searchPages(event, query, opts)`{lang="ts"} | Search stored pages with the configured database                |
| `countPages(event, opts)`{lang="ts"}         | Count pages matching criteria                                   |
| `streamPages(event, opts)`{lang="ts"}        | Read pages in batches                                           |
| `upsertPage(event, page)`{lang="ts"}         | Write prepared page data through an explicit `#ai-ready` import |

Raw SQL access uses `useRawDb` from `#ai-ready`. Prefer the query functions unless you need a database-specific operation.

## CLI

Use the `nuxt-ai-ready` CLI to interact with control endpoints:

```bash
# Check indexing status
npx nuxt-ai-ready status

# Trigger batch indexing
npx nuxt-ai-ready poll

# Use the server batch size, capped at 50 pages
npx nuxt-ai-ready poll --all

# Preview stale routes (dry run)
npx nuxt-ai-ready prune --dry

# Prune stale routes
npx nuxt-ai-ready prune --ttl 604800

# Reindex a single route
npx nuxt-ai-ready reindex /about
```

The CLI reads the secret cached in your local project. That secret must match the deployed server. See the [CLI guide](/docs/ai-ready/guides/cli) for all commands and options.

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
