Silo Docs
Next

Next.js

App Router integration for the Silo upload router.

@silo-storage/sdk-next is the App Router adapter for the Silo SDK stack.

Use it when you already have a typed upload router from @silo-storage/sdk-server and want to expose it through a Next.js route.ts handler.

What it provides

  • createRouteHandler(...) for GET and POST route exports
  • 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-next @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; // you can have whatever you want here
};

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) // do we need a signed url to access?
    .serveImage(({ input }) => input.folder === "avatars") // serve images from the image CDN (transformations etc)
    .expires("30 days") // delete after 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 App Router handler

Create a route handler in your Next.js app, usually at app/api/upload/route.ts.

src/app/api/upload/route.ts
import { auth } from "your-auth-lib/server"; // this can be anything
import { createSiloCoreFromToken } from "@silo-storage/sdk-core";
import { createRouteHandler } from "@silo-storage/sdk-next";

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.NEXT_PUBLIC_SILO_CDN!,
});

export const { GET, POST } = 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> => {
    // do whatever you need here to satisfy the UploadContext type
    const { userId } = await auth(request);
    if (!userId) throw new Error("Unauthorized");
    return { userId };
  },
});

This matches Next.js App Router route handlers, where route.ts exports HTTP method functions such as GET and POST.

What the route handler does

createRouteHandler(...) returns two handlers:

  • 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 in conjunction with the React SDK

When using @silo-storage/sdk-react, you can hydrate the router config on the server so the client does not need to fetch it on first render.

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",
});
src/app/layout.tsx
import { extractRouterConfig } from "@silo-storage/sdk-next";

import { SiloRouterConfigProvider } from "@/lib/upload";
import { fileRouter } from "@/upload";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <SiloRouterConfigProvider routerConfig={extractRouterConfig(fileRouter)}>
          {children}
        </SiloRouterConfigProvider>
      </body>
    </html>
  );
}

For the client-side hooks and headless components themselves, continue with React.

If a route uses a function-based .expects(...) resolver, extractRouterConfig(fileRouter) cannot expose a static picker filter for that route. In that case, pass accept to the React hook or component that opens the file picker.

Completion storage

The Next adapter stores onUploadComplete results temporarily so the client can wait for completion after the upload finishes.

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

You can override that behavior with:

  • completionStore
  • completionStoreUrl
  • completionStoreAuthToken
  • completionStorePathPrefix
  • completionTtlMs

Use a shared store in production when the upload callback and the polling request may run on different instances. For the full flow, the store contract, and replacement examples including Redis, read Completion Store.

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.

Next steps

  • Read Server for route authoring and middleware details
  • Read React for upload hooks and UI helpers
  • See the SDK Demo for a working end-to-end example

On this page