Skip to content

File Storage

EnvironmentStorageHow
Local / cPanelLocal disk (backend/uploads/)Node.js fs.writeFileSync
Cloudflare WorkersCloudflare R2globalThis.BUCKET.put()

The unified interface lives in backend/utils/storage.js and exposes three functions:

saveFile(file, folder) // Returns filename/key string
deleteFile(filename) // Removes file
getFileUrl(filename) // Returns URL string
const saveFile = async (file, folder = 'others') => {
const ext = path.extname(file.originalname).toLowerCase();
const uniqueName = Date.now() + '-' + Math.round(Math.random() * 1E9) + ext;
const key = `${folder}/${uniqueName}`;
if (globalThis.BUCKET) {
// Production: save to R2
await globalThis.BUCKET.put(key, file.buffer, {
httpMetadata: { contentType: file.mimetype }
});
return key;
}
// Development: save to local disk
fs.writeFileSync(path.join(__dirname, '..', 'uploads', folder, uniqueName), file.buffer);
return key;
};

Files come in as Buffer via multer’s memory storage (config/multer.js), so file.buffer is always available.

Known issue: getFileUrl is broken in production

Section titled “Known issue: getFileUrl is broken in production”

getFileUrl currently returns /uploads/${filename}, which works in local dev (Express serves /uploads/ as static files) but is meaningless in a Cloudflare Worker (no filesystem, no static file serving).

In production, R2 files are inaccessible via getFileUrl. Files are saved successfully to R2 but the URL is never a valid R2 link.

The fix requires choosing one of:

  1. R2 with a public bucket + custom domain — Set the R2 bucket to public, configure a custom domain in the Cloudflare dashboard, and update getFileUrl to return https://uploads.shanvitravel.com.np/${filename}.

  2. R2 with signed URLs — Generate a time-limited signed URL per request using the R2 API. More secure but adds per-request latency.

  3. Cloudflare Images — For image files specifically, use Cloudflare Images which provides automatic resizing, CDN delivery, and a clean URL scheme.

Option 1 is the simplest fix and the right starting point.

config/multer.js configures multer to use memory storage (files stored as Buffer in RAM, not written to temp disk). This is required because Cloudflare Workers have no writable filesystem.

// config/multer.js (simplified)
const multer = require('multer');
const storage = multer.memoryStorage();
const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 } }); // 10MB

File type validation should be added here — currently any file extension is accepted. For KYC and submission files, acceptable types should be limited to JPEG, PNG, and PDF.

Folder keyUsed for
kyc/KYC registration and PAN files
others/Default — submission passports, receipts, etc.
vfs/VFS tracking task documents

No explicit folder is passed for submission files, so they land in others/. This makes it harder to clean up orphaned files and harder to set per-folder retention policies. Each feature should pass a meaningful folder argument to saveFile.