Authentication
Overview
Section titled “Overview”Uploadista is designed to work with your existing authentication system. Rather than imposing a specific auth solution, Uploadista provides flexible middleware hooks on the server and credential injection on the client, allowing you to integrate any authentication method you already use.
Key design principles:
- Bring your own auth: Use JWT, API keys, sessions, OAuth, or any auth system
- Middleware-based: Server adapters accept auth middleware that you control
- Client flexibility: Clients can inject credentials via headers, cookies, or custom logic
- Optional by default: Authentication is entirely optional for development or public endpoints
How It Works
Section titled “How It Works”Server-Side: Auth Middleware
Section titled “Server-Side: Auth Middleware”Each server adapter (Hono, Express, Fastify) accepts an authMiddleware function. This middleware:
- Receives the incoming request context
- Validates credentials using your auth system
- Returns an
AuthContextwith client ID and permissions, ornullto reject
// The auth middleware signaturetype AuthMiddleware = (context) => Promise<AuthContext | null>;
// AuthContext returned on successful authenticationtype AuthContext = { clientId: string; // Unique identifier for the authenticated user/client permissions?: string[]; // Optional permissions array (see Permissions section) metadata?: Record<string, unknown>; // Optional custom data};Client-Side: Credential Injection
Section titled “Client-Side: Credential Injection”The client supports two authentication modes:
- Direct Mode: You provide credentials (headers, cookies) for each request
- UploadistaCloud Mode: Automatic JWT token exchange with your auth server
// Direct mode - bring your own authauth: { mode: 'direct', getCredentials: async () => ({ headers: { 'Authorization': `Bearer ${await getToken()}` } })}
// UploadistaCloud mode - automatic token managementauth: { mode: 'uploadista-cloud', authServerUrl: '/api/auth/token', clientId: 'your-client-id'}Server Authentication
Section titled “Server Authentication”Hono Adapter
Section titled “Hono Adapter”import { createUploadistaServer } from "@uploadista/server";import { honoAdapter } from "@uploadista/adapters-hono";
const uploadistaServer = await createUploadistaServer({ dataStore: s3Store({ /* config */ }), kvStore: redisKvStore({ /* config */ }), flows, adapter: honoAdapter({ authMiddleware: async (c) => { // Extract and validate credentials const token = c.req.header("Authorization")?.split(" ")[1]; if (!token) return null;
try { const payload = await verifyJWT(token, process.env.JWT_SECRET); return { clientId: payload.sub, permissions: payload.permissions || [], }; } catch { return null; } }, }),});Express Adapter
Section titled “Express Adapter”import { expressAdapter } from "@uploadista/adapters-express";
const uploadistaServer = await createUploadistaServer({ // ... other config adapter: expressAdapter({ authMiddleware: async (ctx) => { const token = ctx.request.headers.authorization?.split(" ")[1]; if (!token) return null;
try { const payload = await verifyJWT(token, process.env.JWT_SECRET); return { clientId: payload.sub, permissions: payload.permissions }; } catch { return null; } }, }),});Fastify Adapter
Section titled “Fastify Adapter”import { fastifyAdapter } from "@uploadista/adapters-fastify";
const uploadistaServer = await createUploadistaServer({ // ... other config adapter: fastifyAdapter({ authMiddleware: async (ctx) => { const token = ctx.request.headers.authorization?.split(" ")[1]; if (!token) return null;
try { const payload = await verifyJWT(token, process.env.JWT_SECRET); return { clientId: payload.sub, permissions: payload.permissions }; } catch { return null; } }, }),});Common Auth Patterns
Section titled “Common Auth Patterns”authMiddleware: async (ctx) => { const token = ctx.request.headers.authorization?.replace('Bearer ', ''); if (!token) return null;
const user = await verifyJWT(token); return user ? { clientId: user.sub, permissions: user.permissions } : null;}authMiddleware: async (ctx) => { const apiKey = ctx.request.headers['x-api-key']; if (!apiKey) return null;
const client = await db.clients.findByApiKey(apiKey); return client ? { clientId: client.id, permissions: client.permissions } : null;}authMiddleware: async (ctx) => { const sessionId = ctx.request.cookies?.session; if (!sessionId) return null;
const session = await getSession(sessionId); return session ? { clientId: session.userId } : null;}authMiddleware: async (ctx) => { const token = ctx.request.headers.authorization?.replace('Bearer ', ''); if (!token) return null;
// Validate with your OAuth provider const userInfo = await oauth.validateAccessToken(token); return userInfo ? { clientId: userInfo.sub } : null;}Client Authentication
Section titled “Client Authentication”Direct Mode
Section titled “Direct Mode”Direct mode gives you complete control over authentication. Provide a getCredentials function that returns headers or cookies:
import { createUploadistaClient } from "@uploadista/client";
const client = createUploadistaClient({ baseUrl: "https://api.example.com", storageId: "my-storage", chunkSize: 1024 * 1024,
auth: { mode: 'direct', getCredentials: async () => ({ headers: { 'Authorization': `Bearer ${await getAccessToken()}` } }) }});
// Credentials are automatically attached to all requestsawait client.upload(file);Direct Mode Examples
Section titled “Direct Mode Examples”auth: { mode: 'direct', getCredentials: async () => { const token = await oauth.getAccessToken(); return { headers: { 'Authorization': `Bearer ${token}` } }; }}auth: { mode: 'direct', getCredentials: () => ({ headers: { 'X-API-Key': process.env.API_KEY } })}auth: { mode: 'direct', getCredentials: () => ({ cookies: { 'session': getSessionId() } })}UploadistaCloud Mode
Section titled “UploadistaCloud Mode”UploadistaCloud mode provides automatic JWT token exchange. Your backend exchanges credentials with Uploadista Cloud, and the client automatically manages token lifecycle.
const client = createUploadistaClient({ storageId: "my-storage", chunkSize: 1024 * 1024,
auth: { mode: 'uploadista-cloud', authServerUrl: '/api/auth/token', clientId: 'your-client-id' }});Server endpoint implementation (Next.js):
import { getAuthCredentials } from '@uploadista/server/auth';
export const GET = async (req, ctx) => { const { clientId } = await ctx.params; const apiKey = process.env.UPLOADISTA_API_KEY;
const response = await getAuthCredentials({ uploadistaClientId: clientId, uploadistaApiKey: apiKey, });
if (!response.isValid) { return Response.json({ error: response.error }, { status: 500 }); }
return Response.json(response.data);};Token lifecycle features:
- Caching: Tokens are cached to minimize auth server requests
- Auto-refresh: Tokens are refreshed 60 seconds before expiration
- Retry: Automatic retry with fresh token on 401 errors
Permissions
Section titled “Permissions”Uploadista supports fine-grained, permission-based access control. Permissions follow a resource:action format with wildcards and hierarchies.
Permission Format
Section titled “Permission Format”// Permission format: resource:action"upload:create" // Create new uploads"flow:execute" // Execute flows"engine:health" // Access health endpoints"engine:*" // All engine permissions (wildcard)Available Permissions
Section titled “Available Permissions”| Resource | Permission | Description |
|---|---|---|
| Upload | upload:* | All upload operations |
upload:create | Start new uploads, upload chunks | |
upload:read | View upload status, download files | |
upload:cancel | Cancel in-progress uploads | |
| Flow | flow:* | All flow operations |
flow:execute | Run flows on uploaded files | |
flow:status | Check flow job status | |
flow:cancel | Cancel running flows | |
| Engine | engine:* | All admin operations |
engine:health | Access /health endpoint | |
engine:metrics | Access /metrics endpoint | |
engine:dlq | Full Dead Letter Queue access | |
engine:dlq:read | Read DLQ entries | |
engine:dlq:write | Retry or delete DLQ entries |
Granting Permissions
Section titled “Granting Permissions”Include permissions in the AuthContext returned by your auth middleware:
authMiddleware: async (ctx) => { const user = await validateUser(ctx); if (!user) return null;
return { clientId: user.organizationId, permissions: user.isAdmin ? ['engine:*'] // Admin gets full access : ['flow:*', 'upload:*'], // Regular user gets flow and upload access };}Permission Matching
Section titled “Permission Matching”// Exact matchhasPermission(["flow:execute"], "flow:execute"); // truehasPermission(["flow:execute"], "flow:cancel"); // false
// Wildcard matchhasPermission(["flow:*"], "flow:execute"); // truehasPermission(["flow:*"], "flow:cancel"); // true
// Hierarchical matchhasPermission(["engine:dlq"], "engine:dlq:read"); // truehasPermission(["engine:dlq"], "engine:dlq:write"); // trueRole-Based Setup
Section titled “Role-Based Setup”Map your existing roles to Uploadista permissions:
import { PERMISSION_SETS } from "@uploadista/server";
const roleToPermissions = { admin: PERMISSION_SETS.ADMIN, editor: ["flow:execute", "flow:status", "upload:*"], viewer: ["flow:status", "upload:read"], uploader: ["upload:create", "upload:read"],};
// In your auth middlewareauthMiddleware: async (ctx) => { const user = await validateUser(ctx); if (!user) return null;
return { clientId: user.id, permissions: roleToPermissions[user.role] ?? [], };}No Authentication (Development)
Section titled “No Authentication (Development)”Authentication is completely optional. If you don’t provide authMiddleware (server) or auth config (client), everything works without authentication:
// Server - no auth middlewareconst uploadistaServer = await createUploadistaServer({ dataStore: fileStore({ directory: "./uploads" }), kvStore: fileKvStore({ directory: "./uploads" }), flows, adapter: expressAdapter(), // No authMiddleware - all requests allowed});
// Client - no auth configconst client = createUploadistaClient({ baseUrl: "http://localhost:3000", storageId: "local", chunkSize: 1024 * 1024, // No auth - no credentials sent});Error Responses
Section titled “Error Responses”| Status | Error | Description |
|---|---|---|
401 Unauthorized | AUTHENTICATION_REQUIRED | No authentication provided or auth middleware returned null |
403 Forbidden | PERMISSION_DENIED | Authenticated but missing required permission |
// 401 response{ "code": "AUTHENTICATION_REQUIRED", "message": "Authentication required"}
// 403 response{ "code": "PERMISSION_DENIED", "message": "Permission denied: flow:execute required"}Security Best Practices
Section titled “Security Best Practices”- Always use HTTPS in production - credentials sent over HTTP can be intercepted
- Use short-lived tokens - reduce impact if token is compromised
- Validate tokens properly - use established libraries (jose, jsonwebtoken, etc.)
- Use least-privilege permissions - grant only the permissions each user needs
- Separate admin access - keep
engine:*permissions separate from regular users - Don’t log credentials or tokens - they contain sensitive information
- Implement rate limiting - prevent brute force attacks
Troubleshooting
Section titled “Troubleshooting”Always getting 401
Section titled “Always getting 401”- Verify your auth middleware is returning a valid
AuthContextobject (notnull) - Check credentials are being sent correctly from the client
- Add logging to your auth middleware to debug
Client credentials not being sent
Section titled “Client credentials not being sent”- Verify
authconfig is passed tocreateUploadistaClient - Check
getCredentials()is returning the correct format - Ensure credentials are not empty
Getting 403 Forbidden
Section titled “Getting 403 Forbidden”- Check that
permissionsarray is included in yourAuthContext - Verify the required permission is in the user’s permissions list
- Use wildcard permissions (
flow:*,upload:*) for full access
Related Concepts
Section titled “Related Concepts”- Express Integration - Express-specific auth examples
- Hono Integration - Hono-specific auth examples
- Fastify Integration - Fastify-specific auth examples
- React Client - Client-side auth in React