Build Orshot
KINDAreplaces $39/mosaves $468/yrback to the verdict
A template-driven render service whose hard problems are producing the same pixels on every machine, keeping a browser pool warm and bounded under burst, and never letting a caller's data turn into code the browser runs.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the service and the browser driver.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree
Why The renderer. Installing the browser without its system libraries is the most common cause of a service that starts cleanly and renders nothing.
Get it npm i -D playwright then npx playwright install --with-deps chromium. open ↗
Verify
A scripted screenshot of a local page produces a non-empty PNG inside the container - have readyfree with open licences
Why Bundling a font you have no licence to redistribute is a legal problem that arrives long after the code works.
Get it Open-licensed families, or the files your brand licence permits you to embed in a server.
Verify
A written note records each family, its licence and where the file came from - have readypennies per month
Why The cache and the batch output both live on disk and both grow without a ceiling unless you set one.
Get it A volume with a known size, separate from the system disk.
Verify
The cache ceiling is set below the volume size and eviction has been observed - decidefree
Why Each of these is asserted by a test, and choosing them after seeing failures means choosing whatever passes.
Get it Write down which asset hosts are permitted, how long a render may take, and how much disk the cache may use.
Verify
All three appear in the configuration and are named in tests
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- templates(name, version, width, height, default_format, schema_json, font_set, updated_at) · version is part of every cache key - renders(id, template, template_version, payload_hash, format, scale, bytes, duration_ms, cache_hit, api_key_id, created_at) - assets(pattern, kind, sha256, allowed) · every remote image a template may load, checked before a page is opened - fonts(family, weight, style, file, sha256) · hashed into the cache key so replacing a font invalidates only what used it - api_keys(id, label, hashed_key, rate_per_minute, monthly_render_cap, created_at, revoked_at) - failures(id, template, payload_hash, stage, message, console_excerpt, at) · the page's console output, because most failures are a missing asset nobody saw - The cache key is the hash of template version, payload and font set together, so an identical request never opens a browser and editing one template invalidates nothing else.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
PORT | required | 8080 | The HTTP API. Bind to localhost unless a proxy terminates TLS in front. |
TEMPLATES_DIR | required | ./templates | Server-side templates. The only markup the browser ever renders. |
FONTS_DIR | required | ./fonts | Bundled faces. Hashed into the cache key. |
CACHE_DIR | required | ./.cache | Rendered output by content hash, evicted least recently used. |
CACHE_MAX_BYTES | required | 5368709120 | The disk ceiling. Eviction is tested against it, not hoped for. |
RENDER_TIMEOUT_MS | required | 15000 | After this the page is killed, not navigated away from. |
POOL_SIZE | required | 4 | Warm contexts. Beyond this requests queue and then are refused. |
ASSET_ALLOWLIST | required | cdn.example.com,images.example.org | Hostnames a template may load from. Resolved addresses are checked separately. |
The build, in order
One template that renders the same everywhere
The same payload produces the same pixels on your laptop and in a container.
The template states what it needs so the service can reject a payload before opening a browser.
Files
templates/og-card/meta.jsonsrc/templates.tsLoad fires long before the page looks like the design.
Files
src/render.tsA rendering regression is invisible in a diff and obvious in a picture.
Files
tests/golden.test.tstests/golden/og-card.png
done when · tick each as it passeswatch out- Screenshotting as soon as the page loads captures text in the fallback font and images half decoded, and the result looks correct often enough that the bug ships; the fallback then differs between your machine and the container, so the golden test passes locally and the live output is wrong. Wait on fonts ready and image decoding, bundle every font, and assert the computed font family in the test rather than trusting it.
Data in, never markup
A hostile payload cannot make the browser fetch or run anything you did not choose.
Unknown keys are rejected, not ignored, so a typo is an error rather than a blank.
Files
src/validate.tstests/schema.test.tsRaw insertion should not be reachable from a request at all.
Files
src/render.tsThis is the difference between an allowlist and the appearance of one.
Files
src/assets.tstests/ssrf.test.ts
done when · tick each as it passeswatch out- Checking a remote image address against an allowlist and then handing the same address to the browser lets a name that passed the check resolve to 169.254.169.254 on the browser's own lookup, and your cloud credentials arrive inside a PNG. Resolve the name yourself, check the resulting address, and pass the address rather than the name, or route every fetch through a proxy that re-checks.
PDFs that look like the design
The invoice on screen and the invoice in the file are the same document.
Print and screen are different documents and the template should say which it is.
Files
templates/invoice/meta.jsonsrc/pdf.tsThe default is tuned for ink, and your design is not.
Files
templates/invoice/style.cssComparing bytes tells you nothing; comparing pixels tells you everything.
Files
tests/golden-pdf.test.ts
done when · tick each as it passeswatch out- Designing a template in a browser tab, then generating the PDF with the defaults, gives a document with backgrounds dropped and colours shifted, because the print path applies print media and adjusts colours for ink. The template looked right in every preview, so the bug is found by a customer holding a beige invoice. Choose the media explicitly, force the colour adjustment, and make the golden test render the PDF path rather than only the screenshot path.
A browser pool that survives Monday
Burst traffic queues instead of exhausting memory, and a hung page never holds a worker.
Reuse the expensive part, discard the part that holds state.
Files
src/pool.tsAccepting work you cannot finish turns a slow minute into an outage.
Files
src/queue.tstests/load.test.tsMeasure memory over a thousand renders and set the recycle point from the measurement.
Files
src/pool.tstests/timeout.test.ts
done when · tick each as it passeswatch out- A template that waits on an asset which never arrives leaves the page open forever, and after a handful of those the pool has no workers left while the process looks healthy and memory looks fine. Time out the render, kill the page rather than navigating away, and count consecutive timeouts as a reason to recycle the browser.
Caching, keys and a bill that cannot run awayproduct builder
Identical work is done once, every caller is identified, and a loop cannot cost you a month of compute.
One key covering everything that changes the picture.
Files
src/cache.tstests/cache-key.test.tsA caller in a retry loop is the most likely source of a large bill.
Files
src/keys.tsWithout it you cannot tell a slow template from a cold cache.
Files
src/log.ts
done when · tick each as it passeswatch out- Caching on the payload alone means editing a template or swapping a font silently serves last week's picture, and the person who made the edit assumes it did not deploy. Put the template version and a hash of the font set in the key, and invalidate by writing a new key rather than by clearing the cache.
That is the whole plan for Orshot. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A visual template editor.
- Video rendering.
- Social publishing.
- Caller-supplied HTML.
- visual studio editor for non developers
- 2,000 plus prebuilt templates
- one design auto resized to every social format
- scheduled and webhook driven workflows
- publishing to social platforms
- MCP and agent integrations
- white label embedded editor
- workspaces, audit logs, and team access
- Signed render URLs so a template and payload can be embedded in a page without exposing a key
- A second output size derived from the same template, once the golden tests cover the first
- A preview endpoint that renders at low scale for editing, sharing the cache with the full-size path
Need the files? The project pack on the verdict page hands your agent the whole brief · more automation.