s3kit
v0.1.1
Published
Secure, server-driven, framework-agnostic S3 file manager (core + HTTP adapters + client helper).
Readme

s3kit
A secure, server-driven, framework-agnostic S3 file manager with a React UI.
Package modules:
core: S3 operations (virtual folders, pagination, presigned uploads, previews)http: thin HTTP handler (maps requests to core)adapters/*: framework adapters (Express, Next.js, Fetch/Remix)client: browser helper (typed API calls + multi-file upload orchestration)
Install
npm i s3kitQuickstart (local testing)
This repo includes a working Next.js example with a customizer UI and live preview.
1) Set up the Next.js example
cd examples/nextjs-app
# Copy environment template and configure
cp .env.example .envEdit .env with your S3 credentials:
AWS_REGION=us-east-1
S3_BUCKET=your-bucket-name
S3_ROOT_PREFIX=dev # optional, keeps test files under dev/Then run:
npm install
npm run devOpen http://localhost:3000
2) Configure S3 CORS (required for browser uploads)
Uploads use presigned PUT URLs, which means the browser uploads directly to S3.
Your bucket must allow CORS from your UI origin (example: http://localhost:3000) with:
PUT(uploads)GET(previews)HEAD(often used by browsers)
Example CORS configuration:
[
{
"AllowedOrigins": ["http://localhost:3000"],
"AllowedMethods": ["GET", "PUT", "HEAD", "OPTIONS"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"]
}
]If uploads still fail with a CORS error:
- Confirm the UI origin matches exactly (scheme, host, and port).
- Ensure the bucket CORS rules are applied to the correct bucket.
- Include
OPTIONSandPUTinAllowedMethods(preflight + upload). - If you set custom headers in
prepareUploads, include them inAllowedHeaders.
Credentials and security
- S3 credentials must be configured on the server (Node).
- Do not put credentials in the browser app.
- Credentials are read from environment variables or AWS SDK defaults.
Server-side (core)
import { S3Client } from '@aws-sdk/client-s3';
import { S3FileManager } from 's3kit/core';
const s3 = new S3Client({ region: process.env.AWS_REGION });
const manager = new S3FileManager(s3, {
bucket: process.env.S3_BUCKET!,
rootPrefix: 'uploads',
authorizationMode: 'deny-by-default',
lockFolderMoves: true,
lockPrefix: '.s3kit/locks',
lockTtlSeconds: 60 * 15,
hooks: {
authorize: ({ ctx }) => Boolean(ctx.userId),
allowAction: ({ action, path }) => {
if (action === 'file.delete') return false;
if (path && path.startsWith('private/')) return false;
return true;
}
}
});authorize returning false responds with a 401. allowAction returning false responds with a 403. authorizationMode defaults to deny-by-default.
Authorization hooks (agnostic)
Use authorize for auth checks and allowAction for per-action rules. Both hooks are optional, but with the default deny-by-default, you must provide at least one.
Example: API key auth (framework-agnostic)
const manager = new S3FileManager(s3, {
bucket: process.env.S3_BUCKET!,
authorizationMode: 'deny-by-default',
hooks: {
authorize: ({ ctx }) => ctx.apiKey === process.env.FILE_MANAGER_API_KEY
}
});S3-compatible endpoints (MinIO, LocalStack, R2, Spaces, Wasabi, ...)
You can point the AWS SDK client at an S3-compatible endpoint:
const s3 = new S3Client({
region: process.env.AWS_REGION,
endpoint: process.env.S3_ENDPOINT,
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === '1'
});Virtual folder listing + pagination
const page1 = await manager.list({ path: '', limit: 100 }, { userId: '123' });
const page2 = await manager.list({ path: '', cursor: page1.nextCursor, limit: 100 }, { userId: '123' });HTTP layer
The HTTP handler expects JSON POST requests.
Routes:
POST /listPOST /searchPOST /folder/createPOST /folder/deletePOST /folder/lock/getPOST /files/deletePOST /files/copyPOST /files/movePOST /upload/preparePOST /previewPOST /file/attributes/getPOST /file/attributes/set
Notes:
POST /searchreturns file entries in stable, S3 listing order (lexicographic by key/path) so pagination viacursoris deterministic.cursoris the underlying S3 continuation token; passnextCursorfrom the previous response to fetch the next page.- Conflict detection: pass
ifMatch(ETag) for filecopy,move, anddeleteto avoid overwriting if the object changed. A failed precondition returns409 conflict. - Conditional uploads: pass
ifNoneMatch: "*"inupload/prepareitems to enforce "only if not exists". - Folder lock status:
POST /folder/lock/getreturns lock metadata (or null) for folder rename operations. - File attributes:
POST /file/attributes/getreturns content headers, metadata, andexpiresAt;POST /file/attributes/setupdates them.
Next.js (App Router) adapter
// app/api/s3/[...path]/route.ts
import { createNextRouteHandlerFromEnv } from 's3kit/adapters/next';
export const POST = createNextRouteHandlerFromEnv({
basePath: '/api/s3',
authorization: {
mode: 'allow-by-default'
},
env: {
region: 'AWS_REGION',
bucket: 'S3_BUCKET',
rootPrefix: 'S3_ROOT_PREFIX',
endpoint: 'S3_ENDPOINT',
forcePathStyle: 'S3_FORCE_PATH_STYLE',
requireUserId: 'REQUIRE_USER_ID'
}
});This helper reads env values from the map above. At minimum, region and bucket
must point to defined env vars. For production, replace the example allow-by-default
with your own authorize / allowAction hooks.
Express adapter
import express from 'express';
import { createExpressS3FileManagerHandler } from 's3kit/adapters/express';
const app = express();
app.use(express.json({ limit: '2mb' }));
app.use(
'/api/s3',
createExpressS3FileManagerHandler({
manager,
getContext: (req) => ({ userId: req.header('x-user-id') ?? undefined }),
api: { basePath: '/api/s3' }
})
);
app.listen(3001);Fetch/Remix adapter
import { createFetchHandler } from 's3kit/adapters/fetch';
export const handler = createFetchHandler({
manager,
getContext: async (req) => ({ userId: req.headers.get('x-user-id') ?? undefined }),
api: { basePath: '/api/s3' }
});Client helper
import { S3FileManagerClient } from 's3kit/client';
const client = new S3FileManagerClient({
apiUrl: '/api/s3'
});
const listing = await client.list({ path: '' });
const preview = await client.getPreviewUrl({ path: 'docs/readme.pdf', inline: true });
await client.uploadFiles({
files: [
{ file: someFile, path: `docs/${someFile.name}` },
{ file: otherFile, path: `docs/${otherFile.name}` }
],
hooks: {
onUploadProgress: ({ path, loaded, total }) => {
console.log(path, loaded, total);
}
}
});
const lock = await client.getFolderLock({ path: 'docs/' });Alternative client config
If you prefer splitting origin + mount path:
const client = new S3FileManagerClient({
baseUrl: 'http://localhost:3000',
basePath: '/api/s3'
});Multiple S3 configs (multiple API endpoints)
For multi-bucket / multi-environment setups, keep configs on the server and expose them as separate API endpoints.
- The client simply points to the right
apiUrl(e.g./api/s3vs/api/s3-media). - The server routes each endpoint to its own
S3FileManagerinstance.
Example (framework-agnostic http handler):
import { createS3FileManagerHttpHandler } from 's3kit/http';
export const s3Handler = createS3FileManagerHttpHandler({
getManager: () => managers.default,
api: { basePath: '/api/s3' }
});
export const mediaHandler = createS3FileManagerHttpHandler({
getManager: () => managers.media,
api: { basePath: '/api/s3-media' }
});React embed examples
Common UI props
theme:'light' | 'dark' | 'system'mode:'viewer' | 'picker' | 'manager'selection:'single' | 'multiple'toolbar:{ search, breadcrumbs, viewSwitcher, sort }labels: text overrides for buttons and placeholdersviewMode:'grid' | 'list'
The React UI is styled with CSS variables + CSS modules to keep styles scoped.
1) Viewer (read-only file browser)
import { FileManager } from 's3kit/react';
export function FileViewer() {
return (
<FileManager
apiUrl="/api/s3"
mode="viewer"
allowActions={{ upload: false, createFolder: false, delete: false, rename: false, move: false, copy: false, restore: false }}
/>
);
}2) Picker (select one or many files)
import { FilePicker } from 's3kit/react';
export function FileField() {
return (
<FilePicker
apiUrl="/api/s3"
selection="single"
onConfirm={(entries) => {
const file = entries[0];
console.log('Selected:', file);
}}
onSelectionChange={(entries) => {
console.log('Current selection:', entries);
}}
confirmLabel="Use file"
allowActions={{ upload: true, createFolder: true }}
/>
);
}3) Manager (full CRUD)
import { FileManager } from 's3kit/react';
export function FileManagerAdmin() {
return (
<FileManager
apiUrl="/api/s3"
mode="manager"
selection="multiple"
allowActions={{
upload: true,
createFolder: true,
delete: true,
rename: true,
move: true,
copy: true,
restore: true
}}
/>
);
}UI customization (toolbar + labels)
import { FileManager } from 's3kit/react';
export function FileManagerCustomized() {
return (
<FileManager
apiUrl="/api/s3"
toolbar={{ search: false, breadcrumbs: true, viewSwitcher: true, sort: false }}
labels={{
upload: 'Add files',
newFolder: 'Create folder',
delete: 'Remove',
deleteForever: 'Remove forever',
restore: 'Restore',
emptyTrash: 'Clear trash',
confirm: 'Select',
searchPlaceholder: 'Search...'
}}
viewMode="grid"
/>
);
}Example
examples/nextjs-app: Next.js App Router example with a customizer panel and live preview
Security checklist
- Keep S3 credentials on the server only.
- Require auth on the API route (session/JWT/API key).
- Use
deny-by-defaultand implementauthorize/allowAction. - Enforce least-privilege IAM policies for the bucket.
- Validate inputs (paths, allowed actions) and consider rate limiting.
