Valcraven Docs
Features

Search

The three search surfaces in the template — full-text item search (FTS5/LIKE, session-scoped), public docs search, and admin-only dev wiki search — all built on fumadocs and SQLite.

Search

Valcraven ships three distinct search endpoints, each for a different corpus:

EndpointSearchesAuthBacked by
GET /api/searchThe user's own items (app data)Signed-in userlib/search.ts → the raw DB adapter
GET /api/docs-searchThe public customer docs (/docs)Publicfumadocs createFromSource(docsSource)
GET /api/wiki-searchThe dev wiki (/admin/docs)Admin onlyfumadocs createFromSource(devDocsSource)

A fourth, internal surface powers the AI chat: a D1 FTS5 index over the full corpus (customer docs and dev wiki), queried server-to-server by the realtime Worker. It's not a user-facing endpoint — see the dev wiki's Docs Retrieval (FTS5) page for how it works.

1. Item search — /api/search

Full-text search over a user's own items, scoped to the session user. It's the data-search primitive you extend when you build your own searchable feature.

GET /api/search?q=<query>&limit=<n>
  • Requires a session (returns 401 otherwise) and searches only rows owned by session.user.id — one user can never see another's data.
  • q is required; limit is optional (clamped to 1–100, default 20).
  • Responds with { results, query }, where each result is:
interface SearchResult {
  id: string;
  name: string;
  description: string;
  snippet: string;   // highlighted excerpt (<mark>…</mark>) on the FTS5 path
  rank: number;
}

The route is thin — it calls searchItems(userId, query, limit) from lib/search.ts, which delegates to the dialect-appropriate raw adapter (lib/db-raw.ts). Point your own searchable data at the same helper, or copy the pattern for a new corpus.

Where it's used: the command palette

The endpoint powers item search inside the command palette (components/ui/command-palette.tsx). Open the palette with Cmd/Ctrl + K, then type > to switch into item-search mode. Queries are debounced (200 ms) and only fire once you've typed at least two characters.

How full-text search works per dialect

The search index is dialect-specific, which matters because dev and production run on different databases:

  • SQLite (local dev) uses FTS5. Migration 006_create_search_index.sql creates an items_fts virtual table kept in sync by triggers on items. searchItems sanitizes the query (strips '"*()), turns each term into a prefix match ("term"*), ranks results with FTS5's rank, and returns a highlighted snippet(...). If an FTS query is malformed it falls back to a LIKE scan.
  • Cloudflare D1 (production) does not guarantee FTS5, so the D1 adapter always uses the LIKE path (lib/db-raw-d1.ts). Results still come back, but without FTS ranking or a highlighted snippet (rank is 0, snippet is the item name).
  • PostgreSQL uses an ILIKE fallback (tsvector support is future work).

Dev/prod parity gotcha: you'll see ranked FTS5 matches with highlighted snippets in local SQLite, but plain substring (LIKE) matching on D1 in production. Design your search UX so it degrades gracefully — don't rely on the snippet/rank fidelity being identical across environments. See D1 dev/prod parity.

lib/search.ts also exposes rebuildSearchIndex() — an FTS5 rebuild on SQLite, and a no-op on D1/PostgreSQL.

Search over the public customer documentation (the site you're reading, mounted at /docs). It's a one-liner built entirely on fumadocs:

import { docsSource } from "@/lib/docs-source";
import { createFromSource } from "fumadocs-core/search/server";

export const { GET } = createFromSource(docsSource);

docsSource is the fumadocs page tree loaded from the compiled docs content. The docs UI wires its search box to this endpoint automatically — you don't call it by hand. It's public, matching the docs themselves.

The same fumadocs search, but over the internal dev wiki (devDocsSource, mounted at /admin/docs) and gated behind admin auth. The route checks the session and isAdmin(session.user.id) before delegating to the fumadocs handler, returning 401 to anyone who isn't an admin:

const session = await auth.api.getSession({ headers: request.headers });
if (!session || !(await isAdmin(session.user.id))) {
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
return search.GET(request);

This keeps the developer-internal wiki out of reach for regular users while giving admins the same fast search experience as the public docs.

  • The data layer — how the raw DB adapter and dialects fit together
  • D1 dev/prod parity — why FTS5 differs between dev and production
  • Admin — the admin gating the dev wiki search relies on

On this page