---
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-05-06T18:46:01.227Z"
---

**Most sites don't need this.** Prerendering handles page indexing automatically. Use runtime sync only for sites with frequently changing content that you can't prerender.

## 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 your content only changes on deploy, stick with prerendering - it's faster and simpler.

## How It Works

```text
┌─────────────────────────────────────────────────────────────┐
│ Default: Prerendering (source of truth)                     │
│   Build time → crawl sitemap → create SQLite → compress    │
│   Cold start → restore dump → llms.txt works immediately!  │
├─────────────────────────────────────────────────────────────┤
│ Opt-in: Runtime Sync (for dynamic content)                  │
│   Cold start → restore dump → sitemap seeder → poll        │
│   Cron (optional) → background re-indexing of stale pages  │
└─────────────────────────────────────────────────────────────┘
```

## Enabling Runtime Sync

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  aiReady: {
    runtimeSync: {
      ttl: 3600, // Re-index pages & refresh sitemap older than 1 hour
      batchSize: 20, // Pages per batch
      pruneTtl: 0 // Prune routes not in sitemap (0 = never)
    },
    cron: true // Optional: scheduled background indexing (every minute)
  }
})
```

<callout color="blue" icon="i-heroicons-information-circle">

The module auto-generates a `runtimeSyncSecret` when you enable `runtimeSync` or `cron`. Set `NUXT_AI_READY_RUNTIME_SYNC_SECRET` env var to use your own.

</callout>

## Control Endpoints

When you enable `runtimeSync`, these endpoints become available:

```bash
# Check indexing progress
GET /__ai-ready/status
# Returns: { total: 50, indexed: 45, pending: 5 }

# Trigger batch indexing (requires Authorization: Bearer <token> header if secret configured)
POST /__ai-ready/poll
# Returns: { indexed: 20, remaining: 25, errors: [], duration: 1234, complete: false }

# Process all pending pages (with timeout)
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 }
```

You must provide the `Authorization: Bearer <token>` header for POST endpoints if you configure `runtimeSyncSecret`.

### Poll Endpoint Options

<table>
<thead>
  <tr>
    <th>
      Param
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        limit
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        10
      </code>
    </td>
    
    <td>
      Max pages per batch (max: 50)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        all
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Process until complete
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        timeout
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        30000
      </code>
    </td>
    
    <td>
      Max ms for <code>
        all
      </code>
      
       mode
    </td>
  </tr>
</tbody>
</table>

### Prune Endpoint Options

<table>
<thead>
  <tr>
    <th>
      Param
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        dry
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Preview stale routes without deleting
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ttl
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        pruneTtl
      </code>
      
       config
    </td>
    
    <td>
      Prune routes older than this (seconds)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        secret
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      -
    </td>
    
    <td>
      Auth token (required unless dry run)
    </td>
  </tr>
</tbody>
</table>

## Scheduled Indexing

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

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

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

<callout color="amber" icon="i-heroicons-exclamation-triangle">

**Cloudflare Workers**: Cron triggers are auto-configured in wrangler.toml.

**Cloudflare Pages**: Does not support cron triggers. Use an external scheduler to call `GET /__ai-ready/cron` with `Authorization: Bearer <token>` header. See the [Cloudflare guide](/docs/ai-ready/guides/cloudflare#cloudflare-pages) for details.

</callout>

## TTL Configuration

<table>
<thead>
  <tr>
    <th>
      Option
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        ttl
      </code>
    </td>
    
    <td>
      <code>
        3600
      </code>
    </td>
    
    <td>
      Re-index pages & refresh sitemap older than this (seconds)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        pruneTtl
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      Delete routes not in sitemap for this long (0 = never)
    </td>
  </tr>
</tbody>
</table>

Force re-index regardless of TTL:

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

## Database Configuration

The module uses [SQLite](https://sqlite.org) via [db0](https://db0.unjs.io/) for cross-platform support. It auto-detects the best connector:

<table>
<thead>
  <tr>
    <th>
      Runtime
    </th>
    
    <th>
      Connector
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <a href="https://bun.sh" rel="nofollow">
        Bun
      </a>
    </td>
    
    <td>
      <code>
        bun:sqlite
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <a href="https://nodejs.org" rel="nofollow">
        Node.js
      </a>
      
       22.5+
    </td>
    
    <td>
      <code>
        node:sqlite
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Node.js <22.5
    </td>
    
    <td>
      <code>
        better-sqlite3
      </code>
    </td>
  </tr>
</tbody>
</table>

For edge deployments, configure D1 or LibSQL:

<code-group>

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

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

</code-group>

## Serverless Cold Starts

For serverless platforms (Vercel, [Netlify](https://netlify.com), Lambda), the database is ephemeral. The module handles this by:

1. **Build time**: Creates SQLite database and compresses it to `__ai-ready/pages.dump`
2. **Cold start**: `db-restore` plugin decompresses dump and imports into fresh database
3. **Runtime** (if enabled): New pages indexed via sitemap seeder

Prerendered pages are always available, even on cold starts.

## Sync with External Systems

Use the `ai-ready:page:indexed` hook to sync with vector databases, search indexes, or analytics:

```ts [server/plugins/embeddings.ts]
export default defineNitroPlugin((nitro) => {
  nitro.hooks.hook('ai-ready:page:indexed', async (ctx) => {
    // Generate embeddings for vector search
    const embedding = await openai.embeddings.create({
      model: 'text-embedding-3-small',
      input: ctx.markdown
    })

    await vectorDb.upsert({
      id: ctx.route,
      vector: embedding.data[0].embedding,
      metadata: {
        title: ctx.title,
        description: ctx.description,
        route: ctx.route
      }
    })

    console.log(`Indexed ${ctx.route} (update: ${ctx.isUpdate})`)
  })
})
```

## Manual Indexing

Trigger indexing from API routes or plugins:

```ts [server/api/reindex.post.ts]
import { indexPageByRoute } from '#ai-ready'

