Storage and email
Upload files straight from the browser to S3, and send email through one function.
Uploading files
Files go straight from the browser to S3. The backend signs a short-lived URL, so AWS credentials never reach the client and large files never pass through the server.
It takes two calls:
- Request — the client asks for an upload URL. The backend picks the key and signs a
PUT. - Confirm — after the upload, the client sends the key back. The backend checks the key belongs to the caller, then saves it.
The confirm step exists because the backend can't see whether the browser's upload succeeded. The avatar upload in backend/src/trpc/routers/profile.ts is the working example:
requestAvatarUpload: protectedProcedure
.input(z.object({ contentType: z.enum(ALLOWED_AVATAR_CONTENT_TYPES) }))
.mutation(async ({ ctx, input }) => {
const key = `${ctx.session.user.id}/${crypto.randomUUID()}.${EXTENSION_FOR_CONTENT_TYPE[input.contentType]}`;
const uploadUrl = await getPresignedUploadUrl(AVATAR_BUCKET, key, input.contentType);
return { uploadUrl, key };
}),
Storage helpers
All in backend/src/storage/index.ts:
| Function | What it does |
|---|---|
getPresignedUploadUrl(bucket, key, contentType) | A signed PUT URL for the browser |
getPresignedDownloadUrl(bucket, key) | A signed GET URL for the browser |
uploadObject(key, body, contentType) | Write from the server |
getObjectText(key) | Read text from the server |
Store the key in the database, never a URL. Signed URLs expire; keys don't. Resolve a key to a URL when you send it to the client.
Local buckets are created on boot, in RustFS. Browse them at localhost:9001.
Sending email
import { sendEmail } from "../email/index.js";
await sendEmail(user.email, "Welcome to Bones", html);
Locally, every email lands in Mailpit at localhost:8025. In production, sendEmail goes through SES.
Writing a template
Templates are React components in email-templates/emails/. Preview them live while you edit:
yarn email-templates:dev
Then open localhost:3010. Keep to tables and inline styles — Outlook ignores most modern CSS. Colors come from email-templates/tokens.ts, which is generated from shared-ui/src/tokens.css; don't edit it by hand.
The backend imports the built templates. After editing one, restart the backend and it rebuilds them on boot.
Related
- Email — why templates are React.
- AWS — S3 and SES, and their local stand-ins.
- Environment variables