Overview
The whole Files API in the browser, over one endpoint — a React hook, a Vue composable, or Svelte stores, backed by a server gateway you mount in minutes.
files-sdk gives the browser the same API the SDK gives the server. One binding — a React hook, a Vue composable, or Svelte stores — mirrors every Files verb over a single HTTP endpoint:
"use client";
import { useFiles } from "files-sdk/react";
export function Uploader() {
const files = useFiles({ endpoint: "/api/files" });
return (
<>
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) files.upload(file); // keyless — the server mints the key
}}
/>
{files.isUploading && <progress value={files.progress.fraction} />}
</>
);
}<script setup lang="ts">
import { useFiles } from "files-sdk/vue";
const files = useFiles({ endpoint: "/api/files" });
const onChange = (e: Event) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) files.upload(file); // keyless — the server mints the key
};
</script>
<template>
<input type="file" @change="onChange" />
<progress
v-if="files.isUploading.value"
:value="files.progress.value.fraction"
/>
</template><script lang="ts">
import { useFiles } from "files-sdk/svelte";
import { onDestroy } from "svelte";
const files = useFiles({ endpoint: "/api/files" });
const { isUploading, progress } = files;
onDestroy(files.abort); // cancel in-flight calls on unmount
const onChange = (e: Event) => {
const file = (e.currentTarget as HTMLInputElement).files?.[0];
if (file) files.upload(file); // keyless — the server mints the key
};
</script>
<input type="file" on:change={onChange} />
{#if $isUploading}<progress value={$progress.fraction} />{/if}The browser never holds storage credentials. Calls go to your endpoint, which runs the SDK against whatever adapter you configured (S3, R2, GCS, Vercel Blob, …) and streams or signs as needed.
The two halves
| Package | What it is | |
|---|---|---|
| Client | files-sdk/react, files-sdk/vue, files-sdk/svelte | A useFiles binding for your framework — every verb (imperative, with upload progress) plus optional reactive useList / useFile / useSearch. |
| Server | files-sdk/api + a framework adapter (Next.js, Hono, Express) | A gateway you mount at /api/files that exposes the Files API over HTTP, gated by an authorize hook. |
The same gateway backs all three bindings. A framework-agnostic core, createFilesClient from files-sdk/client, sits under them for non-framework (Node, worker) callers.
The gateway proxies download, list, delete, and move to the browser —
it is effectively a remote storage console. It is deny-by-default: nothing
is exposed until you configure authorize or
operations. Read that page before shipping.
Quick start
1. Mount the gateway. Expose the Files API at an endpoint and scope every key to the signed-in user. This example uses Next.js; Hono and Express are a one-liner too:
import { createFiles } from "files-sdk";
import { s3 } from "files-sdk/s3";
import { createFilesRouter } from "files-sdk/api";
import { createRouteHandler } from "files-sdk/next";
const router = createFilesRouter({
files: createFiles({ adapter: s3({ bucket: "uploads" }) }),
allowedOrigins: ["https://app.example.com"],
authorize: async ({ req }) => {
const session = await auth(req); // throw → 401
return { keyPrefix: `users/${session.id}/`, maxExpiresIn: 300 };
},
});
export const { GET, POST, PUT } = createRouteHandler(router);2. Use your binding. Drop the component above into your app — that's the whole loop: uploads stream directly to storage (or proxy through your endpoint for adapters that can't presign), and reads run against your gateway. Keys the client sends are relative to the authorized prefix, so the browser can never address another user's files. For a file browser, the reactive reads (useList / useFile / useSearch) wrap the read verbs with data / isLoading / refetch.
Next: pick your binding — React, Vue, or Svelte — then set up the gateway and its authorization.