A worked example of CalmLens in an image-generation app: private uploads, prompt checks before tokens are spent, cached decisions and signed webhooks.
·7 min read
Integration Showcase
Image Safety
Text Moderation
Webhooks
PolySnap is a mobile app that generates new images and videos from a person's photos and a prompt. Its backend runs on Cloudflare Workers and Durable Objects, and it uses CalmLens at three points: when a source photo is uploaded, before a generation starts, and when a moderation decision arrives later by webhook.
PolySnap's editor. CalmLens checks the text typed in the prompt field before generation; the preset looks above it are not sent.
The calls involved are short. Most of the design work is in where they sit, and in what PolySnap does with each answer.
One ID for each source photo
The app streams each photo to PolySnap's upload route as a raw request body. The route authenticates the user, checks that they own the asset, refuses images of 10 MB or more, and passes the body stream straight to CalmLens:
Using PolySnap's asset ID as the externalId gives both systems the same key. Retries rely on it: if CalmLens answers with asset/duplicate-external-id for that ID, the route treats the upload as already complete. Other failures are sorted by error code. Network faults, timeouts and generic misc/* errors leave the asset uploading so the app can try again, while a specific rejection marks the upload as failed.
The metadata comes from one shared helper, which also adds the user's email. The prompt check below uses the same helper, so a photo and a prompt from the same layer carry the same project, layer and user IDs.
keepContentAfterProcessing: true keeps the bytes so PolySnap can read them later; without it, CalmLens purges source content once processing finishes. The hide policy means flagged content is kept for review but never delivered.
Reading private content through a lens
PolySnap's backend needs the source photo again for image analysis, for the generation request and to show it back to its owner through PolySnap's own authenticated routes. The asset stays private, so none of these use a public URL. The backend reads it with its API key through a lens that scales the image down to PolySnap's largest generation size and returns PNG:
The resize rules live in one versioned definition rather than in every caller. PolySnap runs ensureLenses when it creates the client and records in a replicated cache that the current lens version is in place. Concurrent callers share one in-flight promise per project and lens version, and a failed promise is dropped so the next request can retry. Served responses go into the edge cache. Generated images are hosted separately; only uploaded source photos take this path.
Checking the prompt before tokens are spent
PolySnap's generation processor runs in a Durable Object. Before it marks a job as generating, and before it takes the user's tokens (when token charging is on), it sends the text the user typed or saved to a PromptVerifier Durable Object. The verifier normalises the text (Unicode NFKC, collapsed whitespace, trimmed) and, if anything is left, waits for a decision:
A rejection becomes a Text rejected error on the layer, and no generated layer is created. The check runs before PolySnap waits for pending photo uploads, so a rejected prompt fails the job straight away even while photos are still arriving. Input preparation can start alongside the check, but no model call begins until the prompt has passed and the job has been claimed.
A text asset in the CalmLens portal, with its score in each moderation category.
Caching the outcome
The verifier hashes the normalised text with SHA-256 and uses the hash in two ways. It names the Durable Object instance, so concurrent checks of the same text queue behind one another (blockConcurrencyWhile) instead of each starting an analysis. It is also the cache key. An outcome is looked up in the instance's memory, then the edge cache, then a replicated application cache, and written back to whichever missed. The shared caches use a versioned key and a seven-day expiry.
Only approved or rejected is stored, never the text or the report, and the same wording reuses its decision on any layer. While a result is cached, CalmLens isn't asked about that text again.
Reconciling later decisions with webhooks
Photo uploads use assets.create, which queues classification, so an image's decision usually arrives after the upload has succeeded. PolySnap's webhook route reads the raw body, verifies the signature before parsing anything, and responds with an error when no signing secret is configured, so an unsigned request is never accepted. With the current SDK, verification looks like this:
The route finds the PolySnap asset by the event's externalId. A rejected or error status marks the upload as failed, which fails any queued generation that depends on it, and the route records when it last synced. Events with no external ID, or one PolySnap doesn't recognise, are acknowledged with a 2xx and logged, so CalmLens doesn't keep retrying them.
Testing against the mock client
In its test environment, PolySnap creates MockCalmLensClient from @calmlens/sdk/testing in place of the real client. The code that calls it is the same. PolySnap supplies its own mock storage, keeping asset records in KV and image bytes in R2, and sets mockOptions.analyzedStatus to "rejected" for the rejection tests.
Those tests cover concurrent checks of the same text starting only one analysis, reuse through the edge and replicated caches, blank text approved without a CalmLens call, and rejected prompts with both pending and completed uploads failing the layer without creating a generated layer. Upload tests check the four metadata fields on the stored CalmLens asset. They test PolySnap's handling of decisions, not how accurate the moderation is.
Where the integration stops
The prompt check covers only text the user typed or saved. PolySnap's built-in prompt presets and looks aren't sent to CalmLens.
The verifier treats any returned status other than rejected as approval, including error. A thrown error still fails the job, but an error status needs its own handling before the prompt path can be called approval-only.
Cached outcomes can outlive a change to the project's moderation policy. Until the seven-day expiry or a cache version change, text that was approved under the old thresholds stays approved.
Generation waits for photo uploads to finish, not for their moderation decisions. CalmLens never serves a rejected asset, so a photo that has already been rejected can't be read through the lens. A photo still pending is served by default; projects that need a decision first can turn on Block assets until moderation finishes.
The webhook route has no test of its own in PolySnap's suite.
The JavaScript SDK guide covers each of these calls, and Webhooks covers delivery, retries and signature verification.
We use essential cookies to keep you signed in and the site secure. With your permission, we'll also use functional cookies to remember preferences such as your layout and documentation language, and to offer Google One Tap sign-in. We don't use analytics or advertising cookies. Manage cookies