Skip to main content

WASM in the Browser: Deploying VERT on CloudFront for Free

Table of Contents
WebAssembly lets CPU-heavy work run entirely inside the browser tab — no server, no uploads, no cost. This post covers what WASM actually is, and how to self-host VERT, a fully local file converter, on AWS CloudFront for effectively zero dollars.

What is WebAssembly?
#

WebAssembly (WASM) is a binary instruction format that runs inside the browser at near-native speed. Think of it as a portable compilation target: you write code in Rust, C, C++, or Go, compile it to .wasm, and the browser executes it directly in a sandboxed VM alongside JavaScript.

The browser has always been able to run computation, but JavaScript is an interpreted, dynamically typed language. It is fast enough for most tasks, but not for video encoding, image processing, cryptography, or physics simulations. Before WASM, these workloads had to live on a server. Now they can live in the browser tab.

How it Works
#

WASM is not a language. It is a compilation target with four core types (i32, i64, f32, f64) and a stack-based virtual machine. When you load a .wasm module, the JS engine compiles it to native machine code via JIT — starting from a much lower-level representation that maps directly to hardware.

sequenceDiagram
    participant Browser
    participant WASM VM
    participant JS

    Browser->>WASM VM: Load .wasm module (fetch)
    WASM VM->>WASM VM: JIT compile to native code
    JS->>WASM VM: instantiate + share Memory buffer
    JS->>WASM VM: call exported function(inputPtr, len)
    WASM VM->>WASM VM: process bytes in linear memory
    WASM VM-->>JS: return outputPtr
    JS-->>Browser: read result from shared buffer

The memory model is explicit: WASM gets a linear WebAssembly.Memory buffer, and JS and WASM communicate by reading and writing into it. No garbage collection crossing boundaries, no serialization overhead.

wasm-loader.js
// Load and instantiate a WASM module from JavaScript
const { instance } = await WebAssembly.instantiateStreaming(
  fetch('/converter.wasm'),
  { env: { memory: new WebAssembly.Memory({ initial: 256 }) } }
);

// Call exported WASM functions directly via shared memory
const result = instance.exports.convert_image(inputPtr, inputLen, outputPtr);

The Practical Upside
#

The most important consequence is zero server round-trips for CPU-bound work. If you are converting an image, the file never leaves the browser. No upload latency, no server costs, no privacy concerns. The user’s CPU does the work.

Tip

This changes the economics of entire categories of tools. A workload that previously required a fleet of servers now requires only static file hosting.


VERT: 250+ Format Conversions, Fully Local
#

VERT is an open-source file converter built with SvelteKit and TypeScript. Under the hood it shells out to compiled WASM modules (FFmpeg, ImageMagick, and others) to convert images, audio, and documents entirely in the browser.

FeatureDetails
File size limitsNone — constrained by your RAM, not a server quota
PrivacyFiles never leave the browser
Formats supported250+ across images, audio, documents
Video conversionRequires self-hosted companion daemon
HostingPre-built static site, self-hostable

Hosting on CloudFront + S3 at Zero Cost
#

VERT’s build output is a standard static site: HTML, JS, CSS, and .wasm files. The WASM binaries are large (FFmpeg compiled to WASM weighs several megabytes), so the right hosting choice is a CDN that serves them from an edge cache close to the user.

CloudFront has a permanent free tier: 1 TB of data transfer per month and 10 million HTTP/S requests per month — no expiry.

flowchart LR
    User([User Browser]) -->|HTTPS| CF[CloudFront Edge]
    CF -->|Cache miss only| S3[(S3 Bucket\nprivate)]
    CF -.->|Cached .wasm, .js, .css| User

    subgraph AWS
        CF
        S3
        OAC[Origin Access Control]
    end

    OAC -- SigV4 signing --> S3
    CF -- uses --> OAC
Important

