Search
Run filtered, paginated search across every searchable record type in your workspace.
- Use it for: Public website · Server integration
- Reads: public token — works in the browser
- Writes: none — read-only today
- Auth: what each token can do and where it may run — Authentication
search queries the workspace's search index: one call, filtered and sorted, across the record
types you name. The index covers about forty of them — Contact, Deal, Donation,
DonationCampaign, Event, Form, Product, Story, Task, Note, Employee, ModelItem and
more (the node union of SearchResult lists them all).
Mental model
search(client, { models, params, first, after })
└── SearchResult a Relay connection, not a flat array
├── totalCount, totalPages how many matches
├── term the term the engine searched for
├── took query time, in ms
├── pageInfo { hasNextPage, endCursor }
└── edges[] { cursor, node: { __typename, id } }- Hits are references. Each
nodecarries only__typenameandid. Narrow on__typename, then fetch what you want to show with that domain'sget*(getEvent,getStory,getForm…). The SDK's query is fixed, so you can't asksearchfor more fields per type. modelsrestricts the search to record types:['Event', 'Story']. Always pass it. With a single model the engine applies that model's own defaults (scoping, newest first); a single model that isn't searchable fails the request.- Not indexed: knowledge-base articles. To search a help center, filter its articles in memory (Help Center).
- Scope. Results are limited to the client's workspace and leave out trashed (discarded) records. Your filters can narrow that scope, never widen it.
- If the index fails (missing, being rebuilt), you get an empty result rather than an error — so an empty result isn't proof that nothing matches.
params — every key is optional:
{
term?: string; // full-text term; omitted = everything ('*')
filters?: Array<{
id: string; // the field, camelCase: 'createdAt', 'name'
operator: string; // see below
value?: unknown; // JSON
kind?: string; // field kind: 'date', 'number', 'multipleSelect'…
}>;
sorting?: Array<{ id: string; desc?: boolean }>;
grouping?: string[];
options?: { /* engine options: fields, highlight, aggs, boostBy, misspellings… (JSON) */ };
}operator | Matches |
|---|---|
= / eq | exactly value — case-sensitive; with kind: 'multipleSelect', all of value |
in | any of value (an array) |
!= / notIn, doesNotContain | anything but value |
contains | value as a full-text term on that field — case-insensitive, fuzzy |
>, >=, <, <= | ranges on numbers and dates |
between, notBetween | value: [from, to]; leave either end empty for an open range |
dateOverlap | a date range that overlaps value |
null, notNull | empty or set; kind: 'number' or 'date' treats only null as empty, other kinds '' too |
Sorting: relevance comes first, then your sorting in order, then the default order (newest
first). Pagination is cursor-only: pass the last pageInfo.endCursor as after; there is
no going back. hasNextPage is true whenever a page comes back full, so when the total is an exact
multiple of first the last request returns an empty page.
Use cases
A search box over events and stories
Goal: /search?q=… shows matching events and stories with their titles, from one route.
// src/app/search/page.tsx
import Link from 'next/link';
import { getEvent, getStory, search } from '@uidu/client';
import { uidu } from '@/lib/uidu';
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const { q = '' } = await searchParams;
if (q.trim().length < 2) return <p>Type at least two characters.</p>;
const result = await search(uidu, {
models: ['Event', 'Story'],
params: { term: q },
first: 10,
});
const hits = (result?.edges ?? []).flatMap((e) => (e?.node ? [e.node] : []));
// Hits are { __typename, id }: fetch each one's title from its own domain.
const rows = await Promise.all(
hits.map(async (hit) => {
switch (hit.__typename) {
case 'Event': {
const event = await getEvent(uidu, { id: hit.id });
const href = `/event/${encodeURIComponent(hit.id)}`;
return event && { id: hit.id, kind: 'Event', title: event.name, href };
}
case 'Story': {
const story = await getStory(uidu, { id: hit.id });
const href = `/stories/${encodeURIComponent(hit.id)}`;
return story && { id: hit.id, kind: 'Story', title: story.name, href };
}
default:
return null;
}
}),
);
return (
<>
<p>
{result?.totalCount ?? 0} results in {result?.took ?? 0} ms
</p>
<ul>
{rows.map(
(r) =>
r && (
<li key={r.id}>
<Link href={r.href}>{r.title}</Link> <small>· {r.kind}</small>
</li>
),
)}
</ul>
</>
);
}- Reads work with the public token, so the same call can run in the browser. Doing it in a Server
Component keeps the follow-up
get*calls in one place and off the visitor's connection. - Keep
firstsmall: every hit costs one more request to render.
A filtered, sorted listing with "load more"
Goal: the stories created since the start of the year, newest first, twenty at a time.
// src/app/stories/archive/actions.ts
'use server';
import { search } from '@uidu/client';
import { uidu } from '@/lib/uidu';
export async function storiesSince(from: string, after?: string) {
const result = await search(uidu, {
models: ['Story'],
first: 20,
after,
params: {
filters: [{ id: 'createdAt', kind: 'date', operator: '>=', value: from }],
sorting: [{ id: 'createdAt', desc: true }],
},
});
return {
ids: (result?.edges ?? []).flatMap((e) => (e?.node ? [e.node.id] : [])),
total: result?.totalCount ?? 0,
// Pass back as `after` for the next twenty; null when there are no more.
next: result?.pageInfo.hasNextPage ? (result.pageInfo.endCursor ?? null) : null,
};
}
// storiesSince('2026-01-01') → first page
// storiesSince('2026-01-01', page.next) → the next one- Filter
ids are the indexed field names in camelCase (createdAt, notcreated_at). A filter on a field the model doesn't index matches nothing. =is exact and case-sensitive (Vanini≠vanini). For "name contains", usecontains, or put the text interm.- With no
term, the search matches everything that passes the filters — this is a plain listing, ordered by yoursorting.
API at a glance
Every @uidu/client function in Search, generated from the source — the full reference has the rest of the SDK.
| Function | Kind | Auth | CLI | Description |
|---|---|---|---|---|
search | read | public token | — | Search the workspace index, optionally limited to models, with filters and sorting in params; page with first/after. |
Types: SearchResult