Data

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:

  1. Request — the client asks for an upload URL. The backend picks the key and signs a PUT.
  2. 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:

FunctionWhat 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.