UXDL Docs

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

bash
pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner

Client setup

typescript
// 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

dotenv
AWS_REGION=us-east-1
S3_BUCKET=app-uploads-prod
S3_PUBLIC_BUCKET=app-assets-prod
CLOUDFRONT_DOMAIN=cdn.example.com

Presigned upload flow

typescript
// 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)

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

typescript
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

PrefixPurposeAccess
uploads/{uuid}/User-uploaded filesPresigned GET
exports/{date}/Generated CSV/PDF exportsPresigned GET, TTL 24h
assets/Public static assetsCloudFront (public)

Official documentation