export default defineEventHandler(async (event) => {
  const { paths } = await readBody(event)

  const results = await Promise.all(
    paths.map((path: string) =>
      indexPageByRoute(path, event, { force: true })
    )
  )

  return {
    indexed: results.filter(r => r.success && !r.skipped).length,
    skipped: results.filter(r => r.skipped).length,
    failed: results.filter(r => !r.success).length
  }
})
```

## Direct Database Access

For advanced use cases, access the database directly:

```ts [server/api/search.ts]
import { searchPages, useDatabase } from '#ai-ready'

export default defineEventHandler(async (event) => {
  const { q } = getQuery(event)

  // FTS5 full-text search
  const results = await searchPages(event, q as string, { limit: 10 })

  return results
})
```

Available query functions:

<table>
<thead>
  <tr>
    <th>
      Function
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code className="language-ts shiki shiki-themes github-light github-light material-theme-palenight" language="ts" style="">
        <span class="s0YkB">
          queryPages
        </span>
        
        <span class="sqjlB">
          (event
        </span>
        
        <span class="sx-uw">
          ,
        </span>
        
        <span class="sqjlB">
          opts)
        </span>
      </code>
    </td>
    
    <td>
      Query pages with filters/pagination
    </td>
  </tr>
  
  <tr>
    <td>
      <code className="language-ts shiki shiki-themes github-light github-light material-theme-palenight" language="ts" style="">
        <span class="s0YkB">
          searchPages
        </span>
        
        <span class="sqjlB">
          (event
        </span>
        
        <span class="sx-uw">
          ,
        </span>
        
        <span class="sqjlB">
          query
        </span>
        
        <span class="sx-uw">
          ,
        </span>
        
        <span class="sqjlB">
          opts)
        </span>
      </code>
    </td>
    
    <td>
      FTS5 full-text search
    </td>
  </tr>
  
  <tr>
    <td>
      <code className="language-ts shiki shiki-themes github-light github-light material-theme-palenight" language="ts" style="">
        <span class="s0YkB">
          countPages
        </span>
        
        <span class="sqjlB">
          (event
        </span>
        
        <span class="sx-uw">
          ,
        </span>
        
        <span class="sqjlB">
          opts)
        </span>
      </code>
    </td>
    
    <td>
      Count pages matching criteria
    </td>
  </tr>
  
  <tr>
    <td>
      <code className="language-ts shiki shiki-themes github-light github-light material-theme-palenight" language="ts" style="">
        <span class="s0YkB">
          streamPages
        </span>
        
        <span class="sqjlB">
          (event
        </span>
        
        <span class="sx-uw">
          ,
        </span>
        
        <span class="sqjlB">
          opts)
        </span>
      </code>
    </td>
    
    <td>
      Stream pages for large datasets
    </td>
  </tr>
  
  <tr>
    <td>
      <code className="language-ts shiki shiki-themes github-light github-light material-theme-palenight" language="ts" style="">
        <span class="s0YkB">
          upsertPage
        </span>
        
        <span class="sqjlB">
          (event
        </span>
        
        <span class="sx-uw">
          ,
        </span>
        
        <span class="sqjlB">
          page)
        </span>
      </code>
    </td>
    
    <td>
      Insert or update a page
    </td>
  </tr>
</tbody>
</table>

## IndexNow Integration

Combine runtime sync with [IndexNow](/docs/ai-ready/guides/indexnow) to notify search engines instantly when pages change. When you enable both `runtimeSync.cron` and `indexNow`, IndexNow syncs automatically after each scheduled indexing run.

## 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

# Process all pending 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
```

The CLI auto-authenticates using the generated secret. See the [CLI guide](/docs/ai-ready/guides/cli) for all commands and options.
