Silo Docs
TanStack Start

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 Start server.handlers
  • callback verification and onUploadComplete dispatch
  • 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-core

Define your file router

src/upload.ts
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.

src/routes/api/upload.ts
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:

  • GET returns the router config so clients can discover route metadata safely
  • POST handles upload registration requests from the browser
  • POST also verifies signed Silo callbacks and dispatches the matching onUploadComplete
  • POST handles 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.

src/lib/upload.ts
"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:

  • completionStore
  • completionStoreUrl
  • completionStoreAuthToken
  • completionStorePathPrefix
  • completionTtlMs

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 from SILO_TOKEN, so you usually do not need to pass anything extra into createRouteHandler(...).
  • 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.

On this page