npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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/sdk

Package 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 progress
  • generateUploadUrl() - Get presigned upload URL
  • generateFileUrls() - Get view/download URLs
  • generateBatchUploadUrls() - Batch upload URLs
  • generateDeleteUrl() - Get delete URL
  • generatePublicUrl() - Get public URL for public buckets
  • getStorage() - 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 access
  • getMongoDbConfig() - 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 /storage to avoid MongoDB in storage API route bundles
  • API Routes (Database Only): Use /server if you only need database access
  • Mixed Usage: Keep using main export or use both /storage and /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:

  1. Explicit token parameter: getDb(req, 'your_token')
  2. URL parameters (server-side): /api/users?token=xxx (automatically extracted)
  3. Pages Router query: req.query.token (Next.js Pages Router)
  4. 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_URI from 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:

  1. ✅ Create API routes that use storage functions (server-side)
  2. ✅ Call those API routes from client components
  3. ✅ 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 browser

This 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 URL

Quick 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:

  1. 🔒 Credential Protection: S3 credentials must never be exposed to the browser
  2. 🔑 Token Validation: Server validates JWT tokens before fetching credentials
  3. 👥 Access Control: Server enforces user/group file isolation
  4. 🛡️ Rate Limiting: Server can implement upload limits
  5. 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.