AWS S3
Object storage, presigned uploads, and file access patterns.
AWS S3 is our object storage for user uploads, exports, static assets, and media files. Clients upload directly to S3 via presigned URLs — the Express API never proxies file bytes.
Install
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presignerClient setup
// src/lib/s3.ts
import { S3Client } from "@aws-sdk/client-s3";
export const s3 = new S3Client({
region: process.env.AWS_REGION ?? "us-east-1",
});ECS tasks use an IAM task role — no access keys in production. Local dev uses AWS SSO or ~/.aws/credentials.
Environment
AWS_REGION=us-east-1
S3_BUCKET=app-uploads-prod
S3_PUBLIC_BUCKET=app-assets-prod
CLOUDFRONT_DOMAIN=cdn.example.comPresigned upload flow
// src/services/uploads.service.ts
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { s3 } from "../lib/s3";
import { randomUUID } from "crypto";
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "application/pdf"];
const MAX_SIZE = 10 * 1024 * 1024; // 10 MB
export async function createPresignedUpload(filename: string, contentType: string, size: number) {
if (!ALLOWED_TYPES.includes(contentType)) throw new Error("File type not allowed");
if (size > MAX_SIZE) throw new Error("File too large");
const key = `uploads/${randomUUID()}/${filename}`;
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: key,
ContentType: contentType,
ContentLength: size,
});
const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 });
return { uploadUrl, key, expiresIn: 300 };
}
export function publicUrl(key: string) {
return `https://${process.env.CLOUDFRONT_DOMAIN}/${key}`;
}Download (presigned GET)
import { GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
export async function createPresignedDownload(key: string) {
const command = new GetObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: key,
});
return getSignedUrl(s3, command, { expiresIn: 3600 });
}Server-side upload (exports, generated files)
import { PutObjectCommand } from "@aws-sdk/client-s3";
export async function uploadBuffer(key: string, body: Buffer, contentType: string) {
await s3.send(
new PutObjectCommand({
Bucket: process.env.S3_BUCKET!,
Key: key,
Body: body,
ContentType: contentType,
}),
);
return publicUrl(key);
}Key conventions
| Prefix | Purpose | Access |
|---|---|---|
uploads/{uuid}/ | User-uploaded files | Presigned GET |
exports/{date}/ | Generated CSV/PDF exports | Presigned GET, TTL 24h |
assets/ | Public static assets | CloudFront (public) |