@posif/sdk
v1.0.7
Published
TypeScript SDK for Posif applications with automatic query parameter parsing
Downloads
296
Readme
Posif SDK
A TypeScript SDK for Posif applications with automatic query parameter parsing, designed for Next.js apps launched within Flutter WebViews.
Features
- 🔄 Automatic URL query parameter parsing on initialization
- 🏷️ TypeScript support with full type definitions
- 🌐 Browser and server-side rendering (SSR) compatible
- 📦 ESM and CommonJS module formats
- 🎯 Singleton pattern for easy use across your application
- 🛠️ Utility functions for URL manipulation
- 🔒 Authentication token handling
- 🔌 Complete API integration for users, groups, and networks
- 🗄️ Token-based MongoDB database connection caching
- 🔐 Isolated database access per JWT token
- ⚡ Advanced cache management and configuration
- 🛠️ Development mode with local MongoDB URI fallback
- 🚀 Optimized for Next.js 15+ App Router
- 📦 NEW: S3-compatible Object Storage with presigned URLs
- 🔑 NEW: Automatic S3 key generation with user/group isolation
Installation
npm install @posif/sdk
# or
yarn add @posif/sdk
# or
pnpm add @posif/sdkPackage Exports
The SDK provides three entry points for optimal bundle size and tree-shaking:
Main Export (Full SDK)
// Import everything (backward compatible)
import { getDb, uploadFile, generateUploadUrl } from '@posif/sdk';Use when: You need both database and storage functionality.
Storage Export (Storage Functions for API Routes)
// Import storage functions for API routes (no MongoDB)
import { uploadFile, generateUploadUrl, generateFileUrls } from '@posif/sdk/storage';Use when: You need S3 storage functionality in API routes. This avoids pulling in MongoDB dependencies, reducing bundle size.
Includes:
uploadFile()- Complete file upload with progressgenerateUploadUrl()- Get presigned upload URLgenerateFileUrls()- Get view/download URLsgenerateBatchUploadUrls()- Batch upload URLsgenerateDeleteUrl()- Get delete URLgeneratePublicUrl()- Get public URL for public bucketsgetStorage()- Advanced storage API- Storage cache management functions
⚠️ Note: These functions are SERVER-ONLY and must be used in API routes, not client components.
Server Export (Database Only)
// Import only server-side database functions (no S3)
import { getDb, getMongoDbConfig } from '@posif/sdk/server';Use when: You only need MongoDB functionality in server-side code.
Includes:
getDb()- MongoDB database accessgetMongoDbConfig()- Get database configuration- Database cache management functions
posifSDK- SDK configuration
Benefits
✅ Smaller Bundles: Import only what you need
✅ Better Tree-Shaking: Optimized for modern bundlers
✅ Backward Compatible: Main export still works
✅ Zero Breaking Changes: Existing code continues to work
Migration Guide
Existing code works without changes! But you can optimize by updating imports:
// Before (still works)
import { uploadFile, getDb } from '@posif/sdk';
// After (optimized for storage-only API routes)
import { uploadFile } from '@posif/sdk/storage';
// After (optimized for database-only API routes)
import { getDb } from '@posif/sdk/server';When to migrate:
- API Routes (Storage Only): Use
/storageto avoid MongoDB in storage API route bundles - API Routes (Database Only): Use
/serverif you only need database access - Mixed Usage: Keep using main export or use both
/storageand/server
Quick Start
Basic Usage
import sdk from '@posif/sdk';
// The SDK automatically parses URL parameters on load
const params = sdk.getParams();
console.log(params);
// Output: { token: "abc123", user: "john", group: "dev", ... }
// Get specific parameters
const token = sdk.getParam('token');
const user = sdk.getParam('user');
// Check authentication status
if (sdk.isAuthenticated()) {
console.log('User is authenticated');
}API Methods
import sdk from '@posif/sdk';
// Fetch user information
const userInfo = await sdk.getUserInfo();
console.log(userInfo);
// Fetch group information
const groupInfo = await sdk.getGroupInfo();
console.log(groupInfo);
// Fetch group members
const members = await sdk.getMembers(); // Default: limit=20, offset=0
console.log(members);
// Fetch group members with pagination
const paginatedMembers = await sdk.getMembers(10, 5); // limit=10, offset=5
console.log(paginatedMembers);
// Fetch groups that user is part of
const groups = await sdk.getGroups(); // Default: limit=20, offset=0
console.log(groups);
// Fetch groups with role filter
const ownerGroups = await sdk.getGroups(20, 0, 'owner'); // Only groups where user is owner
console.log(ownerGroups);
// Fetch networks that user is part of
const networks = await sdk.getNetworks(); // Default: limit=20, offset=0
console.log(networks);
// Fetch networks with role filter
const ownerNetworks = await sdk.getNetworks(20, 0, 'owner'); // Only networks where user is owner
console.log(ownerNetworks);Database Access (Next.js 15+ App Router Optimized)
The SDK now includes a powerful getDb() function optimized for Next.js 15+ App Router with secure MongoDB access:
import { NextRequest, NextResponse } from 'next/server';
import { getDb } from '@posif/sdk';
// Next.js 15+ App Router - Automatic token extraction from URL
// Example: /api/users?token=your_jwt_token
export async function GET(req: NextRequest) {
try {
// Get database instance (token automatically extracted from URL parameters)
const db = await getDb(req);
// Use MongoDB native driver
const users = await db.collection('users').find({}).toArray();
return NextResponse.json({ users });
} catch (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}Token Extraction Order:
- Explicit token parameter:
getDb(req, 'your_token') - URL parameters (server-side):
/api/users?token=xxx(automatically extracted) - Pages Router query:
req.query.token(Next.js Pages Router) - SDK params (browser-only): Fallback for client-side rendering
With explicit JWT token from headers:
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
// Extract token from Authorization header
const token = req.headers.authorization?.replace('Bearer ', '');
// Pass token explicitly (overrides URL parameter token)
const db = await getDb(req, token);
const users = await db.collection('users').find({}).toArray();
res.status(200).json({ users });
} catch (error) {
res.status(500).json({ error: error.message });
}
}Development Mode
For local development, the SDK automatically detects development mode and uses your local MongoDB URI:
// .env.local
MONGODB_URI=mongodb://localhost:27017/your-database-name
// In your Next.js API route
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
// SDK automatically detects development mode
// Uses MONGODB_URI from .env.local instead of API calls
const db = await getDb(req);
const data = await db.collection('users').find({}).toArray();
res.json({ data });
} catch (error) {
res.status(500).json({ error: error.message });
}
}Development Mode Features:
- ✅ Automatically detects
NODE_ENV=development - ✅ Uses
MONGODB_URIfrom environment variables - ✅ Skips API calls for MongoDB URI
- ✅ Perfect for local development and testing
- ✅ No need for JWT tokens in development
Check Development Mode:
import { getDevelopmentInfo } from '@posif/sdk';
const devInfo = getDevelopmentInfo();
console.log('Development mode:', devInfo.isDevelopment);
console.log('Using local MongoDB:', devInfo.usingLocalMongoDb);Key Features:
- ✅ NEW: Token-based database connection caching
- ✅ NEW: Isolated database access per JWT token
- ✅ NEW: Advanced cache management and configuration
- ✅ JWT token authentication with automatic extraction from URL parameters
- ✅ Supports explicit token override from Authorization headers
- ✅ Fetches MongoDB URI from Posif API with authentication
- ✅ Connection pooling and reuse (no reconnection overhead)
- ✅ Server-only (throws error if used in client code)
- ✅ Full TypeScript support with native MongoDB driver types
Supported Query Parameters
The SDK automatically parses the following query parameters from the URL:
| Parameter | Type | Description |
|-----------|------|-------------|
| token | string | Authentication token |
| user | string | User identifier |
| group | string | Group identifier |
| network | string | Network identifier |
| mode | string | Application mode |
| color | string | Theme color |
| lang | string | Language code |
API Reference
PosifSDK Instance Methods
getParams(): QueryParams
Returns all parsed query parameters as an object.
getParam<K>(key: K): QueryParams[K]
Returns the value of a specific parameter.
setParams(params: Partial<QueryParams>): void
Updates the stored parameters (useful for testing or manual override).
getUserInfo(): Promise<UserInfo>
Fetches user information from the API endpoint /api/v1/users/{id}.
- Endpoint:
GET {apiBaseUrl}/api/v1/users/{user_id} - Headers:
Accept: application/json,Authorization: Bearer <token>(if token provided) - Returns: User information object
getGroupInfo(): Promise<GroupInfo>
Fetches group information from the API endpoint /api/v1/groups/{id}.
- Endpoint:
GET {apiBaseUrl}/api/v1/groups/{group_id} - Headers:
Accept: application/json,Authorization: Bearer <token>(optional for some groups) - Returns: Group information object
getMembers(limit?: number, offset?: number): Promise<GroupMembersResponse>
Fetches group members from the API endpoint /api/v1/groups/{id}/members.
- Endpoint:
GET {apiBaseUrl}/api/v1/groups/{group_id}/members - Parameters:
limit(default: 20),offset(default: 0) - Headers:
Accept: application/json,Authorization: Bearer <token>(required) - Returns: Group members with pagination information
getGroups(limit?: number, offset?: number, roleType?: 'owner' | 'staff' | 'admin' | 'member'): Promise<GroupsResponse>
Fetches groups that the user is part of from the API endpoint /api/v1/groups.
- Endpoint:
GET {apiBaseUrl}/api/v1/groups - Parameters:
limit(default: 20),offset(default: 0),roleType(optional role filter) - Headers:
Accept: application/json,Authorization: Bearer <token>(required) - Returns: User groups with pagination information and platform filtering status
getNetworks(limit?: number, offset?: number, roleType?: 'owner' | 'staff' | 'admin' | 'member'): Promise<NetworksResponse>
Fetches networks that the user is part of from the API endpoint /api/v1/networks.
- Endpoint:
GET {apiBaseUrl}/api/v1/networks - Parameters:
limit(default: 20),offset(default: 0),roleType(optional role filter) - Headers:
Accept: application/json,Authorization: Bearer <token>(required) - Returns: User networks with pagination information and platform filtering status
isAuthenticated(): boolean
Checks if a valid authentication token is present.
getToken(): string | undefined
Returns the authentication token if present.
isBrowser(): boolean
Checks if the SDK is running in a browser environment.
refresh(): void
Re-parses parameters from the current URL.
clear(): void
Clears all stored parameters.
Utility Functions
parseQueryParams(url?: string): QueryParams
Parses query parameters from a URL string or current browser location.
buildQueryString(params: QueryParams): string
Builds a query string from a parameters object.
mergeParams(base: QueryParams, override: Partial<QueryParams>): QueryParams
Merges two parameter objects.
validateRequiredParams(params: QueryParams, required: (keyof QueryParams)[]): boolean
Validates that required parameters are present.
Type Definitions
QueryParams
interface QueryParams {
token?: string;
user?: string;
group?: string;
network?: string;
mode?: string;
color?: string;
lang?: string;
}UserInfo
interface UserInfo {
id: string;
name: string;
email: string;
avatar?: string;
role?: string;
permissions?: string[];
}GroupInfo
interface GroupInfo {
id: string;
name: string;
userName: string;
description: string;
memberCount: number;
type: 'public' | 'private';
roleType: string;
roleName: string;
avatar: string;
}Group
interface Group {
id: string;
user_name: string;
group_name: string;
member_count: string;
avatar: string;
cover_photo: string;
role_id: string;
role_name: string;
role_type: 'owner' | 'staff' | 'admin' | 'member';
}GroupsResponse
interface GroupsResponse {
groups: Group[];
pagination: {
limit: number;
offset: number;
count: number;
};
platform_filtered: boolean;
}Network
interface Network {
id: string;
user_name: string;
network_name: string;
member_count: string;
avatar: string;
cover_photo: string | null;
role_id: string;
role_name: string;
role_type: 'owner' | 'staff' | 'admin' | 'member';
}NetworksResponse
interface NetworksResponse {
networks: Network[];
pagination: {
limit: number;
offset: number;
count: number;
};
platform_filtered: boolean;
}NetworkInfo
interface NetworkInfo {
id: string;
name: string;
domain: string;
region: string;
status: 'active' | 'inactive' | 'maintenance';
endpoints?: {
api: string;
websocket?: string;
cdn?: string;
};
}Next.js Integration
App Router (app directory)
// app/layout.tsx
'use client';
import { useEffect } from 'react';
import sdk from '@posif/sdk';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
useEffect(() => {
// SDK automatically parses URL parameters
const params = sdk.getParams();
if (params.token) {
// Handle authentication
console.log('User authenticated with token:', params.token);
}
if (params.color) {
// Apply theme color
document.documentElement.style.setProperty('--theme-color', params.color);
}
if (params.lang) {
// Set language
document.documentElement.lang = params.lang;
}
}, []);
return (
<html>
<body>{children}</body>
</html>
);
}Pages Router (pages directory)
// pages/_app.tsx
import { useEffect } from 'react';
import type { AppProps } from 'next/app';
import sdk from '@posif/sdk';
export default function App({ Component, pageProps }: AppProps) {
useEffect(() => {
const params = sdk.getParams();
// Handle URL parameters
if (params.token) {
// Set up authentication
}
if (params.mode) {
// Configure app mode
}
}, []);
return <Component {...pageProps} />;
}S3 Object Storage
The SDK provides server-side storage functions for S3-compatible object storage with presigned URLs. All storage functions are SERVER-ONLY and must be used in API routes, as they require server-side execution to securely fetch S3 credentials.
⚠️ Important: Server-Side Only
Storage functions cannot be called directly from client components. They use getStorage() internally, which has assertServerOnly() protection.
Required Pattern:
- ✅ Create API routes that use storage functions (server-side)
- ✅ Call those API routes from client components
- ✅ Client receives presigned URLs and uploads directly to S3
This will NOT work:
'use client';
// ❌ ERROR: This will throw "Function can only be used on the server"
import { uploadFile } from '@posif/sdk/storage';
const key = await uploadFile(file); // ❌ Throws error in browserThis is the correct way:
// ✅ Step 1: API Route (server-side)
// app/api/upload/route.ts
import { generateUploadUrl } from '@posif/sdk';
export async function POST(req: NextRequest) {
const { filename } = await req.json();
const { key, uploadUrl } = await generateUploadUrl(filename, req);
return NextResponse.json({ key, uploadUrl });
}
// ✅ Step 2: Client Component
'use client';
const response = await fetch('/api/upload', {
method: 'POST',
body: JSON.stringify({ filename: file.name })
});
const { key, uploadUrl } = await response.json();
// ✅ Step 3: Upload to S3
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type }
});✨ Key Features
- 🔒 Server-Side Security: S3 credentials never exposed to browser
- 🔑 Automatic Authentication: JWT token extraction from requests
- 👥 User/Group Isolation: Files automatically organized by user and group
- ⚡ Connection Caching: Optimized credential fetching and reuse
- 📝 Presigned URLs: Secure, temporary URLs for S3 operations
- 💪 Type Safe: Full TypeScript support
Architecture Pattern
Client Component (Browser)
↓ fetch('/api/upload')
API Route (Server)
↓ generateUploadUrl()
SDK → getStorage() → Fetch S3 credentials from Posif API
↓
Generate Presigned URL
↓
Return to Client
↓
Client uploads directly to S3 using presigned URLQuick Start - File Upload
Step 1: Create Upload URL API Route (app/api/upload/route.ts)
import { NextRequest, NextResponse } from 'next/server';
import { generateUploadUrl } from '@posif/sdk';
export async function POST(req: NextRequest) {
try {
const { filename, contentType, isPrivate } = await req.json();
// Token automatically extracted from request (?token=xxx)
const { key, uploadUrl } = await generateUploadUrl(filename, req, undefined, {
contentType,
isPrivate: isPrivate !== false, // Default to private
});
return NextResponse.json({ key, uploadUrl });
} catch (error: any) {
return NextResponse.json(
{ error: error.message },
{ status: 500 }
);
}
}Step 2: Client Component
'use client';
import { useState } from 'react';
export default function FileUploader() {
const [file, setFile] = useState<File | null>(null);
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const handleUpload = async () => {
if (!file) return;
setUploading(true);
try {
// Step 1: Get upload URL from API route
const response = await fetch('/api/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
isPrivate: true,
}),
});
const { key, uploadUrl } = await response.json();
// Step 2: Upload directly to S3 with progress tracking
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
setProgress(Math.round((e.loaded / e.total) * 100));
}
});
await new Promise<void>((resolve, reject) => {
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
} else {
reject(new Error(`Upload failed: ${xhr.status}`));
}
});
xhr.addEventListener('error', () => reject(new Error('Upload failed')));
xhr.open('PUT', uploadUrl);
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);
});
alert('Upload successful! Key: ' + key);
} catch (error: any) {
alert('Upload failed: ' + error.message);
} finally {
setUploading(false);
setProgress(0);
}
};
return (
<div>
<input
type="file"
onChange={(e) => setFile(e.target.files?.[0] || null)}
disabled={uploading}
/>
<button onClick={handleUpload} disabled={!file || uploading}>
{uploading ? `Uploading ${progress}%...` : 'Upload'}
</button>
</div>
);
}Storage API Functions (All Server-Side Only)
All these functions must be used in API routes:
uploadFile(file, req?, token?, options?)
Complete server-side upload (use in API routes that accept FormData).
// app/api/upload-direct/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { uploadFile } from '@posif/sdk';
export async function POST(req: NextRequest) {
const formData = await req.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
}
try {
// Upload file to S3 (all handled server-side)
const key = await uploadFile(file, req, undefined, {
isPrivate: true,
onProgress: (percent) => {
console.log(`Upload progress: ${percent}%`);
},
});
return NextResponse.json({ key });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}Client-side usage:
// Upload file via FormData
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/upload-direct', {
method: 'POST',
body: formData,
});
const { key } = await response.json();generateUploadUrl(filename, req?, token?, options?)
Generate presigned upload URL (use in API routes).
// app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { generateUploadUrl } from '@posif/sdk';
export async function POST(req: NextRequest) {
const { filename } = await req.json();
// Token auto-extracted from request URL (?token=xxx)
const { key, uploadUrl } = await generateUploadUrl(filename, req, undefined, {
isPrivate: true,
expiresIn: 3600, // 1 hour
});
return NextResponse.json({ key, uploadUrl });
}generateFileUrls(key, req?, token?, isPrivate?)
Generate view and download URLs (use in API routes).
// app/api/files/[key]/urls/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { generateFileUrls } from '@posif/sdk';
export async function GET(
req: NextRequest,
{ params }: { params: { key: string } }
) {
const { viewUrl, downloadUrl } = await generateFileUrls(
params.key,
req,
undefined,
true // isPrivate
);
return NextResponse.json({ viewUrl, downloadUrl });
}Client-side usage:
const response = await fetch(`/api/files/${key}/urls`);
const { viewUrl, downloadUrl } = await response.json();
// Display image
<img src={viewUrl} alt="File" />generateDeleteUrl(key, req?, token?, isPrivate?)
Generate delete URL (use in API routes).
// app/api/files/[key]/delete-url/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { generateDeleteUrl } from '@posif/sdk';
export async function GET(
req: NextRequest,
{ params }: { params: { key: string } }
) {
const deleteUrl = await generateDeleteUrl(params.key, req);
return NextResponse.json({ deleteUrl });
}Client-side usage:
const response = await fetch(`/api/files/${key}/delete-url`);
const { deleteUrl } = await response.json();
await fetch(deleteUrl, { method: 'DELETE' });generateBatchUploadUrls(files, req?, token?, options?)
Generate multiple upload URLs at once (use in API routes).
// app/api/upload/batch/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { generateBatchUploadUrls } from '@posif/sdk';
export async function POST(req: NextRequest) {
const { files } = await req.json();
const results = await generateBatchUploadUrls(files, req, undefined, {
isPrivate: true,
});
return NextResponse.json({ results });
}Client-side usage:
const files = [
{ filename: 'photo1.jpg', contentType: 'image/jpeg' },
{ filename: 'photo2.png', contentType: 'image/png' }
];
const response = await fetch('/api/upload/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ files })
});
const { results } = await response.json();
// results: [{ key, uploadUrl, id }, ...]generatePublicUrl(key, req?, token?)
Get direct public URL for files in public buckets (use in API routes).
// app/api/files/[key]/public-url/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { generatePublicUrl } from '@posif/sdk';
export async function GET(
req: NextRequest,
{ params }: { params: { key: string } }
) {
const publicUrl = await generatePublicUrl(params.key, req);
return NextResponse.json({ publicUrl });
}Benefits of Public URLs:
- ✅ No Expiration: URL never expires (unlike presigned URLs)
- ✅ No Token Needed: After storing, use URL directly without authentication
- ✅ Database Friendly: Perfect for storing in databases
- ✅ CDN Compatible: Can be cached by CDNs
Use for: Profile pictures, product images, public content
Don't use for: Private documents, sensitive data
Complete Example - File Manager with API Routes
API Routes:
// app/api/files/upload-url/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { generateUploadUrl } from '@posif/sdk';
export async function POST(req: NextRequest) {
const { filename, isPrivate } = await req.json();
const { key, uploadUrl } = await generateUploadUrl(filename, req, undefined, {
isPrivate: isPrivate !== false,
});
return NextResponse.json({ key, uploadUrl });
}// app/api/files/[key]/urls/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { generateFileUrls } from '@posif/sdk';
export async function GET(
req: NextRequest,
{ params }: { params: { key: string } }
) {
const { viewUrl, downloadUrl } = await generateFileUrls(params.key, req);
return NextResponse.json({ viewUrl, downloadUrl });
}// app/api/files/[key]/delete-url/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { generateDeleteUrl } from '@posif/sdk';
export async function GET(
req: NextRequest,
{ params }: { params: { key: string } }
) {
const deleteUrl = await generateDeleteUrl(params.key, req);
return NextResponse.json({ deleteUrl });
}Client Component:
'use client';
import { useState } from 'react';
interface FileItem {
key: string;
filename: string;
viewUrl?: string;
}
export default function FileManager() {
const [files, setFiles] = useState<FileItem[]>([]);
const handleUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
try {
// Step 1: Get upload URL from API
const uploadResponse = await fetch('/api/files/upload-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: file.name, isPrivate: true }),
});
const { key, uploadUrl } = await uploadResponse.json();
// Step 2: Upload to S3
await fetch(uploadUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type },
});
// Step 3: Get view URL
const urlsResponse = await fetch(`/api/files/${key}/urls`);
const { viewUrl } = await urlsResponse.json();
// Step 4: Add to list
setFiles((prev) => [...prev, { key, filename: file.name, viewUrl }]);
} catch (error: any) {
alert('Upload failed: ' + error.message);
}
};
const handleDelete = async (key: string) => {
try {
// Get delete URL
const response = await fetch(`/api/files/${key}/delete-url`);
const { deleteUrl } = await response.json();
// Delete from S3
await fetch(deleteUrl, { method: 'DELETE' });
// Remove from list
setFiles((prev) => prev.filter((f) => f.key !== key));
} catch (error: any) {
alert('Delete failed: ' + error.message);
}
};
return (
<div>
<input type="file" onChange={handleUpload} />
<div>
{files.map((file) => (
<div key={file.key}>
{file.viewUrl && (
<img src={file.viewUrl} alt={file.filename} width="100" />
)}
<span>{file.filename}</span>
<button onClick={() => handleDelete(file.key)}>Delete</button>
</div>
))}
</div>
</div>
);
}Advanced: Using getStorage() Directly
For maximum control in API routes:
import { NextRequest, NextResponse } from 'next/server';
import { getStorage } from '@posif/sdk';
export async function POST(req: NextRequest) {
// Get storage instance (server-side only)
const storage = await getStorage(req);
// Generate key
const key = await storage.generateKey('photo.jpg', true);
// Generate presigned URL
const uploadUrl = await storage.getPresignedUrl(key, 'upload', true);
// Batch operations
const files = [
{ filename: 'photo1.jpg', contentType: 'image/jpeg' },
{ filename: 'photo2.png', contentType: 'image/png' },
];
const batchResults = await storage.getBatchUploadUrls(files, true);
return NextResponse.json({ key, uploadUrl, batchResults });
}Why API Routes are Required
Security & Architecture:
- 🔒 Credential Protection: S3 credentials must never be exposed to the browser
- 🔑 Token Validation: Server validates JWT tokens before fetching credentials
- 👥 Access Control: Server enforces user/group file isolation
- 🛡️ Rate Limiting: Server can implement upload limits
- ⚡ Performance: Credentials are cached server-side
The storage functions use assertServerOnly() which throws an error if called from client-side code.
Package Exports Clarification
⚠️ Important Note about /storage export:
The /storage export (@posif/sdk/storage) is for bundle optimization in API routes. It excludes MongoDB dependencies to reduce bundle size, but the storage functions still require server-side execution.
// ✅ Correct: Use in API routes to avoid MongoDB in bundle
// app/api/upload/route.ts
import { generateUploadUrl } from '@posif/sdk/storage';
export async function POST(req: NextRequest) {
const { key, uploadUrl } = await generateUploadUrl(filename, req);
return NextResponse.json({ key, uploadUrl });
}// ❌ Incorrect: This will throw error
'use client';
import { uploadFile } from '@posif/sdk/storage';
// ❌ ERROR: "Function can only be used on the server"
const key = await uploadFile(file);Benefits
✅ Secure: S3 credentials never exposed to browser
✅ Simple API Routes: Minimal boilerplate required
✅ Direct S3 Upload: Files go straight to S3 (no proxy through your server)
✅ Progress Tracking: Built-in progress callbacks (via XMLHttpRequest)
✅ Type Safe: Full TypeScript support
✅ User/Group Isolation: Automatic file organization
✅ Flexible: Use high-level functions or getStorage() for control
✅ Optimized Bundles: Use /storage export to exclude MongoDB from API routes
What the SDK Handles
The SDK completely manages (server-side):
- 🔐 S3 credential fetching and caching
- 🔑 JWT token extraction and validation
- 📝 Presigned URL generation
- 👥 User/group file isolation
- ♻️ Connection pooling and reuse
- ⚠️ Error handling and retries
What You Build
You need to create:
- 🛜️ API Routes: Server endpoints that call SDK functions
- 🎨 UI Components: Upload buttons, galleries, progress bars
- 💾 Database Logic: Store S3 keys and metadata (optional)
- 🎯 Business Logic: Validation, permissions, etc.
For more examples including batch uploads and custom progress bars, see examples/storage-examples.ts
License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
