
How I Implemented Secure S3 Presigned URLs and Server Uploads for Procurement Documents
Direct answer (40 words): Use the AWS SDK v3 S3Client with a path-style endpoint for S3-compatible hosts, generate keys with timestamp + random + sanitized filenames, upload server-side from server actions, and issue short-lived presigned GET URLs only when the user is authorized—never expose raw bucket URLs in the UI.
Why this pattern matters
Procurement systems store contracts, invoices, and supplier compliance PDFs. Public URLs or permanent bucket links are a compliance risk. Presigned URLs trade link permanence for time-boxed access aligned with audit expectations.
Key implementation from this codebase
1. Client + key generation
Keys are never taken from the client as-is; filenames are sanitized and prefixed by domain folder.
export function generateS3Key(folder: string, filename: string): string {
const sanitized = filename.replace(/[^a-zA-Z0-9._-]/g, "_");
const timestamp = Date.now();
const randomId = Math.random().toString(36).substring(2, 10);
return `${folder}/${timestamp}-${randomId}-${sanitized}`;
}
2. Upload and presign
export async function uploadFile(
buffer: Buffer,
key: string,
contentType: string
): Promise<string> {
const command = new PutObjectCommand({
Bucket: BUCKET,
Key: key,
Body: buffer,
ContentType: contentType,
});
await s3Client.send(command);
return key;
}
/**
* Generate a presigned URL for secure, temporary access to an S3 object.
* @param key - The S3 object key.
* @param expiresIn - URL validity in seconds (default: 3600 = 1 hour).
*/
export async function getPresignedUrl(
key: string,
expiresIn: number = 3600
): Promise<string> {
const command = new GetObjectCommand({
Bucket: BUCKET,
Key: key,
});
return getSignedUrl(s3Client, command, { expiresIn });
}
3. Environment contract
.env.example documents S3-compatible variables (Tigris, AWS S3, Cloudflare R2, etc.):
# S3-compatible Storage (e.g. Tigris, AWS S3, Cloudflare R2)
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
AWS_REGION="auto"
AWS_BUCKET_NAME=""
S3_ENDPOINT=""
Operational notes
- Rotate keys and restrict IAM (or equivalent) to PutObject/GetObject on the procurement prefix.
- Default expiry of one hour balances UX and leakage; shorten for highly sensitive bundles.
- Keep authorization checks in the server action or route before calling
getPresignedUrl.
Read the full case study: CPMS — Comprehensive Procurement Management System