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:
| Endpoint | Searches | Auth | Backed by |
|---|---|---|---|
GET /api/search | The user's own items (app data) | Signed-in user | lib/search.ts → the raw DB adapter |
GET /api/docs-search | The public customer docs (/docs) | Public | fumadocs createFromSource(docsSource) |
GET /api/wiki-search | The dev wiki (/admin/docs) | Admin only | fumadocs 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. qis required;limitis 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.sqlcreates anitems_ftsvirtual table kept in sync by triggers onitems.searchItemssanitizes the query (strips'"*()), turns each term into a prefix match ("term"*), ranks results with FTS5'srank, and returns a highlightedsnippet(...). If an FTS query is malformed it falls back to aLIKEscan. - Cloudflare D1 (production) does not guarantee FTS5, so the D1 adapter
always uses the
LIKEpath (lib/db-raw-d1.ts). Results still come back, but without FTS ranking or a highlighted snippet (rankis0,snippetis the item name). - PostgreSQL uses an
ILIKEfallback (tsvectorsupport 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 thesnippet/rankfidelity 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.
2. Docs search — /api/docs-search
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.
3. Dev wiki search — /api/wiki-search
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.
Related
- 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
Feature Flags
A feature flag system with a three-tier transport — Cloudflare Flagship on Workers, an OpenFeature fallback, and local env/JSON flags for dev — plus built-in maintenance mode.
Theming & dark mode
The useTheme hook, the ThemeToggle, system-aware light/dark switching, and the input text-color rule you must follow.