TanStack Start
Server route integration for the Silo upload router.
@silo-storage/sdk-tanstack-start connects a typed Silo file router to TanStack Start server routes.
Use it when you already have a typed upload router from @silo-storage/sdk-server and want to mount it directly in a Start route file under server.handlers.
What it provides
createRouteHandler(...)for TanStack Startserver.handlers- callback verification and
onUploadCompletedispatch - registration handling for browser uploads
- completion polling used by the React client
extractRouterConfig(...)for optional client hydration
Getting started
Install the package
npm install @silo-storage/sdk-tanstack-start @silo-storage/sdk-react @silo-storage/sdk-server @silo-storage/sdk-coreDefine your file router
import type { FileRouter } from "@silo-storage/sdk-server";
import { createSiloUpload } from "@silo-storage/sdk-server";
import { z } from "zod";
export interface UploadContext {
userId: string;
}
const f = createSiloUpload<Request, UploadContext>();
export const fileRouter = {
imageUploader: f(
z.object({
folder: z.enum(["avatars", "attachments"]).default("avatars"),
kind: z.enum(["image", "binary"]).default("image"),
}),
)
.middleware(async ({ context, input }) => {
if (!context?.userId) {
throw new Error("Unauthorized");
}
return {
userId: context.userId,
folder: input.folder,
};
})
.expects(({ input }) =>
input.kind === "binary"
? [
{
mimeTypes: ["application/xyz", "application/abc"],
maxFileCount: 4,
maxFileSize: "16MB",
},
]
: {
image: {
maxFileSize: "8MB",
maxFileCount: 4,
mimeTypes: ["image/png", "image/jpeg"],
},
},
)
.public(false)
.serveImage(({ input }) => input.folder === "avatars")
.expires("30 days")
.onUploadComplete(async ({ metadata, file }) => {
return {
uploadedBy: metadata.userId,
fileKeyId: file.fileKeyId,
};
}),
} satisfies FileRouter<Request, UploadContext>;
export type AppFileRouter = typeof fileRouter;If you want more detail on route definitions, middleware, or callback behavior, read Server.
Mount the Start route
Create a TanStack Start route file and pass the generated handlers into server.handlers.
import { createFileRoute } from "@tanstack/react-start";
import { createSiloCoreFromToken } from "@silo-storage/sdk-core";
import { createRouteHandler } from "@silo-storage/sdk-tanstack-start";
import type { UploadContext } from "@/upload";
import { fileRouter } from "@/upload";
const core = createSiloCoreFromToken({
url: process.env.SILO_URL!,
token: process.env.SILO_TOKEN!,
cdnHost: process.env.SILO_CDN!,
});
const handlers = createRouteHandler<UploadContext>({
router: fileRouter,
core,
// Defaults to "sse" in development and "callback-url" otherwise.
// Override it when you want to force one transport explicitly.
completionTransport: "auto",
resolveContext: async (request: Request): Promise<UploadContext> => {
const userId = request.headers.get("x-user-id");
if (!userId) throw new Error("Unauthorized");
return { userId };
},
});
export const Route = createFileRoute("/api/upload")({
server: {
handlers,
},
});createRouteHandler(...) keeps the same HTTP contract as the Next adapter:
GETreturns the router config so clients can discover route metadata safelyPOSThandles upload registration requests from the browserPOSTalso verifies signed Silo callbacks and dispatches the matchingonUploadCompletePOSThandles completion polling used by the React SDK after an upload finishes
By default, the adapter uses completionTransport: "auto", which selects sse when NODE_ENV === "development" and callback-url otherwise.
When the adapter uses callback URLs, it defaults to the current route URL. Override that with callbackUrl if your public callback origin differs from the incoming request origin.
Set completionTransport explicitly when you want to force one mode:
"auto"to keep the environment-based default"sse"to always use the development SSE flow"callback-url"to always use signed callbacks
Using with the React SDK
The React integration stays the same. Point createSiloReact(...) at the Start route URL.
"use client";
import { createSiloReact } from "@silo-storage/sdk-react";
import type { AppFileRouter } from "@/upload";
export const {
useUpload,
useStagedUpload,
UploadButton,
UploadDropzone,
SiloRouterConfigProvider,
} = createSiloReact<AppFileRouter>({
endpoint: "/api/upload",
});The zero-config path is to let the client fetch route config from the adapter's GET endpoint on demand. If you already have a server-rendered app shell and want to avoid that first client fetch, you can still call extractRouterConfig(fileRouter) and provide it through SiloRouterConfigProvider.
If a route uses a function-based .expects(...) resolver, that extracted config cannot provide a stable picker filter for the client. Pass accept to the React hook or component that opens the file picker when you need that filter.
Completion storage
The Start adapter stores onUploadComplete results temporarily so the client can wait for completion after the upload finishes.
You can control the store with the same options as the Next adapter:
completionStorecompletionStoreUrlcompletionStoreAuthTokencompletionStorePathPrefixcompletionTtlMs
By default it will:
- use an HTTP-backed completion store when it has a base API URL and auth token
- fall back to an in-memory store otherwise
Use a shared store in production when the upload callback and the polling request may run on different instances.
Notes
- Callback verification depends on
core.config.signingSecret.createSiloCoreFromToken(...)fills that fromSILO_TOKEN, so you usually do not need to pass anything extra intocreateRouteHandler(...). - If you force
completionTransport: "callback-url"in development, make sure the configured callback endpoint is publicly reachable by Silo. extractRouterConfig(...)only exposes the client-safe subset of your router definition for routes with statically known expectations.
API reference
For generated type tables covering the route handler and completion store options, see API Reference.