This project now includes full-stack capabilities with integrated S3 file storage functionality.
The file storage system uses Manus's built-in S3 storage service, which is automatically configured and requires no manual API key setup.
Located in server/routers.ts, the files router provides two main procedures:
- getUploadUrl - Generates a unique file key for upload
- upload - Uploads the file data to S3 storage
import { storagePut, storageGet } from "./server/storage";
// Upload a file
const { url, key } = await storagePut(
"user-123/profile/avatar.jpg",
fileBuffer,
"image/jpeg"
);
// Get a file URL
const { url } = await storageGet("user-123/profile/avatar.jpg");import { trpc } from "@/lib/trpc";
function FileUploadComponent() {
const uploadMutation = trpc.files.upload.useMutation();
const handleFileUpload = async (file: File) => {
// Convert file to base64
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = async () => {
const base64Data = reader.result as string;
const base64Content = base64Data.split(',')[1];
// Get upload URL
const { fileKey } = await trpc.files.getUploadUrl.mutate({
fileName: file.name,
contentType: file.type,
});
// Upload file
const result = await uploadMutation.mutateAsync({
fileKey,
fileData: base64Content,
contentType: file.type,
});
console.log('File uploaded:', result.url);
};
};
return (
<input
type="file"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleFileUpload(file);
}}
/>
);
}Files are automatically organized by user ID to prevent conflicts:
user-{userId}/uploads/{timestamp}-{random}-{filename}
- All file operations require authentication (using
protectedProcedure) - Files are stored in a public S3 bucket, so URLs are directly accessible
- Random suffixes prevent file enumeration attacks
- Maximum file size: Determined by your Manus plan
- Supported file types: All types (images, documents, videos, etc.)
- Store metadata in database: Save file URLs, names, sizes, and other metadata in your database tables
- Use S3 for bytes only: Never store file content in database columns
- Add random suffixes: Always include random components in file keys to prevent enumeration
- Validate file types: Check file types on both client and server side
- Handle errors: Implement proper error handling for upload failures
// Database schema (drizzle/schema.ts)
export const users = mysqlTable("users", {
id: int("id").autoincrement().primaryKey(),
profilePictureUrl: text("profilePictureUrl"),
profilePictureKey: text("profilePictureKey"),
// ... other fields
});
// Backend procedure (server/routers.ts)
updateProfilePicture: protectedProcedure
.input(z.object({
fileData: z.string(),
contentType: z.string(),
fileName: z.string(),
}))
.mutation(async ({ ctx, input }) => {
const userId = ctx.user.id;
const fileKey = `user-${userId}/profile/${Date.now()}-${input.fileName}`;
const buffer = Buffer.from(input.fileData, 'base64');
const { url } = await storagePut(fileKey, buffer, input.contentType);
// Update database
await db.update(users)
.set({ profilePictureUrl: url, profilePictureKey: fileKey })
.where(eq(users.id, userId));
return { url };
}),- Check that
BUILT_IN_FORGE_API_URLandBUILT_IN_FORGE_API_KEYare set (automatically injected by platform) - Verify file size is within limits
- Check network connectivity
- Ensure the returned URL is used correctly
- Check browser console for CORS errors (should not occur with Manus storage)
- See
server/storage.tsfor storage helper functions - See
server/routers.tsfor file upload API implementation - Refer to the main README.md for general full-stack development guidelines