Developer guide
Four calls from upload to <img>.
@assetlake/core is framework neutral. The server entry holds the write token; @assetlake/core/url and @assetlake/core/contracts are safe to ship to browsers.
Public images only. A Sanity image asset is readable by anyone who has its URL, so AssetLake is for avatars, covers, and other images meant to be seen.
1. Create the server client
Server only. The write token never gets a NEXT_PUBLIC_ prefix and never reaches a browser.
import { createAssetLake } from "@assetlake/core";
export const assetLake = createAssetLake({
projectId: "oshzwvjy",
dataset: process.env.SANITY_DATASET!,
apiVersion: "2026-10-04",
token: process.env.SANITY_WRITE_TOKEN!, // server only
});2. Upload from your route handler
Core checks the declared type, size, and magic bytes against the policy before anything reaches Sanity. A repeated Idempotency-Key returns the existing record.
const image = await assetLake.images.upload({
body: new Uint8Array(await file.arrayBuffer()),
filename: file.name,
contentType: file.type,
applicationId: "assetlake-application-campus-demo",
purpose: "avatar",
entity: { type: "user", id: session.userId },
actorId: session.userId,
idempotencyKey: request.headers.get("Idempotency-Key") ?? undefined,
});3. Resolve preset URLs on the server
Presets live in Sanity, so an edited preset applies without a redeploy (cached for 60 seconds). responsive() uses the stored hotspot and returns a bounded srcSet of 256, 512, and 768w.
const avatarUrl = await assetLake.images.url(image.id, { preset: "avatar" });
const card = await assetLake.images.responsive(image.id, { preset: "card" });
// card: { src, srcSet, sizes, width, height, lqip }4. Render a plain <img>
No image optimizer in between: the browser requests cdn.sanity.io directly. Show the LQIP while it loads, if Sanity returned one.
<img
src={card.src}
srcSet={card.srcSet}
sizes={card.sizes}
width={card.width ?? undefined}
height={card.height ?? undefined}
alt="Project cover"
style={card.lqip ? { backgroundImage: `url("${card.lqip}")` } : undefined}
/>Building URLs in the browser
@assetlake/core/url is browser safe: it needs only the public project id and dataset. Prefer server-resolved URLs for stored records so hotspots apply.
import { createImageUrls } from "@assetlake/core/url";
const urls = createImageUrls({ projectId: "oshzwvjy", dataset: "production" });
const src = urls.buildUrl(image.assetId, {
width: 256,
height: 256,
fit: "crop",
autoFormat: true,
});HTTP contract
The demo backend's upload endpoint. Every response uses the same envelope.
multipart/form-data: file, purpose ("avatar"), alt (optional)
Idempotency-Key: <uuid> optional, safe retries
201 { "success": true, "data": { id, assetId, url, mimeType, size, width, height, aspectRatio, lqip, blurHash, status } }
4xx { "success": false, "error": { "code": "FILE_TOO_LARGE", "message": "..." } }
401 UNAUTHENTICATED · 400 UNSUPPORTED_IMAGE_TYPE · 413 FILE_TOO_LARGE · 429 RATE_LIMITED