Short answer: host compressed course artwork and reference it from the HTML email. This keeps the message small and preserves open measurement, while attaching every image increases message size and spam risk. The catch is real: hosted images disappear when a mail client blocks remote content. Build the lesson announcement so the title, date, destination link, and primary action still work with every image turned off.
For an edtech product, moderation coverage is the first gate. Compression is useful only after an asset is acceptable to send. My ship-first decision is therefore a remote-asset pipeline with moderation at ingestion, format conversion before storage, and a queue boundary for work that shouldn't delay the authoring request. Attachments remain a deliberate fallback for material that must render without remote loading, not the default transport for decorative course artwork.
What should happen before a course image reaches an email?
Treat the email as the final consumer, not as the image-processing system. An instructor uploads artwork; the service checks the chosen moderation provider's coverage, converts the accepted image to an email-appropriate format, stores it privately, and obtains a presigned delivery URL. A worker can own that chain when processing doesn't belong on the interactive request path. The HTML receives only the resulting URL and useful alternative text.
Order matters. Moderation after delivery leaves a window in which unreviewed artwork can be referenced. Conversion after email assembly makes message generation depend on media work. A queue boundary separates those concerns, but standard queues require idempotent consumers because delivery is at least once. Use a stable asset job identifier so a retry can't create a second logical result.
No moderation match means no send.
The fallback should be text or pre-approved artwork, not the original upload. This is stricter than treating moderation as a dashboard warning, and it's the right bias for student-facing mail.
Inspect the contract before wiring the pipeline
The least risky runnable example is the discovery call, because it avoids guessing request fields. Infrai exposes a public self-describing discovery surface: the capability response includes the request JSON Schema, response schema, billing information, and runnable examples. The live surface reports 295 routes across 20 modules, with examples in 10 languages. A solo team can read the exact contract at build time, then pin the generated integration in source control.
This TypeScript script checks the three capability contracts needed by the design. It uses one configured base URL and needs no credential because discovery is public. It deliberately stops if a documented method or path changes.
const baseURL = process.env.INFRAI_BASE_URL;
if (!baseURL) throw new Error("Set INFRAI_BASE_URL to the documented v1 base URL");
type Capability = {
id: string;
method: string;
path: string;
available: boolean;
vendors_ready: string[];
vendors_pending: string[];
params: unknown;
};
const expected = [
{ id: "image.moderate", method: "POST", path: "/v1/image/moderate" },
{ id: "image.convert", method: "POST", path: "/v1/image/convert" },
{
id: "storage.object.presign",
method: "POST",
path: "/v1/storage/object/presign/{bucket}/{key}",
},
];
for (const contract of expected) {
const response = await fetch(
`${baseURL}/discovery/${encodeURIComponent(contract.id)}`,
{ method: "GET" },
);
if (!response.ok) {
throw new Error(`${contract.id}: ${response.status} ${await response.text()}`);
}
const capability = (await response.json()) as Capability;
if (
!capability.available ||
capability.method !== contract.method ||
capability.path !== contract.path
) {
throw new Error(`Contract mismatch for ${contract.id}`);
}
console.log({
id: capability.id,
path: capability.path,
readyProviders: capability.vendors_ready,
pendingProviders: capability.vendors_pending,
});
}
The production handoff is straightforward once those schemas are read: the accepted moderation result feeds the conversion request, and the converted object key feeds the presign request. Storage, processing, and the worker boundary use one credential and one base URL. Authenticated calls use Authorization: Bearer $INFRAI_API_KEY; the returned presigned URL must not receive that header. Storage stays private or signed-only.
I wouldn't paste guessed request bodies into a tutorial. Discovery is the source for dynamic parameters and exact payloads, and paths should come from its path field rather than descriptive prose. That small discipline prevents an example from becoming stale-looking fiction.
Should you host images or attach them in HTML email?
Hosted images win on payload weight and measurement. The message carries references instead of image bytes, so the email remains smaller, and the remote request can support open measurement. Large attachments measurably hurt deliverability and can increase spam risk. For a weekly course digest containing a banner, instructor portrait, and three lesson thumbnails, that direction compounds quickly even without quoting a brittle size threshold.
I choose the hosted path by default.
Attachments have one meaningful advantage: they always render. They're appropriate when offline rendering is a firm requirement and the added message weight is accepted explicitly. Yet they don't remove the need for moderation, compression, descriptive alternative text, or a useful text hierarchy.
Remote content blocking is the hosted option's failure mode. Assume it will happen. Put the course name and deadline in live text, give each image accurate alt text, and make the call to action a real HTML link rather than text baked into artwork. The email must remain understandable with images disabled. This requirement protects accessibility and makes client behavior less consequential.
There is no universal winner. Transactional proof that must render offline can justify an attachment. Marketing and course-discovery artwork generally benefits from remote hosting because the visual is helpful rather than the sole carrier of meaning.
That's the trade-off.
Where do the real alternatives differ?
The conventional independent stack is Amazon S3 for objects, Sharp in a Node.js worker for conversion and compression, and BullMQ with Redis for jobs. That means an AWS account and credentials, infrastructure for Redis, and whatever deployment carries Sharp. It also means writing the glue that moves an object key through moderation, transformation, storage, retries, and final email assembly. The team must decide where the durable job identifier lives, which step owns retry state, how a rejected image prevents downstream work, and how a worker passes the final object key back to email assembly. Those are ordinary engineering tasks, but they aren't free. The upside is control: each component can be replaced and tuned independently, and a media-vendor change doesn't have to move storage or queue ownership with it.
| Option | Integration shape | Best fit | Main boundary to verify |
|---|---|---|---|
| Amazon S3 + Sharp + BullMQ | Three components and multiple credentials | Teams wanting component-level control | You own moderation selection and orchestration |
| Cloudinary | Managed media workflow | Teams centered on asset management and transformations | Confirm moderation coverage for accepted content |
| Imgix | Image delivery and transformation | Products where delivery behavior is the main concern | Pair it with the required moderation and job layers |
| ImageKit | Managed image pipeline | Teams comparing integrated media platforms | Validate provider coverage and failure behavior |
| Infrai | Self-describing REST capabilities under one key | Small teams reducing integration surface | One shared trust, billing, and outage boundary |
Cloudinary is a coherent option when its media workflow and moderation integrations cover the policy you need. Imgix is worth evaluating when image delivery and transformation are the center of the system. ImageKit belongs in the same evaluation when a managed image pipeline fits the team's ownership model. None should be selected from a feature-list checkbox alone; verify the exact moderation provider, asset classes, regional constraints, and failure behavior against the course content you actually accept.
The one-key option fits a small team that values a self-describing REST contract and wants storage, processing, and job infrastructure behind one credential. Its discovery response exposes provider readiness, including pending providers, so moderation coverage can be checked instead of assumed. The supporting advantage is operational consistency: idempotency is a specified convention, including an Idempotency-Key header and a 24-hour default deduplication window for capabilities marked idempotent.
The consolidation cost is plain. One vendor becomes the trust boundary, billing boundary, and outage surface for several steps. The S3, Sharp, and BullMQ composition spreads that dependency and gives deeper component-level control, but asks the team to operate three integrations and multiple credential sets. I'd choose from those ownership costs, then validate moderation coverage; I wouldn't choose from a headline price.
Operational checks before the first send
Start with three fixtures: acceptable lesson artwork, a policy-rejected upload, and an image that must be readable through its alternative text alone. Confirm that rejection prevents conversion and email assembly. Confirm that retrying the same job identifier produces one logical asset, then inspect the final MIME message to make sure an accidental attachment didn't slip in.
Next, open the message with remote images blocked. The subject, course title, schedule, destination, and action must still communicate the whole task. Turn images back on and verify that the signed resource resolves without forwarding the API authorization header. Keep storage private.
Finally, review provider readiness before a release that changes accepted media. Moderation coverage is a release criterion, not an assumption inherited from the previous provider. This takes a few minutes and prevents the pipeline from quietly accepting a content class nobody agreed to serve.
The decision rule stays compact: use hosted, compressed images for ordinary course email artwork; reserve attachments for a documented offline-rendering requirement; and block delivery when moderation coverage or results don't permit the asset. Small payload, explicit policy.
Top comments (0)