TanStack Start
Isomorphic loaders are the wrong place for a database handle.
pnpm add @usegraft/sdk-tanstack-start @usegraft/auth @usegraft/mcpnpm i @usegraft/sdk-tanstack-start @usegraft/auth @usegraft/mcpbun add @usegraft/sdk-tanstack-start @usegraft/auth @usegraft/mcpBuild the handle once in a server-only module, then reach it from server functions and server routes.
Create the handle
// src/lib/graft.server.ts
import { createDb } from "@usegraft/db";
import { createGraft } from "@usegraft/sdk-tanstack-start";
import { collections } from "../../graft.config";
export const db = createDb(process.env.DATABASE_URL!).db;
export const graft = createGraft({ db, collections });
Static index instead of Postgres:
import { openStaticIndex } from "@usegraft/db";
export const graft = createGraft({
index: await openStaticIndex(".graft/index.db"),
collections,
});
Read one document
// src/routes/docs.$slug.tsx
import { createServerFn } from "@tanstack/react-start";
import { graft } from "../lib/graft.server";
const getDoc = createServerFn({ method: "GET" })
.validator((slug: string) => slug)
.handler(({ data: slug }) => graft.getContent("docs", slug));
There is no request-level memo. Reads go straight to the index; call sites own memoization.
Mount functions and MCP
Copy these mounts as written. The server-route factory name has moved across TanStack Start releases; the handler signature has not.
// src/routes/api/fn/$name.ts
import { createServerFileRoute } from "@tanstack/react-start/server";
import { createFunctionsHandler } from "@usegraft/core";
import { graftRoute } from "@usegraft/sdk-tanstack-start";
import { functions } from "../../../graft.config";
import { db } from "../../lib/graft.server";
const handler = createFunctionsHandler({ db, functions });
export const ServerRoute = createServerFileRoute("/api/fn/$name").methods({
POST: graftRoute(handler),
GET: graftRoute(handler), // 405s with Allow and a fix
});
// src/routes/api/mcp.ts
import { createActorResolver } from "@usegraft/auth";
import { createServerFileRoute } from "@tanstack/react-start/server";
import { createGraftMcpHandler } from "@usegraft/mcp";
import { graftRoute } from "@usegraft/sdk-tanstack-start";
import { collections, functions } from "../../graft.config";
import { db } from "../lib/graft.server";
const actor = createActorResolver({
issuers: (process.env.GRAFT_TRUSTED_ISSUERS ?? "")
.split(/[,\s]+/)
.filter(Boolean)
.map((issuer) => ({ issuer })),
devTokens: process.env.GRAFT_DEV_TOKEN
? {
[process.env.GRAFT_DEV_TOKEN]: {
kind: "human",
id: "owner",
scopes: ["content:write"],
},
}
: undefined,
});
export const ServerRoute = createServerFileRoute("/api/mcp").methods({
POST: graftRoute(
createGraftMcpHandler({
contentDir: "./content",
db,
collections,
functions,
actor,
}),
),
});
This endpoint serves content writes and asset uploads. JWT verification runs
only when issuers is passed; the library does not read
GRAFT_TRUSTED_ISSUERS (graft serve does). See Auth.
Unauthenticated callers get 401. allowAnonymous: true is only for a
loopback local server — never on anything reachable from a network. A public
docs surface is createDocsMcpHandler, which has no write
tools and no anonymous opt-in.
The parameter is typed structurally as { request: Request }, so this package
needs no @tanstack/react-start dependency.
Cache and invalidation
TanStack Start has no tag-based data cache. Stamp tagsFor(...) into a CDN
surrogate-key header on the responses you serve, and purge
tagsForChanges(branch, changeSet) from your compile webhook.
import { tagsFor } from "@usegraft/sdk-tanstack-start";
const slug = "home";
headers.set("Cache-Tag", tagsFor("main", "pages", slug).join(","));
Cache-Tag is the surrogate-key header. Purge the same strings
tagsForChanges returns from your compile webhook.
The caveat you cannot infer
A route loader is the wrong place for this handle. TanStack Start loaders
are isomorphic: they run on the server for the first paint and in the
browser on client-side navigation. A handle that holds a database connection
has no business in either the client bundle or the browser, which is what
createServerFn is for.
See SDK reference for mounts across adapters and the honest gaps.