Guides
Where does my screenshot go, and is it watermarked?
Your screenshot stays inside the browser. No server route accepts a file and no mark is drawn on the export. The cost is that a refresh drops the image.
Updated 27 September 2026
Handing an unreleased screen to a mockup tool you have never used before is an uncomfortable moment. It is fair to want to know which server holds on to that file, and whether the image you download at the end will carry someone else's logo in a corner.
Here is the answer up front. A screenshot you put into the editor never leaves the browser. Nothing is drawn onto the file you download either. There is one cost that comes with it: refresh the page and the screenshot you added is gone, so you have to add it again.
Take my word for it is not much of an answer, so the rest of this page opens up where that answer comes from. Every statement here was checked against this service's own implementation code. The evidence table at the bottom names the file and the symbol behind each sentence.
There is no address to receive your file
Start on the server side. Not one of the server routes under src/app/api reads a multipart/form-data body, the format a browser uses to upload a file, and not one of them calls formData() on the request object.
The routes that are open without a login read and count. Here is what each one does.
The column to read is the one on the right. All you need to check is whether a single row receives an image a visitor added.
| Public route | What it does | Receives an image |
|---|---|---|
| /api/ads | Returns the ad slots set to visible | No |
| /api/ads/click | Counts ad clicks | No |
| /api/ads/impression | Counts ad views | No |
| /api/business | Returns the privacy officer and contact address shown on the legal pages | No |
| /api/showcase/status | Returns whether the showcase is shown | No |
| /api/ads/image/[slot] | Serves a banner the operator registered | Sends only |
| /api/showcase/image/[side] | Serves an example image the operator registered | Sends only |
| /api/track | Counts visits by path | No |
It is not a policy, there is no way to send it
The screen side is the same. Searching the whole editor feature folder for fetch, XMLHttpRequest, and navigator.sendBeacon returns 0 results. Nowhere in the flow that reads a file, places it, and exports it is there a point that talks to a server. Before any policy about not sending files, there is no code that could send one.
The ad cards on the right of the editing screen are a separate part outside the editor feature folder that only fetches the ad list and images and counts views and clicks, and it never sends your screenshot.
Files come in through three entry points: the file picker, drag and drop, and paste. All three meet in the same function and are decoded inside the browser.
So where is the screenshot you added?
The decoded pixels go into an in memory store called imageRegistry. It is a single JavaScript Map holding ImageBitmap objects, and it is kept apart from the layout document. The document knows which image identifier is attached to which device, and nothing about the pixels themselves.
The table below comes down to one thing for you. Finish editing and leave the screen, and the pixels are released with no copy kept anywhere else.
| Operation | What the code does |
|---|---|
| add(id, bitmap) | If a bitmap already exists under that identifier, the old one is closed and replaced |
| retain(id) / release(id) | Counts references, and at 0 closes the bitmap and deletes it from the Map |
| clear() | Closes every remaining bitmap when the canvas leaves the screen |
References are counted so the moment of release is not left to chance. An ImageBitmap holds a graphics resource, so a reference left behind and forgotten makes the tab grow. That is why the close is explicit in two places: when the count reaches 0, and when the canvas goes away.
What survives a refresh, and what does not
The layout survives, and that is all. Every change to the document is written once to sessionStorage. Immediately before writing, a copy is made with the image identifier on every device node set to null, and only that copy is serialized. What gets stored is which format you picked, where each device sits and at what size, and which background and finishing are in use.
The same handling runs again on restore. The image identifier on a loaded document is forced back to null, so a pixel reference cannot come back to life no matter how a value survived. A failed write does not throw. That keeps editing alive when storage is blocked, as in Safari private browsing or when the quota is full.
The choice of sessionStorage points the same way. It lives per tab, so closing the tab takes the layout with it. If an edit is going to run long, downloading the intermediate result is the safer move.
What if another script gets onto the page?
Separately from there being no send point in the code, the page carries a content security policy. The connect-src directive, which names where a script may send data, holds a single value of its own origin, so even a foreign script that got in would have no channel to an arbitrary address.
The table is an instruction to the browser. For you it means that no script running on this page has a channel out to an address of its choosing.
| Directive | Value | What it means |
|---|---|---|
| connect-src | 'self' | A script can only talk to the same origin |
| img-src | 'self' data: blob: | Allows the in browser addresses used for compositing and export |
| object-src | 'none' | Blocks legacy plugin embedding |
| frame-ancestors | 'none' | Stops another site wrapping this page in a frame |
With ads switched on in production, the ad domain is added to connect-src. That is the channel the ad script uses, and as checked above the editor code makes no network calls at all.
What the visit log records
This site does look at visit numbers. It does not attach an outside analytics tool, though. It uses its own beacon. On every screen path change it sends a small JSON with three values, and the server always answers 204 with no body, whatever the outcome.
The table below is the whole of the visit record. It is worth checking whether any field in it corresponds to what you edited.
| Field | Contents | Length cap |
|---|---|---|
| path | The path visited | 200 characters |
| referrer | The origin address when the visit came from outside | 500 characters |
| vid | A random visitor string the browser generated | 64 characters |
The visitor string is a random value the browser generated and keeps in localStorage. No cookie is used, and a move within the same origin sends the referrer as an empty string. The server adds the values to a daily rollup. Page views, the set of unique visitors, referral type, referral host, and views per path are the whole of it, and the retention period is 180 days. Admin screen paths are left out of the rollup entirely.
There is no slot anywhere in that for an image or a file name. What the rollup holds is that someone opened the editor screen, and the server has no way to know what was edited there.
Will the file you download be watermarked?
It will not. The export composites the stage as drawn on screen and bakes it into a file. Only the editing tool layers are hidden at that point, so selection handles and alignment guides stay out of the result. There is no code that draws a logo or a mark over it. What lands in the composite is decided entirely by what is on screen.
The file name is assembled from mockup, the ratio identifier, the time it was made, and the extension. The original name of the file you added is not used.
In short
- There is nowhere for your file to land: none of the server routes reads a file upload body.
- The editor code has 0 network calls, so there is no means to send anything.
- Pixels live only in the in memory store, and the session keeps layout information only.
- The cost is that a refresh means adding your screenshot again.
- The visit rollup takes three things: path, referrer, and a random identifier.
- No code draws a mark onto the file you download.
Worth reading next
What this article is based on
| Statement | Where it comes from |
|---|---|
| No server route receives multipart/form-data | the route.ts files under src/app/api and the absence of any formData call |
| What the public routes do: ads, business details, showcase status, visit rollup | app/api/ads/route.ts, app/api/ads/click/route.ts, app/api/ads/impression/route.ts, app/api/business/route.ts, app/api/showcase/status/route.ts, app/api/track/route.ts |
| Only the admin routes receive an image, as base64 in a JSON body | the imageBase64 field and Buffer.from(input.imageBase64, 'base64') in app/api/admin/ads/route.ts |
| 0 hits for fetch, XMLHttpRequest, and sendBeacon in the editor folder | a search across all of src/features/editor |
| The ad cards on the editing screen make four kinds of request only: the ad list, ad images, view counts, and click counts | loadEditorAds (/api/ads), editorAdImageSrc (/api/ads/image), and trackEditorAdClick (/api/ads/click, with the slot number as the only body) in app/_components/ads/editorAds.ts, and sendAdImpression (/api/ads/impression, with the slot number as the only body) in app/_components/ads/adImpression.ts |
| 3 input paths, the file picker, drop, and paste, converge on one function | the onFileChange, onDrop, and paste listeners in panels/useImageInput.ts, into engine/ingestImage.ts |
| Pixels exist only in a Map based in memory store | ImageRegistry.map in state/imageRegistry.ts (ImageId → ImageBitmap) |
| Re-registering the same identifier closes the old bitmap, a reference count of 0 releases it, and unmount frees everything | add, release, and clear in state/imageRegistry.ts, imageRegistry.clear() in canvas/EditorStage.tsx |
| Only a copy with imageId set to null is serialized on save | persistLayout in state/persistLayout.ts (nodes.map with imageId: null) |
| Restore forces imageId back to null, and a failed save is ignored quietly | loadLayout in state/persistLayout.ts and the empty catch in persistLayout |
| Saved on every document change, restored once on mount | the persistLayout(doc) effect and the loadLayout() call in EditorRoot.tsx |
| The store is sessionStorage, scoped to one tab | the sessionStorage use in state/persistLayout.ts, editorConstants.PERSIST_KEY |
| connect-src is 'self', img-src is 'self' data: blob:, object-src is 'none', frame-ancestors is 'none' | the directives in lib/security/csp.ts |
| The ad domain is added to connect-src only when ads are active in production | the NEXT_PUBLIC_ADSENSE_PUB_ID branch in lib/security/csp.ts |
| The visit beacon accepts three fields, path, referrer, and vid, capped at 200, 500, and 64 characters | TrackSchema in app/api/track/route.ts |
| Always a 204 response, on success and on failure alike | noContent() in app/api/track/route.ts |
| The visitor identifier is a random UUID in localStorage, internal moves send an empty referrer, and /admin is excluded | getVisitorId, externalReferrer, and EXCLUDED_PREFIXES in app/_components/Tracker.tsx |
| The rollup fields and the 180 day retention | recordVisit in lib/analytics.ts (an:pv, an:vis, an:ref, an:refhost, an:path) and DAY_TTL_SEC |
| The export hides the editing tool layers only, and no watermark is drawn | layers.ui.hide() in engine/exportImage.ts and the absence of any watermark symbol |
| The file name is mockup-{ratio}-{time}.{extension} | buildFileName in engine/clipboard.ts |