Buildspace
Storage

Client SDK

Upload files from the browser.

Next.js: upload through your own route

Create one route with createUploadRoute. It checks the signed-in user, chooses the storage key (the browser never picks the path), and confirms the upload.

// app/api/upload/route.ts
import { createUploadRoute } from "@buildspacestudio/sdk/next";
import { getServerClient } from "@/lib/buildspace";

export const POST = createUploadRoute(getServerClient(), {
  allowedContentTypes: ["image/*"],
  maxSize: 5 * 1024 * 1024,
  // Optional. Defaults to `uploads/{userId}/{uuid}-{filename}`.
  keyFor: ({ session }) => `avatars/${session.user.id}/${crypto.randomUUID()}`,
  // Optional. Runs after the upload is confirmed; the return value reaches the browser as `data`.
  onUploadComplete: async ({ session, key }) => {
    await saveAvatarKey(session.user.id, key);
    return { key };
  },
});

Then upload from a client component with useUpload:

"use client";
import { useUpload } from "@buildspacestudio/sdk/react";

export function AvatarPicker() {
  const { upload, isUploading, progress, error } = useUpload({ endpoint: "/api/upload" });

  return (
    <>
      <input
        type="file"
        accept="image/*"
        disabled={isUploading}
        onChange={async (e) => {
          const file = e.target.files?.[0];
          if (file) {
            const { key } = await upload(file);
          }
        }}
      />
      {isUploading && <progress value={progress ?? 0} max={100} />}
      {error && <p>{error.message}</p>}
    </>
  );
}

createUploadRoute options:

OptionPurpose
allowedContentTypesAccepted types, e.g. ["image/*", "application/pdf"]. Returns 415 otherwise.
maxSizeLargest file in bytes. Returns 413 otherwise.
authorizeExtra check (roles, plan limits). Return false for a 403.
keyForChoose the storage key from the session and file details.
onUploadCompleteSave the key, start processing, etc.

The bucket's own path rules (size limits, allowed types) still apply on top.

Need to inspect or transform the bytes first (strip EXIF, resize, reject SVGs)? Post the file to your own route instead, process it there, and upload the result with the Server SDK.

Sending a server-minted URL yourself

If you build your own route, use uploadToSignedUrl for the PUT. The contentType must match what the URL was signed for.

import { createClient } from "@buildspacestudio/sdk/client";

const bs = createClient(process.env.NEXT_PUBLIC_BUILDSPACE_PUBLISHABLE_KEY!);
await bs.storage.uploadToSignedUrl(file, { uploadUrl, contentType: file.type });

Apps that hold a session token

Outside Next.js (or anywhere you already call bs.setSession(token)), the client SDK can upload directly as the signed-in user:

bs.setSession(sessionToken);

const { key, url } = await bs.storage.upload(file, {
  path: `uploads/${file.name}`,
  onProgress: ({ loaded, total }) => console.log(Math.round((loaded / total) * 100)),
});
const { url: fresh } = await bs.storage.getUrl(key);
const { objects } = await bs.storage.list("uploads/");

list only returns objects the caller may read. Without a session, reads and listings only cover paths with a public rule. Delete files from your server; the browser can't.

On this page