@surrealdb/fabric
v0.2.0
Published
The shared SurrealDB **Fabric API** integration — a typed OpenAPI client, TanStack Query hooks, and the support ticketing surfaces built on top of them. Published to npm so every SurrealDB frontend can talk to `api.surrealdb.com` the same way.
Downloads
263
Readme
@surrealdb/fabric
The shared SurrealDB Fabric API integration — a typed OpenAPI client, TanStack Query hooks, and the support ticketing surfaces built on top of them. Published to npm so every SurrealDB frontend can talk to api.surrealdb.com the same way.
This is the sibling of @surrealdb/ui: same tooling, same conventions, but no Storybook — it is an integration package, not purely a UI library.
What's inside
| Area | Contents |
|---|---|
| API client | configureFabricClient, getFabricClient, fabricFetch, FabricApiError — an openapi-fetch client typed against the generated paths |
| Generated types | src/api/openapi.yml, src/api/schema.ts and every type in src/api/types.ts — all derived from the OpenAPI spec, regenerated with bun run schema |
| Hooks | 37 generated TanStack Query hooks — one per API operation, across Accounts, Sidekick, Support, University and Website |
| Components | Contact modal, help search palette, conversation table, conversation detail, article and collection views — one folder each under src/components, styled with Mantine props rather than custom CSS |
Installation
bun add @surrealdb/fabricPeer dependencies: react 19, @mantine/core/@mantine/hooks/@mantine/dates 9, @surrealdb/ui, @tanstack/react-query 5, and date-fns 4. No auth SDK — this package never imports one.
Import the stylesheet once, alongside the @surrealdb/ui one:
import "@surrealdb/fabric/styles.css";Setup
One provider, one import path. apiBase is the only required field.
import { FabricProvider } from "@surrealdb/fabric";
<QueryClientProvider client={queryClient}>
<FabricProvider config={{ apiBase: import.meta.env.VITE_FABRIC_API }}>
<App />
</FabricProvider>
</QueryClientProvider>;That is the whole setup for a project with no login — the website, the docs. Blogs, events, press, banners and the help centre all work.
Adding a session
Login is optional. Hand the provider a way to fetch a token and the account-bound hooks switch on:
const { getAccessTokenSilently } = useAuth0();
<FabricProvider config={{ apiBase, getAccessToken: getAccessTokenSilently }}>authenticated is inferred from getAccessToken, so that one field is enough. Pass authenticated: isAuthenticated as well if you also want requests suppressed while the user is signed out. Any auth stack works — nothing here imports Auth0, or knows it exists.
What happens without a session
Hooks that need a login stay disabled and say so, once, rather than failing silently or firing a doomed request:
[fabric] useConversationsQuery is disabled because it needs a signed-in session.
Pass `getAccessToken` to <FabricProvider> — e.g. `getAccessToken: getAccessTokenSilently`
from useAuth0(). Public data (blogs, events, press, banners, help centre) needs no session.The query simply never fetches, so rendering an account-bound surface in a project without login is harmless.
| Field | Purpose |
|---|---|
| apiBase | Required. e.g. https://api.surrealdb.com |
| getAccessToken | Enables account-bound hooks. Omit for public data only |
| authenticated | Defaults to whether getAccessToken was given. Set it to also suppress requests while signed out |
| user | Labels replies the current user is composing |
| supportTickets | Mirrors the host's support_tickets flag (default true) |
| teamAvatar | Avatar for staff and bot authors without one of their own |
| openUrl | External links. Defaults to window.open(url, "_blank") |
Navigation
This package never navigates on its own, and never assumes a route exists. Where a component goes is a prop on that component, not global configuration — so the same card can open a drawer in one place and route in another.
<SupportCenterView
onSelectConversation={(c) => router.push(`/support/conversations/${c.id}`)}
onSelectCollection={(c) => router.push(`/support/collections/${c.id}`)}
onViewRequests={() => router.push("/support/requests")}
/>
<ConversationView
id={id}
onBack={() => router.push("/support/requests")}
onOpenSupport={() => router.push("/support")}
/>Every handler is optional, and omitting one is a real choice rather than a broken link:
| Prop | Omitted |
|---|---|
| ConversationTable onSelect | Rows are not clickable |
| ArticleCard / CollectionCard onSelect | Opens the record's public help-centre URL |
| HelpSearchModal onSelect | Same — opens the article externally |
| ContactModal onCreated | Nothing happens after creation |
| *View onOpenSupport / onBack | Those buttons are hidden |
Help-centre articles and collections carry a public url, so those degrade to an external link rather than vanishing — a docs site can render the help centre with no routing at all. Handlers receive the whole record, so you can route on any field.
Composite views forward to what they render: CollectionView passes onSelectArticle down to its ArticleCards, SupportCenterView passes onSelectConversation to its ConversationTable.
resolveOrganizationName is likewise a ConversationTable prop — an app with no org concept simply omits it and the label is left off.
The provider carries only what is genuinely host-wide: the API base, the session, openUrl (a shell capability — Electron versus browser), and the branding used to label authors.
Conversations and tickets
Conversations are not organization-bound — opening one needs no configuration beyond apiBase:
import { openContactModal } from "@surrealdb/fabric";
openContactModal({
type: "conversation",
conversationType: "general",
subject: "Account / billing enquiry",
});Organization tickets are opt-in. Pass the eligible organizations and the contact modal gains ticket mode; resolving which organizations hold a support plan is the host's job, since that is Cloud API data rather than Fabric's:
<FabricSupportModals organizations={organizationsWithSupportPlan} />;
openContactModal({ type: "ticket", organization: org.id });Mount <FabricSupportModals /> once, near the root and inside FabricProvider.
Hooks
Every operation in the Fabric API gets a hook, and they are generated. src/generated/ is emitted from the OpenAPI document — 37 hooks across Accounts, Sidekick, Support, University and Website — so the package covers the whole API rather than a hand-picked subset.
const { data: conversations } = useConversationsQuery();
const { data: article } = useSupportArticleQuery({ id });
const { data: tokens } = usePersonalAccessTokensQuery();
const { data: courses } = useUniversityCoursesQuery();
const reply = useConversationReplyMutation();
await reply.mutateAsync({ id, body: "Thanks!" });
queryClient.invalidateQueries({ queryKey: fabricKeys.supportCloudConversations() });Queries take (params?, options?) — path and query params in one object, TanStack options second. Mutations take no arguments; path params travel with the variables, so one hook instance serves any id.
Everything structural comes from the spec: request/response types, params, query keys. Names and behaviour that the spec cannot express come from tools/hook-policy.ts — session gating, refetch intervals, cache invalidation, and friendlier names than the derived use<Path>Query.
Adding an endpoint to the API needs no code here: re-run bun run schema and it appears with a derived name and full types.
Two operations are deliberately skipped, since neither returns a cacheable JSON payload: the PDF course certificate and the SSE chat stream. The generator reports them, and a test pins the list.
Keys are rooted at ["fabric", …] and exported as fabricKeys, one factory per path.
Regenerating the API schema
Every payload type in this package is generated from the Fabric OpenAPI document — there are no hand-written response interfaces. If a payload comes back as unknown, the fix belongs in api.surrealdb.com (declare the response schema on the route), not here.
The OpenAPI document and its generated types are committed so the package builds offline.
bun run schemabun run schema:prodTo generate against a locally running api.surrealdb.com — the fastest loop when you're changing a response schema — start it there with bun run dev, then:
bun run schema:devThat targets http://localhost:3000/openapi/public.yaml; set FABRIC_DEV_PORT if the API listens elsewhere. All three write src/api/openapi.yml and src/api/schema.ts. Pass --url=<spec-url> (or set FABRIC_OPENAPI_URL) for any other deployment. Commit the result — but commit a staging or production regeneration, not a schema:dev one, so the checked-in types match a deployed API.
Commands
| Command | Description |
|---|---|
| bun install | Install dependencies |
| bun run build | Production build to dist/ |
| bun qc | Biome lint/format check |
| bun qa | Biome autofix |
| bun qts | TypeScript check |
| bun run test | Run the Vitest suite |
| bun run hooks | Regenerate src/generated/ from the committed spec |
| bun run schema | Re-download the staging spec, then regenerate types and hooks |
| bun run schema:dev | Same, from a locally running API (FABRIC_DEV_PORT, default 3000) |
| bun run schema:prod | Same, from production |
