Skip to content

Authentication

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

Each server adapter (Hono, Express, Fastify) accepts an authMiddleware function. This middleware:

  1. Receives the incoming request context
  2. Validates credentials using your auth system
  3. Returns an AuthContext with client ID and permissions, or null to reject
// The auth middleware signature
type AuthMiddleware = (context) => Promise<AuthContext | null>;
// AuthContext returned on successful authentication
type 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
};

The client supports two authentication modes:

  1. Direct Mode: You provide credentials (headers, cookies) for each request
  2. UploadistaCloud Mode: Automatic JWT token exchange with your auth server
// Direct mode - bring your own auth
auth: {
mode: 'direct',
getCredentials: async () => ({
headers: { 'Authorization': `Bearer ${await getToken()}` }
})
}
// UploadistaCloud mode - automatic token management
auth: {
mode: 'uploadista-cloud',
authServerUrl: '/api/auth/token',
clientId: 'your-client-id'
}
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;
}
},
}),
});
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;
}
},
}),
});
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;
}
},
}),
});
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;
}

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 requests
await client.upload(file);
auth: {
mode: 'direct',
getCredentials: async () => {
const token = await oauth.getAccessToken();
return {
headers: { 'Authorization': `Bearer ${token}` }
};
}
}

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

app/api/auth/token/[clientId]/route.ts
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

Uploadista supports fine-grained, permission-based access control. Permissions follow a resource:action format with wildcards and hierarchies.

// Permission format: resource:action
"upload:create" // Create new uploads
"flow:execute" // Execute flows
"engine:health" // Access health endpoints
"engine:*" // All engine permissions (wildcard)
ResourcePermissionDescription
Uploadupload:*All upload operations
upload:createStart new uploads, upload chunks
upload:readView upload status, download files
upload:cancelCancel in-progress uploads
Flowflow:*All flow operations
flow:executeRun flows on uploaded files
flow:statusCheck flow job status
flow:cancelCancel running flows
Engineengine:*All admin operations
engine:healthAccess /health endpoint
engine:metricsAccess /metrics endpoint
engine:dlqFull Dead Letter Queue access
engine:dlq:readRead DLQ entries
engine:dlq:writeRetry or delete DLQ entries

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
};
}
// Exact match
hasPermission(["flow:execute"], "flow:execute"); // true
hasPermission(["flow:execute"], "flow:cancel"); // false
// Wildcard match
hasPermission(["flow:*"], "flow:execute"); // true
hasPermission(["flow:*"], "flow:cancel"); // true
// Hierarchical match
hasPermission(["engine:dlq"], "engine:dlq:read"); // true
hasPermission(["engine:dlq"], "engine:dlq:write"); // true

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 middleware
authMiddleware: async (ctx) => {
const user = await validateUser(ctx);
if (!user) return null;
return {
clientId: user.id,
permissions: roleToPermissions[user.role] ?? [],
};
}

Authentication is completely optional. If you don’t provide authMiddleware (server) or auth config (client), everything works without authentication:

// Server - no auth middleware
const uploadistaServer = await createUploadistaServer({
dataStore: fileStore({ directory: "./uploads" }),
kvStore: fileKvStore({ directory: "./uploads" }),
flows,
adapter: expressAdapter(), // No authMiddleware - all requests allowed
});
// Client - no auth config
const client = createUploadistaClient({
baseUrl: "http://localhost:3000",
storageId: "local",
chunkSize: 1024 * 1024,
// No auth - no credentials sent
});
StatusErrorDescription
401 UnauthorizedAUTHENTICATION_REQUIREDNo authentication provided or auth middleware returned null
403 ForbiddenPERMISSION_DENIEDAuthenticated but missing required permission
// 401 response
{
"code": "AUTHENTICATION_REQUIRED",
"message": "Authentication required"
}
// 403 response
{
"code": "PERMISSION_DENIED",
"message": "Permission denied: flow:execute required"
}
  1. Always use HTTPS in production - credentials sent over HTTP can be intercepted
  2. Use short-lived tokens - reduce impact if token is compromised
  3. Validate tokens properly - use established libraries (jose, jsonwebtoken, etc.)
  4. Use least-privilege permissions - grant only the permissions each user needs
  5. Separate admin access - keep engine:* permissions separate from regular users
  6. Don’t log credentials or tokens - they contain sensitive information
  7. Implement rate limiting - prevent brute force attacks
  • Verify your auth middleware is returning a valid AuthContext object (not null)
  • Check credentials are being sent correctly from the client
  • Add logging to your auth middleware to debug
  • Verify auth config is passed to createUploadistaClient
  • Check getCredentials() is returning the correct format
  • Ensure credentials are not empty
  • Check that permissions array is included in your AuthContext
  • Verify the required permission is in the user’s permissions list
  • Use wildcard permissions (flow:*, upload:*) for full access