Crux
CookbookBasics

Safe image generation

Generate, guard, persist, and deliver an image while keeping model, Safety, and storage failures separate.

This recipe produces one image, evaluates canonical output policy, stores the approved asset, and returns an application identifier rather than raw media or a bearer reference.

1. Bind the adapter and store

import OpenAI from "openai";
import { createOpenAI } from "@use-crux/openai";
import { inMemoryAssetStore } from "@use-crux/core/storage";

const openai = createOpenAI(new OpenAI({ apiKey: process.env.OPENAI_API_KEY }));
const assets = inMemoryAssetStore();

Use a durable AssetStore adapter in production. The in-memory store keeps the example focused on lifecycle boundaries.

2. Define output policy

import { boundary, guardrail } from "@use-crux/core/safety";

const imageTypes = guardrail({
  id: "generated-image-types",
  on: boundary.output.media(),
  run: ({ subject }) =>
    subject.part.mediaType === "image/png"
      ? { action: "allow" }
      : { action: "block", reason: "Only PNG images may be published." },
});

For semantic classification, replace the local MIME check with an explicit media classifier guardrail.

3. Generate and persist

async function createIllustration(ownerId: string, prompt: string) {
  const result = await openai.generateImage({
    model: "gpt-image-2",
    prompt,
    n: 1,
    extra: { output_format: "png" },
    guardrails: [imageTypes],
    timeout: {
      totalMs: 60_000,
      stepMs: 45_000,
    },
  });

  // This is a storage retry boundary, not a model retry boundary.
  const stored = await assets.put(result.image);
  const imageId = crypto.randomUUID();
  await saveImageRef({
    imageId,
    ownerId,
    ref: stored.ref,
  });

  return {
    imageId,
    mediaType: stored.mediaType,
    traceId: result._meta.traceId,
  };
}

If generateImage() fails, apply your model-routing or product fallback. If assets.put() fails, retry the same asset rather than generating another image.

saveImageRef() is application code that persists the owner, application ID, and store-owned ref in server-only metadata. The returned imageId is safe to place in an application route; it does not grant access to the asset store.

4. Deliver through your application

The store-owned ref remains server-side. Resolve the application ID behind your authorization boundary:

async function GET(
  _request: Request,
  context: { params: Promise<{ imageId: string }> },
) {
  const { imageId } = await context.params;
  const ref = await lookupAuthorizedImageRef(imageId);
  const asset = await assets.get(ref);

  if (asset.type !== "data") {
    return new Response("Stored image is not locally materialized.", {
      status: 409,
    });
  }

  return new Response(new Blob([asset.data], { type: asset.mediaType }), {
    headers: {
      "content-type": asset.mediaType,
      "cache-control": "private, max-age=300",
    },
  });
}

lookupAuthorizedImageRef() authenticates the caller, verifies ownership, and maps the application identifier to the store-owned ref.

5. Inspect without retaining the image

Catalog shows the authored image operation and its output policy. Runs shows the logical operation, physical attempts, route, MIME type, image count, duration, terminal state, and Safety outcome.

Neither view stores the prompt, image bytes, data URL, bearer ref, provider file ID, filename, hash, thumbnail, or delivery URL.

Add progressive previews

Replace generateImage() with streamImage() only if intermediate images improve the experience. Complete previews replace earlier previews for the same output; incomplete deltas require provider-aware accumulation.

Read Streaming generated media before exposing progress, especially when output-media enforcement holds provisional bytes.

On this page