Do not enable static website hosting on the S3 bucket. Serving through CloudFront with an Origin Access Control (OAC) is both more secure and cheaper — the bucket stays fully private.

Step 1: Build VERT
#

terminal
git clone https://github.com/VERT-sh/VERT.git
cd VERT
npm install
npm run build

The output lands in build/. It is a fully self-contained static site.

Step 2: Create an S3 Bucket
#

terminal
aws s3 mb s3://my-vert-instance --region eu-central-1

# Block all public access — CloudFront is the only entry point
aws s3api put-public-access-block \
  --bucket my-vert-instance \
  --public-access-block-configuration \
  "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

Step 3: Upload the Build
#

terminal
# Hashed assets: cache forever
aws s3 sync build/ s3://my-vert-instance/ \
  --delete \
  --cache-control "public, max-age=31536000, immutable"

# Entry points: always revalidate
aws s3 cp build/index.html s3://my-vert-instance/index.html \
  --cache-control "public, max-age=0, must-revalidate"

The immutable directive tells CloudFront and the browser that hashed assets like _app/immutable/chunks/index.abc123.js will never change for a given URL — maximizing cache hit rate.

Step 4: CloudFront Distribution
#

Create an OAC so CloudFront can read from the private S3 bucket:

terminal
aws cloudfront create-origin-access-control \
  --origin-access-control-config '{
    "Name": "vert-oac",
    "OriginAccessControlOriginType": "s3",
    "SigningBehavior": "always",
    "SigningProtocol": "sigv4"
  }'

Then create the distribution, using the AWS-managed CachingOptimized cache policy:

distribution-config.json
{
  "Origins": {
    "Items": [{
      "Id": "s3-vert",
      "DomainName": "my-vert-instance.s3.eu-central-1.amazonaws.com",
      "S3OriginConfig": { "OriginAccessIdentity": "" },
      "OriginAccessControlId": "<OAC_ID>"
    }]
  },
  "DefaultCacheBehavior": {
    "ViewerProtocolPolicy": "redirect-to-https",
    "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
    "Compress": true
  },
  "CustomErrorResponses": {
    "Items": [{
      "ErrorCode": 403,
      "ResponsePagePath": "/index.html",
      "ResponseCode": "200"
    }]
  },
  "DefaultRootObject": "index.html"
}
Note

The CustomErrorResponses block is critical. SvelteKit uses client-side routing, so any path CloudFront does not find in S3 returns a 403. Mapping that 403 to index.html with a 200 response lets the SvelteKit router take over.

Finally, allow the OAC to read from S3:

bucket-policy.json
{
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "cloudfront.amazonaws.com" },
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::my-vert-instance/*",
    "Condition": {
      "StringEquals": {
        "AWS:SourceArn": "arn:aws:cloudfront::<ACCOUNT_ID>:distribution/<DIST_ID>"
      }
    }
  }]
}

Cost Breakdown
#

ResourceFree tierCost after free tier
S3 storage (~300 MB)5 GB/month (12 months)~$0.007/month
CloudFront transfer1 TB/month (permanent)$0
CloudFront requests10M/month (permanent)$0
ACM certificateFree$0

For personal use, this is effectively free indefinitely. After the S3 free tier expires, you are looking at less than a cent a month in storage.


Why This Pattern Works
#

The reason the cost is zero is the same reason WASM makes VERT possible in the first place: all computation is pushed to the client. The server (CloudFront + S3) only needs to serve static bytes once. After the first load, the browser caches the WASM modules aggressively. Subsequent visits are served from the local disk cache.

A traditional file converter would need a fleet of worker instances processing uploads 24/7. You would pay for compute, storage, egress, and queue infrastructure. With WASM, your infrastructure cost collapses to “store and deliver static files” — which AWS essentially gives away at personal-use scale.

Tip

WASM is not just a performance trick. It changes the infrastructure model entirely. Any CPU-bound tool that previously required server compute can be re-architected as a static site with zero ongoing costs.

Related