Reference

Hyperscaler vs Distributed: Webflow, Xano and Cloudflare Decision Ladders

For: teams choosing where logic, data and models run when the front end is Webflow, the edge is Cloudflare and the system of record is Xano. Premise: you don't pick a hyperscaler or an edge network. You pick, for each decision, the lowest rung that can make it correctly. Companions: The AI ladder for escalation and mandates; Why Xano is the runtime as a service; Process Management Guide for the Webflow, Xano, Cloudflare and Shopify data layer; Render and Runtime dictionary for terms. Download: diagrams as image or PDF: challenge, solution, benefit (image, PDF) · the frame-budget origin (image, PDF) · bind before you allocate (image, PDF) · incremental knowledge and AI weighting (image, PDF) · directional streams (image, PDF) · the thread (image, PDF) · the two ladders (image, PDF) · the system-of-record gate (image, PDF) · media, 3D render and the GPU (image, PDF).

Challenge, solution, benefit

Task runners, data bindings and two kinds of GPU, in four rows. Task runner: jobs with no owner, spread across crons, webhooks and queue consumers, become one runner per path (a Worker cron and queues at the edge, Xano tasks for the record, idempotency keys), so every job has an owner, a retry policy and a log. Data binding: code that can reach any store becomes bindings declared in config (env.KV, env.DB, env.AI and one Xano endpoint), so the config is the access list. GPU as a place: scarce GPUs are rented as clusters from CoreWeave or Oracle OCI, running NVIDIA-only code such as CUDA and PTX, giving training at scale while the data leaves your boundary. GPU on the device: small GPU work sent away to a cluster becomes portable SPIR-V bytecode run through wgpu on any GPU, written in Rust or another shader language, so small GPU work stays with the data. Rows 3 and 4 split GPU work by size; they don't compete.

Open the SVG full size (1600 × 744) · Download the image · Download the PDF.

Read each row left to right. Rows 1 and 2 decide who may run and what it may touch. Rows 3 and 4 decide whether the data goes to the GPU, or the GPU code goes to the data. They split GPU work by size rather than competing for the same job: training and large models need a rented cluster, while previews, real-time 3D and upscaling can run on the GPU the person already has. Neither row is the goal. The goal is the gate described below, and both kinds of GPU sit under it.

Four definitions

TermWhat it isIn this stackWhere it sits on the spectrum
Task runnerThe thing that starts a job, retries it and records the result, as opposed to the code the job runs.Worker cron triggers and Queues for edge jobs; Xano background tasks for jobs that change the record. Each job carries an idempotency key, so a retry can't apply twice.Either end. Run it where the data the job changes lives.
Data bindingA named handle to a resource, declared in configuration and handed to the code at runtime. The code can only reach what is bound; there is no URL or credential to copy.Cloudflare bindings (env.KV, env.DB, env.R2, env.AI, a Durable Object namespace) in wrangler.toml; Xano endpoints reached through one bound service.Edge. The binding list is the security boundary.
GPU as a place (CoreWeave, Oracle OCI)GPUs you rent by the cluster. CoreWeave is a neocloud that sells little else; Oracle Cloud Infrastructure is a hyperscaler whose Superclusters link tens of thousands of NVIDIA GPUs over RDMA. Code for them is usually NVIDIA-only: CUDA, or PTX, which Rust can also target through Rust CUDA.Rung 4 of the AI ladder: training, fine-tuning, self-hosted large models.Centralized. The data travels to the GPU, so residency and egress are part of every job.
GPU on the device (portable GPU bytecode)GPU programs compiled to SPIR-V, a portable bytecode that wgpu translates for any Vulkan, Metal or DirectX GPU. Rust (through rust-gpu) is one source language; GLSL, HLSL and WGSL also compile to it.Kernels shipped with the application: in a native app, on your own server or, after translation to WGSL, in a browser through WebGPU.The device, at the far distributed end, past the edge. The data stays where it already is.

What portable GPU bytecode does and doesn't buy. SPIR-V is to a GPU roughly what WebAssembly is to a CPU: one compiled artifact, many targets. That makes small parallel work portable, such as image transforms, embeddings or physics, and lets it run on the user's own device, so the data never leaves. It doesn't make a phone into a training cluster, and three limits apply:

In business terms: SPIR-V is the .wasm of the GPU. The pipeline has the same shape as the Rust-to-Wasm path below: write in a source language, compile once to a portable file, and let each platform do the last step for its own hardware.

GPU layerWhat it isThe Rust-to-Wasm parallel
Rust (rust-gpu), GLSL, HLSL, WGSLThe source languages teams write inRust or TypeScript source
rust-gpu, glslang, DXC, nagaThe compilersTrunk and wasm-bindgen
SPIR-VThe portable program, owned by no single vendorThe .wasm file
wgpuThe runtime adapter. Each time the app runs, it finds the GPU, translates the program for it, and hands it work and dataThe Workers runtime, or a browser
Vulkan, Metal, DirectXThe platform APIs. Only Vulkan runs SPIR-V directly; for Metal and DirectX, naga translates firstThe browser engines: V8, WebKit
DriverVendor software on the operating system. It does the final compile into the chip's own instructionsThe engine's JIT compiler
FirmwareVendor software on the chip itself, such as NVIDIA's GSP firmware and VBIOS. It schedules work, manages power and enforces memory isolationCPU microcode and BIOS
GPUThe siliconThe CPU

The data doesn't travel inside the program. It moves separately, into buffers, and the result lands in a render target: the frame, or viewport, the person sees. The program is the recipe; the buffers are the ingredients.

Below every sandbox: firmware and drivers. The bottom two software layers belong to the vendor and run beneath anything you build. wgpu's validation and Rust's memory safety protect the layers above them; a flaw in a driver or in firmware can only be fixed by a vendor patch. The NVIDIA container escape, explained in the neocloud section below, was in the Container Toolkit, not the firmware, but the lesson is the same: the fix is the vendor's to ship, and your job is to know you need it.

The MPN: the part number that ties a GPU to its patches. An MPN (manufacturer part number) names the exact hardware model, the same way it names a product in a PIM. On a GPU it is the join key between the physical card and everything the vendor publishes about it: which driver and firmware branches support it, which security bulletins apply, and when it reaches end of life. Vendor bulletins list affected product families and software versions, so the MPN is how you find out which of them is yours.

Optimizing the bytecode: a frame for a person, or throughput for a model. The same compile step serves two very different customers, and each is optimized for a different finish line.

Browser render, for peopleAI work, for models
The goalEvery frame on time: about 16.7 ms at 60 Hz. A missed frame is a visible stutterThe most results per dollar and per watt: inferences, tokens or embeddings per second
What gets optimizedSize and startup. Shrink the Wasm (opt-level = "z", wasm-opt; the demo build in this article is 32 KB), compile while downloading, keep shaders small, and prepare pipelines before the first frameMath and memory. Lower precision (FP16, INT8), fused kernels that make one pass over the data instead of several, memory layouts that keep the GPU fed, and large batches
PrecisionAn eye can't see a one-bit color error, so 8 bits per channel is enoughModels often tolerate lower precision too, but it changes outputs, so every reduction is measured against a reference
Where the final compile happensOn the person's device, at load time, for whatever GPU is thereUsually ahead of time, for one known GPU: an NVIDIA TensorRT engine, for example, is built for a specific GPU model and software version
Portability vs peak speedPortable first: one Wasm or WGSL file for every devicePeak speed first: CUDA and per-GPU engines, which tie the work to one vendor
What "done" meansThe page looks right and feels smooth to a personThe output matches the reference within a set tolerance
The security riskUntrusted code from the web running on someone's device, so the browser validates and sandboxes itThe data. The optimized kernels are native and privileged, and a model reads what a person never sees, such as hidden SVG text

In business terms: a browser render is a print shop making one poster while the customer waits, so speed to the first copy and a small file matter most. AI work is a factory line producing a million units, so the cost per unit matters most, and the line is retooled for one specific machine.

Three rules follow for this stack:

The frame-budget origin

The GPU started out rationed. NVIDIA was founded in 1993 to make 3D graphics for games, and a game engine has a fixed budget, about 16.6 ms per frame at 60 frames per second, on whatever GPU the player happens to own. It can't buy more GPU, so it removes everything it doesn't need before it shades a single pixel. Data-center AI dropped that discipline, reserving GPUs by contract before demand was known. "Bind before you allocate" brings it back.

The frame-budget origin. Two paired pipelines. Game engine, on a 16.6 ms frame budget: frame budget, culling, level of detail, streaming and mipmaps, batching, the player's GPU, and a render farm only for cinematics and lightmaps. Bind before you allocate, on a GPU and token budget: GPU budget, the gate, rank then fall back, edge cache, queues, device first, and a rented GPU cluster only for what is left. Renderers: Flash Player in C++ is replaced by Ruffle in Rust compiled to Wasm; SVG renderers in C by librsvg in Rust and resvg; ImageMagick in C by Rust image decoders that can run as Wasm at the edge; a native renderer isolated in a cluster pod becomes a safe renderer moved to the device or edge. Compile once, render cheap: Rust source is optimized at build into a compact Wasm bundle, compiled while it downloads, and run without JIT warm-up or GC pauses, with WebGPU for the GPU part. Timeline: 1993 NVIDIA founded for 3D games; 1999 GeForce 256 sold as the first GPU; 2006 to 2007 CUDA; 2012 AlexNet trained on two gaming cards; late 2010s mining GPUs turn into GPU clouds; 2020s GPUs reserved by contract.

Open the SVG full size (1600 × 1186) · Download the image · Download the PDF.

Game engine, under a frame budgetBind before you allocate, under a GPU budget
Frame budget: 16.6 ms is fixed, and the work fits the budgetGPU and token budget: context assembly caps what reaches a model
Culling: off-screen or hidden objects are never drawnThe gate: ineligible or unconsented data is never sent
Level of detail: distant objects get simpler meshesRank, then fall back: a small model first, a larger token model only below the threshold
Streaming and mipmaps: reuse loaded textures at the size neededEdge cache: reuse answers and transforms, after the gate
Batching: group draw calls so the GPU isn't interruptedQueues and task runners: group jobs per subject and region
The player's GPU renders the frameDevice first: previews and real-time 3D stay on the person's device
Render farm: only cinematics and lightmaps that can't be real-timeRented GPU cluster: only what is left after the funnel

How the GPU left the player's desk. NVIDIA sold the GeForce 256 in 1999 as the first "GPU". CUDA (2006–07) opened the same parallel hardware to any workload. AlexNet, the 2012 network that started modern deep learning, was trained on two gaming cards. GPUs bought for crypto mining later became the stock of the first GPU clouds. Each step moved the GPU further from the person who owned it and closer to a balance sheet that has to borrow to buy it.

The renderers: C and C++ parsers, and what Rust fixes

The renderers that sit next to the GPU have their own history, and most of it was written in C and C++. Those languages leave memory management to the programmer, so a crafted file can overflow a buffer and run code. Microsoft and the Chromium project have each reported that about 70% of their serious security bugs are memory-safety bugs.

C or C++ rendererWhat went wrongRust replacementWhat changes
Flash Player (C++)A browser plugin that parsed untrusted SWF files, with years of exploits; Adobe ended it on 31 December 2020Ruffle, a Flash emulator in Rust compiled to WebAssemblyThe same SWF files play inside the browser's sandbox, with memory safety
SVG renderers in Clibrsvg began in C; a crafted SVG could reach memory bugs in the parserlibrsvg, rewritten in Rust, and resvgMemory-safety bugs are designed out of the parser and renderer
ImageMagick (C)Hundreds of format coders; ImageTragick in 2016Rust image decodersMemory-safe decoding that can run as Wasm inside a Worker

This is an allocation point, not only a security one. A C renderer that parses untrusted files has to be isolated: a locked-down pod in a cluster, far from the data and paid for separately. A memory-safe renderer can run on the device or at the edge, next to the data. Safety moves the render to the left of the spectrum, which is where "bind before you allocate" wants it. Rust removes memory-safety bugs; it doesn't remove logic bugs, and code marked unsafe still needs review. See SVG, Rust and Elm: the security gaps for the wider picture.

Compile once, render cheap: the Wasm bundle

A WebAssembly bundle moves work from every run to a single build:

  1. Optimized at build. A release build with link-time optimization, then wasm-opt, is paid for once, by the developer, not on every page load or request.
  2. A size budget, like a frame budget. The bundle is downloaded before it runs, so it gets a budget. Split it, and load what isn't needed on first render later.
  3. Streamed compile. Browsers compile WebAssembly while it is still downloading, and decoding it is faster than parsing the equivalent JavaScript.
  4. Predictable runtime. Rust compiled to Wasm has no garbage collector and no JIT warm-up, so a frame or a request costs the same the first time as the hundredth.

The limits: Wasm reaches the DOM and the GPU only through JavaScript bindings (WebGPU for the GPU, which is how wgpu runs in a browser), a large bundle can cost more to download than it saves at runtime, and Workers cap bundle size. Measure the bundle like a frame: if it doesn't fit, it isn't done.

Shopify Functions: Wasm as a checkout security boundary

Shopify already runs this model in production, inside cart and checkout. A Shopify Function is a WebAssembly module that Shopify runs for discount, delivery, payment and validation logic, in place of the Ruby Scripts that came before it. The format is what makes it safe to run third-party code on the path where money changes hands:

Wasm propertyWhat Shopify enforcesSecurity benefit
No ambient accessThe module reads its input only through imported API functions and returns operations. No filesystem; no network, except a fetch target limited to custom apps on Enterprise stores, where Shopify makes the HTTP call on the function's behalfThe code has no outbound path of its own, so it can't send cart or customer data anywhere, and a prompt or payload injected into the cart has nowhere to go
Declared inputAn input query names the fields the function receivesData minimisation by schema: a field not in the query never reaches the code. This is the gate's redaction, done by Shopify
DeterministicNo randomness and no clockThe same input gives the same output, so a decision can be replayed for audit, and there is no timing channel to probe
Hard budget256 kB binary, 10,000 kB linear memory, 512 kB stack, and for carts up to 200 lines, 11 million instructions, 128 kB input and 20 kB outputA runaway or hostile module is stopped by an instruction count, not a timeout, so one bad function can't stall checkout for everyone
No URLShopify Functions are never invoked directly by URLThere is no public endpoint to scan, authenticate or rate-limit
Server-sideThe module runs on Shopify, not in the browserA price computed in the browser can be edited in the browser; a price computed in a Function can't

This is bind before you allocate, at checkout scale: Shopify sizes the sandbox, the function reads only the fields it declared, decides, and hands back operations for Shopify to apply. Nothing is allocated to it beyond the budget, and nothing leaves it.

The limits: Wasm isolates the function, it doesn't make the function correct; a wrong discount rule is applied to every cart, so test it like any other release. Shopify strongly recommends Rust, because a JavaScript Function carries a JavaScript engine inside its module and spends the instruction budget faster, which is how large carts fail. Custom apps that use the Function APIs need Shopify Plus. See October 1: script tags, Functions, the cart for the migration and the language choice.

Shopify apps: where the Wasm sits, the security rules, and the deprecation calendar

The Wasm bundle is one slot in a Shopify app, not the whole app layer. A Shopify app has several parts, and only Shopify Functions run as Wasm:

Part of a Shopify appWhere it runsWhat it's made of
Shopify FunctionsShopify's servers, during cart and checkoutWasm, compiled from Rust, or from JavaScript through Javy. Deterministic, no network, hard limits. Discounts, delivery and payment rules, validation, bundles
Checkout and customer account UI extensionsThe buyer's browser, in a sandboxed Web WorkerJavaScript or TypeScript drawing Shopify's own components. Not Wasm
Theme app extensions (app blocks and app embeds)The storefront theme, turned on by the merchant in the theme editorLiquid, JavaScript and CSS
Web pixelsA sandbox on the storefront and checkout, gated by the customer's privacy choicesJavaScript, for analytics and conversion events only
Embedded admin appAn iframe in the Shopify admin, using App Bridge and session tokensYour web app, hosted wherever you choose
App backend: webhooks, App Proxy, APIsYour own servers, such as a Worker in front of XanoAny language

The same Rust source can build two different artifacts: a Shopify Function, Wasm on Shopify's servers that decides prices and rules, and a browser bundle, Wasm on the device that draws and computes on the page. The browser bundle reaches a storefront through an app embed block or through the App Proxy described below. It runs like any page script; it gets neither the Function's checkout privileges nor its limits.

The security rules a public app must meet. These are Shopify's App Store requirements and data rules, and each one is permissions as architecture:

RuleWhat Shopify requiresWhat it binds
Encryption in transitA valid TLS certificate; all traffic between client and app server encrypted, with no certificate errors (requirement 3.1.1)The wire
Least-privilege scopesRequest only the access scopes the app needs, and be ready to justify them; sensitive scopes such as read_all_orders need approval (3.2)What the app may reach
OAuth firstAuthorize through OAuth immediately on install and on reinstall, before any other step (2.3)Who granted the access
Session tokensEmbedded apps authenticate with session tokens, without third-party cookies or local storage (1.1.1)Each admin request
Mandatory compliance webhooksSubscribe to customers/data_request, customers/redact and shop/redact; answer with a 2xx, reject a bad HMAC with 401, and complete the action within 30 days, even if the app stores no personal dataData subject rights
Protected customer dataLevel 1 for any customer data: minimize, encrypt at rest and in transit, set retention. Level 2 adds name, address, email and phone: encrypted backups, limited staff access, access logs that are reviewed, and data protection reviews if selectedPersonal fields, field by field
Current APIs onlyNew public apps must use the GraphQL Admin API (since 1 April 2025); an app using APIs deprecated within 90 days can't be submittedTime: no new code on a dying surface

In this architecture the Worker carries most of these rules once, for every theme: it checks the App Proxy HMAC, holds the session, answers the compliance webhooks and routes them to the subject's home Xano instance, and Xano's ledger records that each request was completed within 30 days.

Delisting. Shopify publishes one automatic rule, in its API versioning policy: "If your app continues to use unsupported resources after the upgrade deadline, it's delisted from the Shopify App Store." Merchants are also blocked from installing it for at least seven days, and see warnings in the admin until seven days after the last use of an unsupported resource. The Partner Dashboard's API health report lists which calls need changing. Shopify doesn't publish an inactivity or install-count rule. Beyond the versioning rule, its requirements say the review team may reject an app at its discretion, and that breaking the rules can lead to "removal of reviews, demotion or delisting of your app, or termination of your Partner account" (requirement 1.3). An app whose core functionality has changed since it was listed must be resubmitted for a full review. Developers report delistings after missing the response window on a Shopify governance email, and in some cases every app under the same Partner account was delisted at once. Those reports are anecdotal, but the pattern is familiar: one account is one perimeter. Three practices follow:

Case: the October 2026 checkout hijack. Between 10 and 14 October 2026, attackers used a few third-party Shopify apps to send some shoppers to fake payment pages. Shopify's own checkout wasn't breached; what the apps sent to stores was changed.

The review checked the code the app shipped; it couldn't check what that code fetched later. Remote configuration can change without a release, which is why it is useful, and that is also why a compromised configuration system reaches every store that reads it. In this article's terms the slot was the right shape, but the gate was missing on the app's own config path:

The questionWhat went wrongThe rule in this design
What runs first?Reviewed code, delivered from Shopify's CDNKeep it. App embeds are visible and can be switched off
What does it fetch afterwards?Unsigned configuration from the developer's APISign config bundles and verify them before applying, like the policy bundle; deny on a bad or stale signature
How is that content handled?A data field executed as codeNo code fields in remote configuration. Insert text with textContent, never innerHTML
Who can change it?An internal tool that reached many stores at onceTwo-person approval and an audit log for any change that reaches more than one store
Where can checkout go?Off-domain, to a lookalikeCheckout links are built only from allowlisted origins, and a synthetic test fails on any off-domain redirect

Was it linked to the deprecation calendar? No public evidence says so, and the facts point the other way. Hulk Product Options was already on the approved slot: its script loaded from Shopify's CDN, and Shopify responded by disabling the app's theme extension, which is the app-embed route the calendar moves apps toward, not a deprecated script tag. The attack used no deprecated feature; it came through the app's own configuration feed. The dates don't connect either: the exploit began on 10 October, nine days after the 1 October script-tag freeze, which doesn't apply to app embeds, and six days before API version 2025-10 retired on 16 October. HulkApps has said only that "an exploit on October 10 introduced unauthorized configurations." The apps' later removal from the App Store most likely followed the breach, though neither Shopify nor the developers have said why.

So the calendar and the breach answer different questions. The calendar fixes how app code gets onto the page: through a slot the merchant can see and switch off. It doesn't govern what that code fetches once it's running. Migrating to app embeds is necessary, and it isn't sufficient; the configuration path needs its own gate.

The same gap applies to any executable, and most of all to firmware. A review, a scan or a signature proves what was delivered at one moment. What matters is what runs later, and what it loads after the check. The NVIDIA container escape was this gap at a smaller scale: a time-of-check, time-of-use flaw.

ExecutableChecked whenWhat can change after the checkThe gate
Shopify app embedAt App Store reviewThe configuration it fetches at runtimeSigned configuration, no code fields
Browser Wasm bundleAt buildAnything it fetches or is told to loadA content security policy that allowlists where it may connect
Shopify FunctionAt every deployNothing: no network, declared input onlyAlready gated. This is the model the others aim for
Container imageAt build and scanA tag that points to a new image; what the runtime mounts into itPin images by digest, sign them, and keep the GPU toolkit patched
Driver and firmwareSigned by the vendor at releaseThe update channel, its configuration, and anything below the operating systemAccept only signed images, verify at boot, attest the device, roll out in stages

Firmware is the hardest case. It runs below every sandbox in this article, survives a reinstall of the operating system, and few tools can inspect it. One firmware image ships to every unit of a part number, so a bad update reaches the whole fleet at once: the castle vault again. A signature doesn't settle it either. If the vendor's update channel is compromised, the malicious update arrives signed, as in the 2019 ShadowHammer attack on ASUS's Live Update tool. The defenses are the same rules applied lower down: an inventory by MPN and firmware version, updates only from the vendor's channel and rolled out in stages, measured boot, and device attestation where the hardware offers it. On rented GPUs, NVIDIA's attestation for confidential computing lets a job check what it is running on before it receives data.

What a remote load through Cloudflare protects, and what CORS doesn't. Everything in the Hulk attack was visible in the browser. The app's script, on every storefront, called productoption.hulkapps.com/api/v2/store/get_all_relationships and ran whatever came back in advanced_users.custom_js. That visibility is how a developer found the attack in the browser's developer tools, and it is also why it worked: the page trusted a third-party endpoint to send code, and nothing between the two checked it.

Hard-code nothing in the theme you wouldn't publish. Whatever a client-side render needs is readable by anyone: the theme's Liquid output, its JavaScript, app embed settings, and every network call the page makes. Never put these in a theme: API keys or tokens (other than ones designed to be public, such as a Storefront API token with read-only scopes), internal tool or admin URLs, third-party configuration endpoints keyed by shop domain, pricing or discount logic (that belongs in a Function), or switches that unlock behavior. The theme should carry only a proxy path, a version number, a content hash and public IDs.

Load remote code and configuration through the Worker, not straight from a vendor. The page calls the store's own App Proxy path. Shopify signs the request; the Worker verifies the HMAC, fetches the configuration from Xano with a server-side secret, checks its signature, keeps only the fields the schema allows, and returns data, never code. Wasm and JavaScript bundles are named by version, served immutable, and pinned in the theme by hash.

<!-- In the theme: a path, a version and a hash. Nothing secret. -->
<script type="module">
  const cfg = await (await fetch("/apps/crm/config?v=42")).json(); // App Proxy: same origin, signed by Shopify
  const { instance } = await WebAssembly.instantiateStreaming(
    fetch("https://assets.example.com/app-42.wasm", { integrity: "sha384-..." }), {} // bytes must match
  );
  document.querySelector("#option-label").textContent = cfg.label; // text, never innerHTML
</script>
// In the Worker, behind the App Proxy: data fields only, whatever the source contains.
const ALLOWED = ["label", "badge", "max_options"];
if (!(await verifyAppProxy(url, env.SHOPIFY_APP_SECRET))) return new Response("", { status: 401 });
const raw = await getSignedConfig(env, shop); // fetched with a server secret; throws on a bad signature
const cfg = {};
for (const k of ALLOWED) if (["string", "number", "boolean"].includes(typeof raw[k])) cfg[k] = raw[k];
return Response.json(cfg, { headers: { "Cache-Control": "no-store" } });

A custom_js field injected upstream never reaches the page: the allowlist drops it, and the theme has no code path that would run it. Each control covers one part of the path, and none covers it alone:

ControlWhat it protectsWhat it doesn't
CORS allowlist on the Worker: reflect only listed origins, with Vary: OriginWhich other sites' pages can read your API's responses in a browserIt isn't authentication: curl ignores it, and a plain <script src> needs no CORS. It can't stop an allowed origin from receiving bad data, which is exactly the Hulk case
App Proxy with HMACThe request came through Shopify, for that shop, on the store's own domain; no vendor endpoint appears in the themeWhat the response contains
Server-side secret and signed configurationOnly your pipeline can change configuration; tampering in transit or in storage fails the signatureA compromised signing tool, as with Selleasy's internal tool. That needs two-person approval and an audit log
Schema allowlist in the WorkerCode fields are dropped even if they were injected upstreamA malicious value inside an allowed text field, which is why the page inserts text with textContent
Integrity hash on Wasm and JavaScriptThe bytes that run are the version you pinnedWhat that version does with its inputs. Cross-origin fetches need the asset to send CORS headers for the check to run
Content security policy, where you control the page's headers (a Webflow export served by the Worker)Where page code may load scripts from and send data to; a lookalike checkout domain is refusedPages whose headers the platform sets for you

For a public, immutable bundle, Access-Control-Allow-Origin: * is fine, because there is nothing to protect in reading it. For an API that returns store or customer data, list the origins and never echo the request's Origin back unchecked.

Nothing secret runs in the client: anything visible on inspect is safe to show. Script injection is contained: the page runs only pinned code, and receives only signed data, never code. Contained, not eliminated: what remains is a compromised signing tool, a bug in your own code, or a malicious extension on the shopper's device.

Once the gates pass: AI-assisted data binding. The gates aren't only a cost. Data that has passed them is typed, minimized, labeled with its consent and region, signed, and stripped of code. That is the shape a model handles well, so the work that usually goes into hand-written glue code can be assisted:

The rule that keeps this safe: the model proposes, the gate decides. A proposal is data, not an action. It holds no credentials and binds nothing on its own; it becomes a change only after the same schema allowlist, the tests below and a person's approval have passed it. Because the model sees only what already passed the gates, a wrong proposal costs a failed test, not a leak. The benefit is speed with a paper trail: bindings are written faster, every one is a reviewed diff with a test, and there is less hand-written glue code, which is where bugs like a label rendered through innerHTML come from.

Tests are the thresholds, and Rust runs them as you work. In Rust, tests are part of the language and the toolchain: a #[test] function sits next to the code it checks, and cargo test runs it with no extra framework. The gate's thresholds become tests, so a change that breaks one can't ship:

use serde_json::{Map, Value};

const ALLOWED: [&str; 3] = ["label", "badge", "max_options"];
const MAX_POLICY_AGE_SECS: u64 = 300;

/// Keeps only allowlisted fields that hold plain values; code never passes.
pub fn allow(raw: &Map<String, Value>) -> Map<String, Value> {
    ALLOWED
        .iter()
        .filter_map(|k| match raw.get(*k) {
            Some(v @ (Value::String(_) | Value::Number(_) | Value::Bool(_))) => Some((k.to_string(), v.clone())),
            _ => None,
        })
        .collect()
}

/// Fail closed: a policy older than the threshold denies.
pub fn policy_fresh(age_secs: u64) -> bool {
    age_secs <= MAX_POLICY_AGE_SECS
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn code_fields_never_pass() {
        let raw = json!({ "label": "Size", "custom_js": "hijack()", "badge": { "html": "<img onerror>" } });
        let cfg = allow(raw.as_object().unwrap());
        assert_eq!(Value::Object(cfg), json!({ "label": "Size" }));
    }

    #[test]
    fn stale_policy_denies() {
        assert!(policy_fresh(300));
        assert!(!policy_fresh(301));
    }
}

Two layers run at different moments. On every save, the editor's rust-analyzer runs cargo check by default, so type and ownership errors appear as you type. The tests run on every save too when a watcher such as bacon test is running, and they always run in the build: put cargo test before the compile in the Workers Builds command, so a failed threshold stops the deploy. The same tests judge every AI-proposed binding, which is what makes accepting AI help safe: the threshold doesn't care who wrote the change.

One more check to run: a third-party tracker's snapshot marks 126 apps delisted, 89 of them dated 15 October 2026, with every cause unverified. Some may be fallout from the hijack and some from the 2025-10 retirement, but those dates come a day before the 16 October cutoff, so the versioning rule alone doesn't explain them. Shopify publishes no list of delisted apps; trackers diff the App Store over time. If you run a public app, check its listing status in the Partner Dashboard.

The deprecation calendar. Shopify releases a new API version every quarter, on the first day of the quarter at 17:00 UTC, and supports each version for at least 12 months. A request for a retired version falls forward to the oldest supported version, so old code keeps receiving answers in a newer shape. Read the X-Shopify-API-Version response header to see which version actually answered. The dated changes that matter here, as of October 2026:

DateChangeReplacementStatus
1 Oct 2024REST Admin API becomes legacyGraphQL Admin APIDone
1 Apr 2025New public apps must use GraphQLGraphQL Admin APIDone
28 Aug 2025Plus stores: checkout.liquid and script tags on the Thank you and Order status pages turned offCheckout and customer account UI extensions, web pixelsDone
1 Jan 2026No new legacy custom apps created in the Shopify adminApps created in the Dev Dashboard, with a client credentials grant for tokensDone
15 Apr 2026Shopify Scripts can no longer be edited or publishedShopify FunctionsDone
30 Jun 2026Shopify Scripts stop runningShopify FunctionsDone
26 Aug 2026Non-Plus stores: Thank you and Order status script tags turned offCustomer account UI extensions, web pixelsDone
1 Oct 2026scriptTagCreate and scriptTagUpdate return errors on every API version; existing storefront script tags keep runningApp embed blocks in a theme app extension; web pixels for analyticsDone
16 Oct 2026, 15:00 UTCAPI version 2025-10 stops being accessible; apps still using unsupported resources are delisted2026-01 or laterDone
16 Jan 2027, 15:00 UTCAPI version 2026-01 stops being accessible2026-04 or laterNext
1 Mar 2027Shopify stops injecting script tags into storefrontsApp embed blocks; web pixelsComing

After that, a version retires every quarter on the 16th at 15:00 UTC: 2026-04 in April 2027, 2026-07 in July 2027, 2026-10 in October 2027. Shopify CLI refuses to deploy UI extensions that target a version more than 12 months old, so the tooling blocks a stale app before the cutoff does. Fields can also be removed between versions: something deprecated in one release can be gone in the next.

Read the calendar as one direction of travel. Each deprecation moves code out of open page scripts, which could read and change anything on the page, into a slot with a declared boundary: Ruby Scripts into Wasm Functions, checkout.liquid into sandboxed UI extensions, script tags into app embeds the merchant can see and switch off, and tracking into web pixels that respect consent. Shopify is applying permissions as architecture to its own platform. An app built on those slots from the start has much less to migrate.

The Rust path: a Webflow page, a Cloudflare compile, a Xano record

Ruffle keeps old SWF files playable. New interactive work doesn't need the SWF format or an emulator: write it in Rust, compile it to Wasm once, and nest it in the stack this article already uses. Each system keeps one job, and none of them takes on another's:

LayerIts jobWhat it holdsWhat it never does
WebflowThe pageLayout, CMS copy, a <canvas> and one module script tag; or a Webflow Cloud app mounted at a path such as my-site.webflow.io/my-appCompile code, hold the binary, or hold personal data
CloudflareThe compile, the serve and the gateWorkers Builds turns a git push into a .wasm bundle; Workers static assets serve it; the gate Worker signs R2 URLs and checks postureHold the truth
XanoThe recordSign-in, entitlements, consent, saved state, signed media URLsRun the Wasm, or parse an untrusted file
The deviceThe renderThe compiled module, run with WebGPU and a WebGL2 fallbackSend anything the gate didn't allow

The compile happens once, on Cloudflare. Rust isn't preinstalled in the Workers Builds image, so the build command installs it. Trunk then fetches wasm-bindgen and wasm-opt itself, and the size check makes the frame budget a build failure rather than a guideline:

# Workers Builds › build command
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --target wasm32-unknown-unknown
. "$HOME/.cargo/env"
cargo install --locked trunk          # compiles from source: cache it, or fetch the release binary
trunk build --release --dist dist     # with filehash = false in Trunk.toml, so the Webflow tag never changes
find dist -name '*.wasm' -size +3M | grep -q . && { echo "wasm over budget"; exit 1; } || true
// wrangler.jsonc — the deploy command stays `npx wrangler deploy`
{ "name": "rust-app", "compatibility_date": "2026-10-19", "assets": { "directory": "./dist" } }

Wrangler sets each file's Content-Type from its extension, so .wasm is served as application/wasm, and the default ETag revalidation means a new build is picked up without renaming the file.

The page only loads it. In a Webflow custom code embed:

<canvas id="app"></canvas>
<script type="module">
  import init from "https://app.example.com/rust-app.js";
  init();
</script>

Module scripts and streamed Wasm are fetched with CORS, so a Worker on a subdomain needs a _headers file in dist that sends Access-Control-Allow-Origin for the Webflow domain, and www and the bare domain count as different origins. A Webflow Cloud app at a mount path avoids that: it runs on the Workers runtime under the site's own domain. If the site sets a Content Security Policy, script-src needs 'wasm-unsafe-eval'.

Xano is reached through the gate. The module calls the gate Worker with fetch, the gate checks posture and forwards to Xano, and Xano answers with data and signed, expiring R2 URLs. Results go back the same way. Xano never sees the binary, and the device never sees a field the gate removed.

What the path buys, against the rest of this article:

The limits: Wasm reaches the page and the GPU only through JavaScript bindings; a bundle over budget costs more to download than it saves; and Rust removes memory-safety bugs, not logic bugs. Keep Ruffle for the SWF files you already have, with the same hosting rules (application/wasm, 'wasm-unsafe-eval', CORS on the file's origin).

The strategy: bind before you allocate

Rent or buy GPUs last. Every request first passes through the cheaper distributed layers, and each layer either answers it or removes data from it. A GPU is sized only for what is left. That ordering is where both the cost saving and the security saving come from, and it can be shown with arithmetic before any GPU contract is signed.

Bind before you allocate, an illustrative funnel of one million requests. The device handles 30%, leaving 700,000. The binding gate denies 10% and redacts personal fields from 12 to 3, leaving 630,000. The edge cache answers 40%, after the gate, leaving 378,000. Workers AI triage settles 70%, leaving 113,400. Only 113,400 reach GPU allocation, as redacted, signed payloads. Result: about 89% fewer GPU calls and about 97% fewer personal-field exposures. Net saving equals GPU cost avoided minus the cost of Workers, Workers AI and the cache.

Open the SVG full size (1600 × 788) · Download the image · Download the PDF.

The rates in the diagram are illustrative, not measurements. They show the method. Replace each one with the pass-through rate you measure at that stage.

The cost case

GPU spend scales with the requests that reach it:

requests_at_gpu = requests × (1 − device) × (1 − denied) × (1 − cached) × (1 − triaged)
gpu_cost        = requests_at_gpu × gpu_seconds_per_request × price_per_gpu_second
net_saving      = gpu_cost(all requests) − gpu_cost(requests_at_gpu) − (Workers + Workers AI + cache cost)
StageRemovesStill travellingCost of the stage
Incoming1,000,000
1 · Device30%: previews and real-time 3D on the person's GPU700,000None to you; the person's device does the work
2 · Binding gate10%: unconsented, out-of-policy or unbound requests630,000One Worker invocation and a Durable Object read
3 · Edge cache40%: repeat transforms and answers378,000Cache storage and reads
4 · Workers AI triage70%: easy cases a small model can settle113,400Workers AI inference, priced per use
5 · GPU allocation113,400Sized for about 11% of the original demand

Two things make the saving larger than the request count suggests. GPU capacity is bought for the peak, and the device and cache stages remove the most repetitive, burst-prone traffic first. And a smaller, steadier GPU workload can often run on a few GPU pods of your own (media ladder rung 3) instead of a rented cluster (rung 4).

The security case

Count the personal fields that leave each boundary, not just the requests:

Requests reaching a GPUPersonal fields per requestPersonal-field exposures
GPU first1,000,0001212,000,000
Bind before you allocate113,4003, after redaction at the gate340,200, about 97% fewer

The other savings don't show up in the count:

What to measure before sizing GPUs

  1. The share of requests the device can serve: previews, real-time 3D, small on-device models.
  2. The gate's deny rate and the number of fields it redacts, per market.
  3. The cache hit rate, measured after the gate.
  4. The share of cases Workers AI settles, and how often its answer is overturned later. A triage that misroutes hard cases costs quality, not just money.
  5. GPU-seconds per remaining request, and the peak-to-average ratio of what reaches the GPU.

If the remaining demand is small and steady, own a few GPU pods. If it is large or bursty, rent. Either way, the decision is made on the 11%, not on the 100%.

Incremental knowledge vs boil the ocean

There are two ways to make AI know your business. Boiling the ocean means putting everything you have into a general-purpose model, by training or fine-tuning it, and hoping the answers come out right. Incremental knowledge means leaving the model general and giving it, for each question, only the ranked evidence that question needs. The first changes model weights with GPUs. The second changes evidence weights with rows, and reaches a GPU only when it must. That makes it the knowledge side of "bind before you allocate".

Incremental knowledge vs boil the ocean, and what AI weighting weighs. Six comparisons. Starting point: a model trained or tuned on everything, versus a general model that reads only what is retrieved. Adding knowledge: retraining with GPU hours, versus one new row in minutes. Fixing a wrong answer: retraining, versus correcting or demoting the source. Erasure: the fact stays in the weights, versus deleting the row. Citations: none, versus every answer citing its rows and weights. GPU need: up front, versus only for cases weighting can't settle. AI weighting: score equals authority times freshness times eligibility times relevance, with eligibility zero when consent or rights don't allow the use. Illustrative authority tiers: system of record 1.0, reviewed spec 0.8, knowledge base 0.6, inbox and tickets 0.4, open web 0.2. Rank first, fall back last: rank the data; if the top score clears the threshold, a small model answers from the ranked rows and the answer is cited; if not, a larger token model answers with more context, tokens and cost and a higher error risk, and the answer is flagged for the record or a person to confirm.

Open the SVG full size (1600 × 1100) · Download the image · Download the PDF.

Boil the ocean (general purpose)Incremental knowledge
Adding a factRetrain or fine-tune: GPU hours and a new model versionAdd one row to the index: minutes, no GPU
Fixing a wrong answerRetrain, or prompt around itCorrect or demote the source; the next answer changes
Erasure requestsThe fact stays in the weights until the next retrainDelete the row; it is gone from the next answer
AuditThe model can't say where a fact came fromEvery answer cites its rows and their weights
GPU spendUp front, before the first answerOnly for the cases ranking can't settle

This knowledge base works the incremental way. A new article is one line in the book's summary, and it is listed and searchable after the next index run. No model is retrained.

Fine-tuning still has a place: tone, output format, and narrow classification tasks a small model must do fast. It is the wrong tool for facts, because facts change, and a fact held in the weights can't be cited, corrected or erased on its own.

AI weighting: evidence, not parameters

Weighting here means how strongly an answer favours a piece of evidence, not a model's parameters. It matches the definition in AI enabled forms with LLM weighting: a weight is set by what the corpus can prove, not by the model alone.

score = authority × freshness × eligibility × relevance
FactorWhat it measuresHow it is set
AuthorityHow much the source can be trustedBy tier (illustrative): system of record 1.0, reviewed spec 0.8, knowledge base 0.6, inbox and tickets 0.4, open web 0.2
FreshnessHow recently the source was trueDecays at a half-life set per tier: hours for a price, months for a policy
EligibilityWhether this use is allowed at all1 or 0, from the gate: consent, rights, market. Never a fraction
RelevanceHow well the source matches the questionRetrieval similarity, the only factor a model computes

Because eligibility multiplies, a source that the gate doesn't allow scores zero however relevant it is. The model never sees it, so it can't leak it.

Rank first, fall back to a larger token model last

  1. Rank the data. Score every candidate row and keep the top few, with their weights.
  2. Check the threshold. The top score must clear it, and the top rows must broadly agree with each other.
  3. If it clears: a small model phrases what the rows say. It uses few tokens and has a low error risk, and every claim cites a row. To fix the answer, fix the row.
  4. If it doesn't: fall back to a larger token model, with more context, more tokens and a higher cost per answer. It fills the gaps from its own weights, which is exactly where the error risk lives. Its answer is flagged as low confidence, and the record or a person confirms it before anyone acts on it.

The fallback is a deliberate trade, not a failure: you accept a bigger error risk in exchange for an answer, and you label it as such. Count how often it fires. A rising fallback rate means the evidence is missing, not that the model is too small, and the fix is a row, not a GPU.

Context and data streams: scale by direction, tokenize last

Everything above happens before a model reads a single token. This section names that boundary. Context reaches a model from two streams that run in opposite directions and scale in opposite ways, and both should be finished (typed, gated and ranked) before the person's intent is turned into tokens or vectors.

Context and data streams: scale by direction, tokenize intent last. Outbound: catalog and content from the system of record fan out through the edge cache into typed outbound context with tier, freshness and market. Inbound: events, forms and questions from people and agents fan in through the gate and a queue partitioned per subject and region, into typed inbound context holding one subject's consented state. Both meet in context assembly, which joins by key, ranks by weight, drops ineligible rows and caps the token budget. Only then is the intent, with the assembled context, tokenized or vectorized. Rules: direction sets the scaling; filter while typed; gate before embedding; tokens are the bill.

Open the SVG full size (1600 × 660) · Download the image · Download the PDF.

Outbound streamInbound stream
CarriesWhat you publish: products, prices, policies, articlesWhat people and agents send: events, forms, questions, consent
ShapeFans out: one source, many readersFans in: many sources, one record per subject
Scales byCache: more readers cost almost nothing at the edgePartition: per subject and per region (Durable Objects, queues, one Xano instance per jurisdiction)
Arrives asClaims with tier, freshness and market attachedOne subject's consented state, and nothing about anyone else
FailsStale: serve the last good copy, marked with its ageClosed: no posture means no context

Why direction matters. Each stream scales in its own way. Scaling the outbound stream means adding cache, not compute. Scaling the inbound stream means adding partitions, not making one database bigger. A design that sends both streams through the same pipe, such as one vector store holding catalog and customer text together, has to scale both the expensive way and loses the boundary between them.

Why tokenize last. Intent is what the person wants, and it only becomes tokens (for a model to read) or vectors (for a similarity search) at the end. Before that line the context is typed: every field still has a name, a source tier, a market and a consent state, so it can be filtered, joined and capped. After the line it is text and numbers:

Read with the funnel above: the device, gate, cache and triage stages all sit before the tokenization line. "Bind before you allocate" and "tokenize last" are the same rule seen from the cost side and from the data side.

The thread, in one picture

Hyperscaler vs distributed: the thread. A spectrum runs from centralized to distributed: hyperscalers (AWS, Azure, Google Cloud), neoclouds (CoreWeave), edge networks (Cloudflare, Fastly, Akamai), and the person's own device. Four questions sit along it. CoreWeave or Cloudflare: different layers, use both. Hyperscaler or distributed: keep the core central and the edge in front. Where do AEM Edge Functions run: on Fastly Compute behind Adobe's managed CDN, in beta. Can Xano distribute Kubernetes: no, one instance per jurisdiction with an edge router and gate. The answers feed an orchestration ladder and an AI binding ladder, which lead to the enterprise impact.

Open the SVG full size (1600 × 750) · Download the image · Download the PDF.

This article began as four questions, each asked after the one before was answered. Read the diagram top to bottom: the spectrum, then the questions placed on it, then what the answers add up to.

  1. CoreWeave or Cloudflare? Different layers. CoreWeave rents dense GPU clusters for training and serving large models. Cloudflare runs the network between users and applications: CDN, DDoS protection, DNS, a firewall, zero trust, and a developer platform (Workers, R2, D1, KV, Workers AI). A model trained or hosted on one can sit behind the other.
  2. Hyperscaler or distributed? A hyperscaler brings the work to the data center; an edge network brings the data center to the user. Most production systems keep the core central and put the edge in front.
  3. Where do AEM Edge Functions run? Adobe rents its edge. The functions are JavaScript modules deployed to Adobe's managed CDN, which runs on Fastly Compute, and they are reached through origin-selector rules in cdn.yaml. The feature is in beta.
  4. Can Xano distribute Kubernetes? No. Xano hides its infrastructure from you, and each instance is a regional island. Distribution happens one level up: one Xano instance per jurisdiction, with an edge router and gate in front.

One spectrum, three models

HyperscalerNeocloud (GPU cloud)Edge network
ExamplesAWS, Azure, Google CloudCoreWeaveCloudflare, Fastly, Akamai
FootprintFew, huge regionsFew, dense GPU sitesHundreds of small sites
Optimized forThroughput, rich state, every service in one placeRaw GPU computeLatency, global reach, request handling
Typical workDatabases, data pipelines, enterprise back endsTraining, fine-tuning, large-model servingCDN, security, edge functions, small-model inference
Data modelCentralized, strongly consistentCentralized, job-scopedCached, replicated, eventually consistent
You pay inDistance from the region; egress feesScarcity and commitmentDepth: little room for heavy state

Past the edge sits the device: the person's own phone, laptop or console, and its GPU. It isn't a vendor's network, but it is the most distributed place compute can run, and the only one where data never travels.

The lines are blurring. Hyperscalers sell edge products such as CloudFront, Lambda@Edge and Azure Front Door, and edge networks keep adding storage, databases and GPU inference. The model you start from still decides what is cheap and what is hard.

Rented edges. Not every edge belongs to the vendor that sells it. AEM Edge Functions run on Fastly, and Webflow Cloud apps deploy to Cloudflare Workers through Wrangler. So "edge" in a vendor's diagram can mean someone else's network, with someone else's limits, logging and data-processing terms. Ask whose edge it is before you write consent or residency logic for it.

The edge distributes; the system of record decides

The edge distributes and the system of record decides. A request reaches a Cloudflare Worker that acts as router and gate. The Worker reads the subject's posture from a per-subject Durable Object, routes writes to the subject's home Xano instance (EU, US or KR, each with Postgres and a ledger), and sends only signed, gated payloads downstream. Xano pushes a projection to the Durable Object after each write. Four rules: Xano wins disagreements; fail closed; personal data stays in its home instance; the signature binds the posture and the ledger head hash.

Open the SVG full size (1600 × 760) · Download the image · Download the PDF.

System of record data gating means the system of record holds the truth, and nothing leaves until a gate has checked that truth at the moment of the action.

Xano suits the system-of-record role: deterministic, auditable logic in Postgres with its own API layer. It is a poor fit for the gate itself, because it lives in a few regions and would add latency to every request. Keep the hot path in Workers and Durable Objects, and keep Xano as the slower, authoritative write path.

Regional language weights: loaded by audience and data set, selectively to native

The same split applies to the model itself. Instead of one large model that knows every language and every market, ship a small base model plus regional pieces: a language adapter (a small set of extra weights, such as a LoRA, that tunes the base model for Korean or English) and a data set for that audience, such as a market's catalog and glossary. The edge chooses which pieces a person gets; the system of record decides which pieces may exist and who may receive them. Native here means the person's own device: a browser through WebGPU or Wasm, or a native app.

A Korean shopper on www.example.kr gets the Korean adapter and the Korean catalog, served from the Seoul-region bucket. A US shopper on the same build gets the English adapter and the US catalog, and never receives the Korean pieces. Cloudflare R2 offers enforced jurisdictions only for the EU and FedRAMP; elsewhere, such as Asia-Pacific, a location hint is a preference, not a guarantee, so weights tied to Korean data belong in the Seoul Xano instance's own storage.

In business terms: one chain of stores, with signage and shelves stocked by market. Head office (the system of record) decides what each market may stock, the trucks (the edge) deliver it, and each store (the device) keeps only its own market's stock. A customer's personal file never goes on the shelf.

Kubernetes and VMs limit reach, not what a model may weigh

A common assumption is that a model running in Kubernetes inside a VM can only weigh the data on that VM. It can weigh whatever it can reach, and the VM doesn't decide that:

Inside one cluster, reach is set by five controls, and the model sees only what they let through:

ControlWhat it decides
Default-deny NetworkPolicyWhich pods may open a connection at all
Service mesh mTLS with workload identityWhich service is really calling
Authorization policy (OPA, Cedar, Istio AuthorizationPolicy)Which call is allowed: this service, this method, this purpose
Workload identity mapped to cloud IAMWhich buckets, databases and models the pod can touch
An egress gateway with an allowlistWhere data may leave to

Between VMs, clusters or clouds, data moves only through a gate, and the rules are enforced at every hop rather than carried inside the data:

  1. The sending gate decides, in deterministic code, not a prompt: who asks, for what purpose, under which consent, in which region. It strips the fields the receiver doesn't need.
  2. The payload carries a signed label: purpose, consent state, region, expiry and the ledger head hash that allowed it, issued for one receiver and valid briefly.
  3. The receiving gate checks again with its own policy, and rejects anything expired, off-purpose or out of region. It doesn't take the sender's word.
  4. The model comes last. Ranking, embedding and inference run only on what passed both gates, which is where eligibility becomes the 0-or-1 factor in the weighting.

The limit: a copy that has landed can't be recalled. Rules decide what is sent; they can't follow a copy after it arrives. So send IDs, redacted fields or the answer instead of the records; prefer expiring tokens to standing access; and use confidential VMs (AMD SEV-SNP, Intel TDX) where the host itself isn't trusted.

The hyperscalers compared: what each controls, and what an enterprise adds

All three clouds have strong controls for data moving between workloads. Each one stops at its own edge:

ControlAWSGoogle CloudAzure
Org-wide guardrailsOrganizations SCPs and RCPsOrganization Policy; IAM deny policiesAzure Policy on management groups
Data perimeter (nothing leaves the org)SCPs and RCPs with aws:ResourceOrgID; VPC endpoint policies (data perimeter)VPC Service ControlsPrivate Link; network security perimeter for supported PaaS services
Pod identityEKS Pod Identity, IRSAWorkload Identity Federation for GKEMicrosoft Entra Workload ID on AKS
In-cluster network and admission policyVPC CNI network policy; Gatekeeper or KyvernoGKE Dataplane V2; Policy ControllerAzure CNI with Cilium; Azure Policy for AKS
Service to serviceVPC Lattice (App Mesh support ended 30 September 2026)Cloud Service MeshIstio-based service mesh add-on
Application authorizationVerified Permissions (Cedar)No managed engine; run OPA or CedarNo managed engine; run OPA or Cedar
Finding sensitive dataMacieSensitive Data ProtectionMicrosoft Purview
Data in useNitro Enclaves; SEV-SNP instancesConfidential VMs; Confidential GKE NodesConfidential VMs; AKS confidential nodes
KeysKMS, CloudHSM, external key storeCloud KMS, Cloud EKMKey Vault, Managed HSM
Model guardrailsBedrock GuardrailsModel ArmorContent Safety Prompt Shields

What none of them gives an enterprise running more than one cloud, or serving more than one jurisdiction:

The per-cloud controls stay. They are the floor: they make sure nothing reaches a bucket, a cluster or a model except along the path the gate controls. The enterprise adds the layer above them.

Policy management as a service: Webflow and Xano author, Cloudflare enforces

The missing layer can be built from the stack this article already uses, with Cloudflare at the top, in front of every hyperscaler:

LayerIts jobWhat it never does
WebflowThe policy console: pages where privacy, legal and operations read the rules in plain language and propose changes, as a Webflow Cloud app or member-gated pagesStore a rule or make a decision
XanoThe policy store: tables for purposes, data classes, regions and rules (who, what, purpose, region, consent → allow, redact or deny), each version with its author and approver, in the append-only ledger. It publishes a signed, versioned bundleSit in the request path
CloudflareEnforcement above every cloud: the gate Worker evaluates the bundle, held in Durable Objects, on every request; Access checks identity; Cloudflare Tunnel and Workers VPC (in beta) reach private services in AWS, Google Cloud and Azure without public ingress; AI Gateway applies DLP and guardrails in front of model providers; the Data Localization Suite keeps processing in-regionHold the truth
The hyperscalersRun the workloads and keep the floor: perimeters, workload identity, confidential compute. Accept traffic only from the gate's pathDecide purpose or consent

A rule change moves through it like any other record:

  1. Propose in the Webflow console, in plain language mapped to fields.
  2. Approve in Xano: a second person signs off, and the rule's tests run against recorded decisions before it can publish.
  3. Publish a signed bundle with a version number. Xano pushes it to the Durable Objects; a bundle older than its threshold means deny.
  4. Enforce at the edge on every request, before any cloud, any egress fee or any GPU.
  5. Record each decision, signed with the bundle version and the ledger head hash, back to Xano.

Why this is the optimized version: a rule is written once and applies in front of all three clouds; the people who own the rules change them without an infrastructure pipeline, but not without review; denials happen before data reaches a cloud, which is bind before you allocate again; and an auditor reads one log.

The limits:

Worked example: one order, four platforms

A retailer sells on Shopify, keeps customers in Salesforce and Klaviyo, runs finance on SAP S/4HANA or Dynamics 365, and buys audiences in Google Ads. Each sits on a different cloud, and each needs a different slice of the same order:

SystemWhere it runsNeeds from the orderFloor you control
Shopify and its FunctionsShopify's platform; Functions run inside its checkout, with no network accessA loyalty tier for the discount FunctionThe Function's input query: only declared fields reach it
Salesforce, Klaviyo (CRM)SaaS, hosted on AWSEmail and an order summary, for marketingAPI scopes on the integration user; your own AWS account for any export bucket, under SCPs and RCPs
SAP S/4HANA or Dynamics 365 (ERP)AzureFull order lines and billing address, for invoicingA private endpoint reached only through Cloudflare Tunnel; the network security perimeter and Azure Policy
Google Ads (audiences)Google CloudA hashed email for Customer Match, nothing elseThe BigQuery audience dataset inside a VPC Service Controls perimeter
XanoIts regional instanceThe whole record, and the rulesThe system of record and the policy store
CloudflareAbove all of themNothing keptThe gate Worker, Durable Objects, Tunnel and Workers VPC

Salesforce and Klaviyo are SaaS: you don't control their AWS accounts, so there the floor is the integration's scopes and the data-processing agreement. The SCPs and RCPs apply to your AWS account, where any staging bucket or Lambda lives.

The order moves like this:

  1. Shopify sends orders/create. The gate Worker verifies the HMAC on the raw body and writes the order to Xano first. Xano is the truth.
  2. The gate reads the customer's posture from their Durable Object: consent per purpose (including Consent Mode v2's ad_user_data and ad_personalization), home region, and the bundle version.
  3. It applies one rule per destination, all from the same published bundle:
DestinationAllowed whenFields sentOtherwise
ERP on AzureAlways: the contract needs an invoiceOrder lines, billing address, tax IDsn/a
CRM on AWSMarketing consent granted for this regionEmail, order total, product categoriesTransactional fields only; no marketing profile
Google Ads on Google Cloudad_user_data and ad_personalization both grantedA SHA-256 hash of the normalized emailNothing
Workers AI segmentingAnalytics consent grantedA pseudonymous ID and category countsAggregate counts only
Back to ShopifyAlwaysA loyalty_tier metafield computed from ERP and CRM totalsn/a
  1. Each payload is signed with the bundle version and the ledger head hash, and sent over its own path: Workers VPC to the ERP's private endpoint, the CRM's API with a scoped token, the audience upload into the perimeter-protected dataset.
  2. Checkout enforces without a network call. At the next purchase, the Shopify discount Function reads loyalty_tier through its input query. It never sees the email, the address or the CRM. The decision was made at the gate; the Function only applies it.

When marketing consent is withdrawn, one rule record changes in Xano, a new bundle publishes, and every flow above changes on the next request: the CRM stops receiving profiles, Google Ads gets nothing, and the ERP keeps invoicing. Nobody edits three clouds' IAM.

The platforms themselves already work this way. shopify.dev and the Shopify Dev Dashboard answer from Cloudflare (server: cloudflare), while Shopify's DNS steers other hosts, such as the Partner Dashboard, to a Google edge in front of its Google Cloud core. Hydrogen storefronts on Oxygen run on Cloudflare: Shopify's own write-up describes a gateway Worker that routes each request with Workers for Platforms' dynamic dispatch. Webflow Cloud apps run on the Cloudflare Workers runtime. Cloudflare on top of a hyperscaler isn't an add-on to this stack; it is how the stack's own vendors are built.

One Worker, any theme: a Webflow export and a Shopify theme with the same permissions

Because the gate is its own origin with one contract, the page in front of it is interchangeable. A vanilla Webflow export (HTML, CSS and JavaScript, with no Webflow CMS or form backend behind it) can be served from webflow.io, a custom domain, a .kr domain, or rebuilt as a Shopify native theme, and it behaves identically. The page holds no token, no rule and no reasoning. It loads one component and calls the Worker; the Worker and Xano decide what that visitor may see and do. Moving the page to another domain changes one allowlist row, not the code.

There are two doors into the same Worker, and both end at the same subject in Xano and the same rules:

DoorUsed byHow the Worker knows who is calling
CORSThe Webflow export on any domain; a Shopify theme calling the Worker's own domainThe Origin header, matched against the published allowlist; the visitor's session token
Shopify App ProxyA Shopify theme calling /apps/crm/... on the store's own domainShopify's HMAC signature on the query string, and logged_in_customer_id

The allowlist is a policy record, not a config file. It lives in Xano with the other rules and ships in the same signed bundle. Each row says what one origin may do:

{ "origins": [
  { "origin": "https://example.webflow.io", "host": "webflow",         "consent": "crm-sync-banner",          "region": "US", "kakao_registered": false },
  { "origin": "https://shop.example.com",   "host": "shopify-proxy",   "path": "/apps/crm", "consent": "shopify-customer-privacy", "region": "US", "kakao_registered": true },
  { "origin": "https://www.example.kr",     "host": "webflow-export",  "consent": "crm-sync-banner",          "region": "KR", "kakao_registered": true }
] }
// CORS door: an exact match from the bundle. Never reflect an Origin that isn't listed.
const entry = bundle.origins.find((o) => o.origin === request.headers.get("Origin"));
if (!entry) return new Response(null, { status: 403 });
const cors = { "Access-Control-Allow-Origin": entry.origin, "Vary": "Origin" };

// Kakao Pay redirects go only to a domain Kakao has on file for the merchant.
if (rail === "kakaopay" && !entry.kakao_registered) return deny("kakao_domain_not_registered");
const kakaoBase = entry.origin + (entry.path || "");

The App Proxy door, verified in the Worker. A Shopify theme can call https://shop.example.com/apps/crm/... on its own domain; Shopify forwards it to the Worker and signs the query string. The Worker recomputes the signature with the app's secret before anything else:

// Shopify App Proxy: drop `signature`, join repeated keys with commas, sort the "k=v" pairs,
// concatenate with no separator, HMAC-SHA256 with the app secret, compare in constant time.
async function verifyAppProxy(url, secret) {
  const q = new URLSearchParams(url.search);
  const sig = q.get("signature") || "";
  q.delete("signature");
  const vals = {};
  for (const [k, v] of q) (vals[k] ??= []).push(v);
  const msg = Object.keys(vals).map((k) => `${k}=${vals[k].join(",")}`).sort().join("");
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
  const mac = [...new Uint8Array(await crypto.subtle.sign("HMAC", key, enc.encode(msg)))]
    .map((b) => b.toString(16).padStart(2, "0")).join("");
  const fresh = Math.abs(Date.now() / 1000 - Number(q.get("timestamp"))) < 300;
  const a = enc.encode(mac), b = enc.encode(sig);
  return fresh && a.byteLength === b.byteLength && crypto.subtle.timingSafeEqual(a, b);
}

A valid signature proves Shopify sent the request, not that the customer owns the data asked for: the Worker still checks that logged_in_customer_id maps to the subject whose records it returns. Three App Proxy facts shape the design. Shopify strips Set-Cookie from proxy responses, so the session is the signed parameters, not a cookie. A response sent as Content-Type: application/liquid is rendered inside the store's theme. And merchants can rename the proxy path, so links are built from the path_prefix parameter, never hard-coded.

What this keeps constant, whichever page is in front:

See Fragments for any frontend for the component side of the same idea.

Hydrogen and Oxygen: the storefront code and where it runs, with no plugins required

Hydrogen is the code; Oxygen is where it runs. Hydrogen is Shopify's framework for a custom storefront, built on React Router 7 (Remix before 2025). Oxygen is Shopify's hosting for it: edge Workers on Cloudflare's workerd runtime, the same layer as the gate in this article. In business terms, Hydrogen is the storefront you build and own; Oxygen is the building Shopify lends you on paid plans, next to its own checkout and data.

HydrogenOxygen
What it isA framework for a headless Shopify storefrontShopify's hosting for that storefront
Built onReact Router 7, reading the Storefront API and Customer Account APIStorefront Workers behind a gateway Worker on Cloudflare
You controlRoutes, components, queries, which events are sent and whenEnvironments (production, a preview per branch), variables, domains
Run it withnpm run dev locally, with Mini Oxygen as the stand-in hostshopify hydrogen deploy, or a GitHub push through the Hydrogen sales channel
Replaceable?Yes: any framework can call the Storefront API (the Headless sales channel issues its tokens)Yes: Hydrogen also runs on Vercel, Netlify or your own Cloudflare Worker

Hydrogen is always headless, but not all or nothing. It renders no Liquid theme, so there is no theme mode. It can still be one route or one subdomain (a configurator, a market microsite, an agent-facing page) while the theme serves the rest, with a Worker in front routing paths. The cart ID and the consent state must carry across both sides, and every buyer still ends at Shopify checkout, where checkout extensions and Functions run unchanged.

No plugin requirement. A Hydrogen storefront needs no app from the App Store to run. The only install is Shopify's own Hydrogen or Headless sales channel, which issues the API tokens; it is not a third-party plugin. Theme app extensions, app embeds and script tags have nowhere to load, because there is no theme for them to inject into:

What a theme gets through a pluginWhat Hydrogen does instead
App embeds and theme app extensions (reviews, product options, Klaviyo onsite tracking)The team calls the vendor's API or SDK from its own code, pinned to a version and reviewed like any dependency
Script tags and custom_js fields fetched at runtimeNothing is fetched as code; data arrives as JSON through the gate and is rendered as text
Web pixels added by appsHydrogen's analytics components send the events the team chooses, after the Customer Privacy API says yes
Theme editor sectionsMetaobjects or a CMS, read through the Storefront API

That is the security case in one line: no plugin means no injection path. The October 2026 checkout hijack ran through an app's theme extension loading attacker-edited JavaScript; a Hydrogen storefront has no such door, because only code the team built and deployed runs on the page. The cost is ownership: each integration a plugin would have installed in a click becomes code the team writes, tests and keeps current, and merchandisers lose the visual theme editor unless a CMS replaces it.

What doesn't change: Hydrogen renders on the server by default, so the consent rule in this article applies there too. A loader can send customer data to a third party before the visitor answers the banner; gate those calls on the consent signal the same way the Worker does. And Hydrogen replaces no checkout logic: discounts, delivery and payment rules still run as Shopify Functions inside Shopify.

Sandbox before on-prem: a smaller perimeter, owned privately

Data from outside (a partner feed, an agent's output, an upload, a SaaS export) shouldn't land on-prem as it arrives. It lands first in a sandbox that can reach nothing, is restricted there, and only the reduced, labeled result is let in. The on-prem perimeter shrinks because less data, from fewer systems, ever reaches it, and what does arrive is managed privately by the internal teams that own it.

StageWhereWhat happens
1. LandA private R2 bucket and a Queue, written only by the gate WorkerRaw data arrives in a zone with no route to on-prem and no credentials for anything else. The Worker deletes each object once it is promoted or rejected; an R2 lifecycle rule removes anything left after a day.
2. RestrictWorkers, with rules from XanoSchema and size checks; files parsed by memory-safe parsers; data classification; the consent and purpose check; personal fields redacted or tokenized
3. LabelThe gateEach record that passes is signed with its data class, purpose, region and bundle version. A record that fails stays in the sandbox with its reason, for its owner to review. Nothing is dropped silently.
4. PromoteOn-prem pulls; nothing pushes inAn on-prem service pulls labeled records from a Queues pull consumer over outbound HTTPS and acknowledges them. Its API token is limited to the on-prem egress addresses and expires. On-prem opens no inbound port.
5. OwnOn-prem, by internal stakeholdersEach data domain has a named internal owner. Only reduced fields arrive, so fewer people need access, and no outside processor handles the result.

A Queue message holds at most 128 KB, so a reduced record travels in the message itself; anything larger stays in the sandbox and the message carries a short-lived link to it.

What it buys:

Where it breaks:

An AI strategy is not a JVM: permissions as architecture

Java tried to secure code from inside one shared runtime. Everything ran in the JVM, trusted by default, and a Security Manager checked permissions from within. Few applications used it, and it never became the boundary it was meant to be: Java deprecated it in JEP 411 and permanently disabled it in JDK 24 with JEP 486. Isolation moved out of the runtime, to processes, containers and services.

Many AI strategies are repeating the JVM's design. One model or agent platform becomes the runtime, it is connected to the CRM, the ERP, the ad accounts and the file shares, and safety is a layer inside it: a system prompt, a guardrail model, an instruction not to. That is a security manager written in English, and it fails the same way: whatever gets inside inherits everything the runtime can reach.

Security for AI is permissions as architecture. The model is a component with no authority of its own. Every capability it uses is a separate, explicit grant, issued and checked at a barrier outside it:

AI as a runtime (the JVM pattern)Permissions as architecture
Where authority livesIn the platform the model runs inIn gates outside the model
DefaultConnected to everything; prompts say "don't"Connected to nothing; each tool, field, purpose and region is granted
How a rule holdsAn instruction or a guardrail model, which can be argued withDeterministic code, which can't
One prompt injection reachesEvery system the platform can reachOnly what that one call was granted
Changing a ruleRe-prompt, re-tune, redeployChange a policy row; the next request follows it
What an auditor readsWhat the model saidWhat it was allowed to do, signed, with the ledger state that allowed it

The least-privilege barrier has the same properties everywhere in this article: deny by default; grants per call, per purpose, per field and per region; expiry on every grant; a signature on every decision; fail closed when the policy or the ledger can't be read; and the gate written as the smallest, most deterministic, memory-safe code in the system. The model can be as capable as you like behind that barrier, because capability is not authority.

The concern people have about AI is pointing at a real problem: ambient authority, a system that can reach more than any one task needs. The answer isn't a better-behaved model. It is a structure in which behavior doesn't decide what can be reached.

Stack and heap: a memory model for data and permissions

Programs keep data in two kinds of memory, and the split between them is a useful model for both data modeling and permissions.

Most memory bugs live at the boundary. Use-after-free, double free and leaks are heap bugs: the question of who owns the memory, and when it ends, was answered wrongly. The worst stack bug is a reference that escapes: a pointer to a call's local data kept after the call has returned. Rust refuses to compile that, with the error "borrowed value does not live long enough" (the stack and the heap, in the Rust Book).

The same split, applied to the systems in this article:

MemoryData modelingPermissions
StackLives for one call; freed on return; private to the callThe request's working set: the decoded token, the consent decision, decrypted fields, the context assembled for one model callA grant issued for this request, which expires with it
HeapOutlives the call; shared by reference; needs an ownerRecords in the system of record, caches, queues, logs, embeddingsStanding access: roles, service accounts, long-lived tokens
OwnershipOne owner decides when the memory endsEach record has one system of record that may change or delete itEach standing grant has one owner who can revoke it
EscapeA reference outlives its frameRequest data written into a log, a cache, KV or a vector indexA per-call token stored and reused later

The rules that follow:

For AI the mapping is direct. A model's context window is stack: assembled for one call, from what the gate allowed, and gone when the call ends. Long-term memory, fine-tuning data and embeddings are heap: they outlive the conversation, so each needs an owner, a region, the consent it was made under and a way to delete it. Most AI data leaks are escapes, where something placed in one call's context ends up in a store that outlives it.

In business terms: who clears the file room

Picture a company handling paperwork.

Each language runs the file room differently:

LanguageWho clears the file roomIn business termsWhat goes wrong
CThe programmer, folder by folderNo records policy: every employee shreds their own filesForget, and storage fills up (a leak). Shred too early, and someone works from a destroyed file, which is how most serious security bugs are exploited.
JavaA garbage collector sweeps periodically, removing what nobody refers toFacilities staff dispose of files on their own scheduleNothing in use is shredded, so that class of bug is gone. Cleanup costs extra space and occasional pauses, and a secret can't be guaranteed destroyed on time: the crew may have copied it while tidying.
PythonEach object counts who uses it and is freed at zero; a collector handles circular casesA sign-out sheet on every folderSafe for ordinary code, but much of Python's speed comes from libraries written in C, such as NumPy and image and model loaders, so C's risks sit underneath.
JavaScript and Next.jsA garbage collector in V8, like Java'sThe same facilities crewMemory corruption isn't the main business risk; data lifetime is. A long-running server keeps anything stored outside a request alive across requests, so one customer's data can reach the next, and data rendered on the server is copied into the page sent to the browser. Both are escapes.
RustNobody sweeps; the ownership rules are checked before the software shipsAn auditor approves the records plan before the company opensEvery folder has one owner and is shredded the moment the owner is done. A plan that could shred a file in use, or keep a slip to a wiped whiteboard, is rejected at build time rather than found in production.

The trade, for a business: C is fast and cheap to run and expensive when it fails, because its mistakes surface as breaches. Java, Python and JavaScript buy safety at runtime, paid for in memory and pauses, and give up control over when data is destroyed. Rust buys safety at build time: more effort up front, then no cleanup overhead, predictable disposal, and secrets wiped exactly when their owner finishes.

Most organizations run their data the Java way: a retention job sweeps eventually. Permissions as architecture runs it the Rust way. Each record has an owner, a lifetime and access rules decided before it is stored, and the system enforces them instead of people remembering to.

Immutable and dynamic data: what goes in KV, and what doesn't

Workers KV is built for data that is read constantly and written rarely. A write is visible at once in the location that made it, and can take up to 60 seconds (or the reader's cacheTtl) to appear everywhere else; two writes to the same key race, and the last one wins. KV also has no jurisdiction restriction for where its values are stored. So the dividing line is whether the value changes, and whether a reader acting on the old value causes harm:

DataChangesWhere it belongsWhy
A published policy bundle, keyed by version (policy:v42)Never; a new version gets a new keyKVImmutable: every location agrees, however late it reads
The pointer to the current version (policy:current)RarelyKV, with the stale-bundle threshold in the gateA reader that lags a minute uses the previous version, which was also signed and valid
Allowlists, translations, the docs indexRarelyKVRead on every request; a short lag is harmless
Revoked tokens (a denylist that expires with the token)Write onceKVEach entry is written once and never edited; the up-to-60-second gap between locations is a known, stated window
Consent, entitlements, a checkout in progress, idempotency keys, countersConstantlyA Durable Object per subject, or XanoNeeds one consistent answer now; KV's last-write-wins and lag would let two requests both pass
Personal dataVariesXano in the subject's region, or a Durable Object restricted to a jurisdictionKV can't be pinned to a jurisdiction

The rule: in KV, write new keys instead of changing old ones, and name them by version. Anything that must be current, per person, or kept in a region goes to a Durable Object or to Xano.

Claims and extras: layered permissions for people

Signing in proves who someone is. It shouldn't decide everything they may do. CRM Sync's /auth/me keeps identity, profile and permission in separate layers, and each layer can only narrow what the one before it allows:

LayerHoldsChangesLives in
TokenThe subject ID and an expiry, signedNever during its life; revoked on logoutThe JWT, checked by the Worker; revocations in a KV denylist
Account statusActive or deletedRarelyXano, checked before anything else runs
ClaimsIdentity links: the Google subject, the Shopify customer IDRarely; reconciled at sign-inXano, user_claims
ExtrasProfile and preference fieldsOften; cleared when marketing consent is withdrawnXano, user_extras
EntitlementWhat this person may use: roles and capability capsWhen they buy, are granted or lose accessXano, returned to the page as a hint only
The decisionWhether this call, for this purpose, is allowedEvery requestThe gate Worker

Four design rules come out of it:

Agents get none of this by default. An agent acts under a mandate issued for a task, with its own cap and expiry, never under the person's sign-in.

Arc, row gating, claims and extras: who may share what

Rust's Arc<T> (atomically reference counted) lets many parts of a program share one value in memory. Arc::clone doesn't copy the data; it hands out another handle and adds one to a counter. When the last handle is dropped, the counter reaches zero and the value is freed, with no garbage collector. An Arc is read-only by default; changing the value needs a lock, Arc<Mutex<T>>, so only one holder writes at a time, and the compiler won't let you skip it. (Rust Workers run as single-threaded Wasm, where the non-atomic Rc is usually enough; Arc matters when threads really share data.)

In the file-room picture, an Arc is one master document with a sign-out sheet: everyone gets a handle to the same copy, the sheet counts who still holds one, and the last one to hand it back shreds it. What an Arc doesn't have is a clerk. Holding a handle is the permission. That is the whole difference from the system of record.

Arc against row-based gating. Row-based gating checks every read: in Xano, an endpoint that requires authentication and filters each query on $auth.id; in Postgres, a row-level security policy. Each row comes back only to its owner or to a role allowed to see it.

Arc (shared in memory)Row-based gating (Xano, Postgres)
When access is checkedOnce, before the value is sharedOn every read
GranularityThe whole valuePer row, and per field through the response shape
Who can readAnyone holding a handleOnly the row's owner, or a role allowed to read it
How long it lastsUntil the last handle is dropped, or the process stopsUntil a rule or a person deletes it
HistoryNone: an overwritten value is goneLedger and audit log
Right forData that is the same for every request: the verified policy bundle, the field allowlist, the public keys that verify tokens, a parsed schemaData that belongs to someone: orders, consent, profiles
How it failsA handle in the wrong place exposes the whole valueA query that forgets the filter returns everyone's rows. Keep the filter in one shared function, and test it

The rule that joins them: an Arc may hold only data that every reader would pass the row gate for, which means data no person owns. Load it once, verify it, share it, and replace it with a new version rather than editing it, the same rule KV follows.

Arc against claims and extras. Each layer of /auth/me has a different owner and lifetime, so each belongs in a different kind of memory:

LayerShared in an Arc?Where it belongs insteadWhy
Policy bundle, allowlist, token-verifying keysYesLoaded once per versionThe same for everyone, and immutable
The verified tokenNoThe request: stack-shaped, dropped at the end of the callIt belongs to one caller and expires
Account statusNoRead fresh from Xano before any writeA cached status is how a deleted account kept creating customers on 31 August 2026
Claims (user_claims)NoThe request, after the gate reads them from XanoIdentity links are personal data
Extras (user_extras)NoXano, read per request through the row gatePersonal, change often, and are cleared when consent is withdrawn; a shared copy would outlive the withdrawal
EntitlementNoA per-subject Durable Object, as a projection of XanoMust reflect a revocation now, not when the last handle is dropped
The decisionNoThe request, signed, then goneIt answers one call, for one purpose

In business terms: the Arc is the rules posted on the wall, which everyone may read and nobody signs out. Claims are the badge that says who you are. Extras are the preferences card inside your own folder. Row gating is the clerk who checks your badge every time you ask for a folder. Post only rules on the wall; anything with a person's name on it goes through the clerk.

Functions, flows and the event bus: where each one gates

Commerce stacks run logic in three other shapes, usually wired together by an event bus. Each has a different place where access is checked, which is what decides what it may safely see.

Runs whenSeesAccess checked byStateTheme-dependent?Carries over to
Arc in a serviceInside one processOne shared, read-only valueNobody, after it is loadedIn memory until the last handle dropsNoWherever the service runs
Row gate (Xano)Every requestThe rows the caller may readThe query filter on $auth.idThe system of recordNoAny front end that calls the API
Shopify FunctionSynchronously, in cart and checkoutOnly the fields its input query declaresShopify: the declared query and hard limitsNoneNo. It runs in checkout, so any theme, and headless storefronts, get the same logicAny Shopify storefront; not other platforms
Shopify FlowAfter an admin event, asynchronouslyWhole orders, customers and productsStore-wide admin permission; no per-row gateWorkflow historyNo. Its triggers are admin events, not theme codeShopify only
Klaviyo flowAfter an event, a list or segment change, or a dateThe full Klaviyo profileKlaviyo account permissionsProfiles and event historyPartly. Order and customer events come from the integration; Viewed Product and Active on Site come from the Klaviyo app embed in the theme, which must be turned on again after a theme changeAny platform that sends Klaviyo events
Edge Wasm (the Worker)Every request, before anything elseThe request, plus what the gate fetchesThe Worker itself: HMAC, origin allowlist, signed policyNone, or a Durable ObjectNo. It sits in front of the pageAny front end: a Webflow export, a Shopify theme through the App Proxy, a headless app
Event busAfter the event, at least once, in no guaranteed orderWhatever the payload carries; by default the whole objectCloud permissions on the topic, or the HMAC on HTTPSRetained, and replayable on some busesNoAny consumer subscribed to it

Theme dependence, corrected. Shopify Flow isn't theme-dependent: it runs on admin events and keeps working through a theme change. What depends on the theme is anything injected into the page: app embeds, theme app extensions, and the onsite tracking that feeds Klaviyo's browse triggers. Shopify Functions and AEM Edge Functions both carry over, but each only within its own platform: a Function applies to any storefront on that Shopify store, and an AEM Edge Function to anything served through Adobe's CDN. Only the edge Worker carries across platforms, because it sits in front of all of them.

Flows against Functions, in business terms. A Function is the cashier's rulebook at the register: fixed rules, applied in the moment, looking only at the receipt. A flow is the back-office assistant with a master key, doing the follow-up after the sale: tag the customer, send the email, create the task. The edge Worker is the front door, checking everyone before they reach either one. The event bus is the building's announcement system: everyone subscribed hears every message, and some buses keep the recordings.

The same claims and extras rules apply to each:

Consent, events and agent permissions all arrive asynchronously, and an agent or a chatbot acts in milliseconds. Two requirements keep the speed from outrunning the permission: wait for a confirmed consent posture, and give every action an idempotency key. They apply to agentic answer-engine matching (an AI agent matching a shopper's question to the catalog), to agents that act, and equally to chatbot and customer service data collection.

Wait for consent: Consent Mode v2. Google's Consent Mode v2 sets four signals: ad_storage, analytics_storage, ad_user_data and ad_personalization. The page sets them to denied by default, before any tag loads, and wait_for_update holds the tags for a set time while the consent banner, OneTrust or Shopify's Customer Privacy API resolves:

gtag("consent", "default", {
  ad_storage: "denied", analytics_storage: "denied",
  ad_user_data: "denied", ad_personalization: "denied",
  wait_for_update: 500, // milliseconds to wait for the banner before tags run as denied
});

The same wait belongs at the gate. Until a person's posture is confirmed, signed and recorded in the ledger, the Worker treats it as denied, never as granted-until-told-otherwise:

PostureAnswer-engine matchPersonalized match or adsChatbot and customer serviceAgent actions
PendingYes: public catalog data needs no personal consentNoAnswer from public content; collect nothing beyond the questionNothing that changes a record
DeniedYesNoCollect only what the request needs (an order number to answer "where is my order"), under the contract, not consent; no marketing extrasOnly transactional actions the person asked for
Granted, signedYesYes, within the purposes and region grantedCollect profile extras for the purposes granted, with a retention dateWithin the mandate, each with an idempotency key

Key every action. Agents retry, networks drop, and webhooks arrive twice. An action without a key can run twice: two refunds, two customers, two tickets. The caller generates one key per intended action and sends it as an Idempotency-Key header, the convention Stripe's API uses. The Worker runs the action once per key and returns the stored result to any repeat; a repeat with the same key but a different body is refused. Keys live in a Durable Object or Xano, not KV, because two copies of the same request must get one answer.

const key = request.headers.get("Idempotency-Key");
if (!key) return new Response("Idempotency-Key required", { status: 400 });
const id = env.ACTIONS.idFromName(`${subject}:${key}`); // one Durable Object per subject and key
return env.ACTIONS.get(id).fetch(request); // runs once; a repeat gets the stored result

TLS confirms the channel, not the permission. TLS proves the connection is encrypted and that the server is who it claims to be. It says nothing about whether the person consented, or whether the agent on the other end may act. Three separate confirmations are needed:

The agent's request carries all three, plus the idempotency key. The gate checks each and fails closed if any is missing or stale.

Customer service collects the most and is gated the least. A chat transcript gathers names, emails, addresses and order details in free text, often before anyone has looked at consent. The same rules apply:

OneTrust is a session log for one domain

A consent banner such as OneTrust works like a hosted key-value session log: it records what this browser chose on this site, in the OptanonConsent cookie and, with consent logging turned on, in OneTrust's cloud. That suits one website. It doesn't stretch across many:

So horizontal scale (more domains, more storefronts, more agents) can't rest on the banner. Consent has to become part of the data design that the AI services read:

  1. Treat the banner as one input. OneTrust on one site, Shopify's Customer Privacy API in a theme, your own banner on a Webflow export: each origin's allowlist row names its source.
  2. Normalize into the subject's posture. The gate records every choice in Xano with its source, time and region, and projects the result into the subject's Durable Object.
  3. Read posture, not cookies. Pages, agents, feeds and model calls all ask the same posture, so a choice made on one domain applies on every other once the person is known.
  4. Withdrawals apply everywhere. A withdrawal from any source takes effect across all of them on the next request.

The identity layer above already signs JWTs, so the same subject ID can be passed to OneTrust when its own cross-domain profile is wanted. OneTrust then mirrors the posture; it doesn't replace it.

The decision ladders

Two decision ladders. Orchestration, bottom to top: Webflow publish; Cloudflare Worker; Durable Object; Xano as the system of record; hyperscaler or BYOC cluster. AI binding, bottom to top: no model; Workers AI binding; AI Gateway to a hosted model; agent with tools on Xano; dedicated GPUs. Each rung names the condition that earns the next. Rules on every rung: Xano wins disagreements, fail closed when the projection is stale, personal data stays home, data leaves only through the signed gate.

Open the SVG full size (1600 × 770) · Download the image · Download the PDF.

Start every decision at rung 0 and climb only when the climb when condition is true. Each rung adds cost, latency or audit scope, so a decision made higher than it needs to be is paying for something it doesn't use.

Orchestration: where the logic runs

RungRuns onUse it forClimb when…What it gives up
0Webflow publishPages, CMS collections, static output from a CDNA request needs a decision, not a pagePer-request logic
1Cloudflare WorkerRouting, caching, auth and consent checks, the gateThe decision needs memory of one subjectDurable state
2Durable ObjectPer-subject posture, rate limits, coordinationThe action must change the recordAuthority: it is a projection
3Xano, the system of recordBusiness rules, Postgres, the append-only ledger, one instance per regionOne instance can't meet residency or loadEdge latency
4Hyperscaler or BYOC clusterXano Enterprise in your own AWS, Azure or Google Cloud account; warehouses; batchA contract or a regulator requires itSimplicity: you now run accounts, networks and licences

AI binding: where the model runs, and what it may touch

RungRuns onUse it forClimb when…What it gives up
0No modelDeterministic rules in a Worker or a Xano functionThe input is language, images or a fuzzy matchNothing; start here
1Workers AI binding (env.AI)Small models at the edge: classify, embed, moderate, translateThe task needs a frontier modelModel size
2AI Gateway to a hosted modelClaude, Gemini or GPT behind one gateway with logs, caching and rate limitsThe model must read or write recordsData stays outside the model, so it also has no context
3Agent with tools on XanoTool calls to Xano endpoints, each write passing the gate under a mandateHosted models fail on cost, privacy or tuningA smaller audit scope: the model now acts
4Dedicated GPUsSelf-hosting, fine-tuning or training on neocloud or hyperscaler GPUsThe model weights are the productOperational simplicity and a variable bill

Rung 4 has a sideways exit. When the work is small and parallel rather than a large model, portable GPU bytecode can run on the user's device or your own server instead of a rented cluster. That keeps the data where it is, at the cost of the maturity limits above.

The ladders are independent. A Webflow page (orchestration rung 0) can call an edge classifier (AI rung 1), and a Xano agent (AI rung 3) still answers to the gate at orchestration rung 1. What they share is the four rules in the band under the diagram.

Media, 3D render and the GPU

Media, 3D render and the GPU: where each step runs and what protects it. Three workloads across five places, from closest to the person to closest to the GPU. Images: client preview on the device; Cloudflare Images transforms at the edge; a private original in R2 or S3, with Xano holding the record and key; batch processing in C with libvips or ImageMagick pods on Kubernetes; Adobe Firefly Services as a hosted API. 3D game render: real-time render on the player's GPU through WebGPU or native code; glTF and KTX2 asset delivery from the edge; an asset catalog in the system of record; build and bake jobs on Kubernetes with the GPU Operator; offline or streamed rendering on CoreWeave- or OCI-class GPUs. AI media: on-device models; Workers AI at the edge; consent and rights in the system of record; self-hosted models on GPU node pools; hosted generative APIs. Protection rules: base64 is not protection; Kubernetes Secrets are only base64 by default, so enable encryption at rest; lock down ImageMagick with policy.xml; strip EXIF and use signed, expiring URLs.

Open the SVG full size (1600 × 774) · Download the image · Download the PDF.

Media is where the spectrum costs the most, because the payloads are large and they often carry people: faces, voices, locations. Read the grid left to right. The further right a step runs, the further the file travels and the more systems hold a copy. The heavy boxes on the left are the only places where the media never leaves the person's device.

Media ladder

RungRuns onUse it forClimb when…What it gives up
0The devicePreviews, real-time 3D on the player's GPU, small on-device modelsThe work needs the original, or more GPU than the device hasNothing leaves, so nothing to protect in transit
1The edgeResize, format and crop on request; cached asset delivery; Workers AI captions and moderationThe job is a batch, or needs a library the edge can't runNo GPU for your own code; CPU time limits
2Kubernetes, CPU podslibvips or ImageMagick batches in C; texture compression; bakesThe work is parallel enough to need a GPUYou now run the cluster and its secrets
3Kubernetes, GPU podsLightmaps, upscaling, self-hosted image models, through the NVIDIA GPU OperatorSteady demand beats a rented cluster, or the GPUs you need aren't availableMedia stays in your account, but you buy the capacity
4Hosted GPU or APIRender farms and pixel streaming on CoreWeave- or OCI-class GPUs; Adobe Firefly ServicesOnly when the scale or the feature can't be had lower downThe file leaves your boundary under someone else's retention terms

Four terms the grid relies on

3D game render. Real-time rendering belongs on the player's GPU. WebGPU in the browser takes WGSL shaders, and native engines take SPIR-V, which is where the Rust-to-bytecode path from the opening section is most useful. The edge delivers assets (glTF meshes, KTX2 textures) and never renders. The cluster bakes and compresses. Rented GPUs earn their place only for offline render farms, or for pixel streaming, when the player's device can't render the scene at all. The asset catalog in the system of record decides who receives which asset version under which licence, through the same gate as any other data.

The 3D viewport: load out of sight, show only finished frames

A 3D viewport in a page works like the gate: the data is prepared out of sight and released to the visible layer only when it is complete. The person sees finished frames, never the work.

Step 1: compress for the trip. Three kinds of compression save bytes in three different places:

LayerStandardWhat it savesStill compressed on the GPU?
Mesh geometryDraco (Google's library) or meshopt, carried inside glTF through Khronos extensionsDownload size, often 5 to 10 times smallerNo. It is decoded to plain vertex buffers first
TexturesKTX2 with Basis Universal (Khronos)Download size and GPU memoryYes. It is converted into formats the GPU reads directly (BC7, ASTC, ETC2)
The programSPIR-V or WGSLPortabilityIt is compiled, not compressed

Draco helps the download, not the drawing: the GPU never sees it. It is also lossy, since it rounds coordinates to fewer bits. That is fine for showing a product or a room, and wrong for engineering data. Use it on the display copy of a CAD model, and keep the STEP or native CAD file as the system of record.

Step 2: load out of sight, show finished. A 3D frame passes through five stages, and only the last is visible:

  1. Fetch the glTF, Draco and KTX2 files from the edge.
  2. Decode off the main thread: Three.js's DRACOLoader runs a Wasm decoder in a Web Worker, so the page keeps responding.
  3. Upload the decoded buffers and textures to the GPU.
  4. Draw into a back buffer, an off-screen frame. With OffscreenCanvas, a worker can do this away from the page's main thread.
  5. Present. On the display's beat, the finished back buffer swaps to the front and the compositor layers it into the page.

Shadow DOM is the boundary, not the backstage. A Shadow DOM is a web component's private subtree: page styles and scripts can't rearrange what's inside by accident, which is how Google's <model-viewer> packages its canvas. It does not hide or delay anything; a component's shadow content renders like any other. The off-screen staging is the back buffer and the worker. Two cautions: a closed shadow root is encapsulation, not security, since other scripts on the page can still reach it; and what is hidden from the eye is not hidden from a machine reader. Shadow content is in the accessibility tree, and an agent reading the page reads it.

The same pattern in a browser, an app and an operating system. Code computes what the next frame should be; animation is the frames changing over time; the frame rate is how often the visible layer is allowed to change.

BrowserNative appOperating system
The sealed componentA web component with a Shadow DOMA view or scene with its own stateA process, isolated from other processes
Off-screen workWeb Worker, OffscreenCanvas, the WebGL or WebGPU back bufferBackground and render threads, the swap chain's back buffer (Metal, Vulkan, DirectX)Each app's window drawn to its own off-screen surface
The beat that releases a framerequestAnimationFrameCADisplayLink on iOS, Choreographer on AndroidThe compositor (Windows DWM, macOS WindowServer, a Wayland compositor) on the display's refresh
The visible layerThe page's composited layersThe app's windowThe screen

The frame rate sets the budget. At 60 Hz each frame has 16.7 ms; at 120 Hz, 8.3 ms. In a browser, JavaScript, layout and drawing all share that budget on the main thread, so decoding a large mesh there misses frames, which the person sees as stutter. Moving the work to a worker, compressing what crosses the network, and drawing into a back buffer keeps the visible layer on the beat.

In business terms: a theater. The crew sets the next scene backstage (the worker and the back buffer) while the audience watches the current one. The stage manager calls the change on the beat (the display refresh), and the curtain opens only on a finished set. A Shadow DOM is a set piece built by its own crew, which the rest of the production can't rearrange. It isn't locked against the audience. The frame rate is how many scene changes the show promises each second.

The same rule runs through this article: prepare and check out of sight, release only finished and permitted work to the visible layer, and keep the budget for each release small enough to hit.

Strip before egress

The media grid's last box is one rule: clean the file before it leaves, and hand it out only through a link that expires. Egress is data leaving your system: a photo sent to a customer, a partner, a CDN, a model or a social platform.

Remove EXIF GPS and device IDs. Phones and cameras write hidden metadata, EXIF, into every photo. It doesn't show in the picture, but anyone can read it from the file:

Stripping deletes those fields and keeps the image. A product photo shot at someone's house and published unstripped publishes the house's location to everyone who downloads it.

Signed, expiring URLs only. Instead of a permanent public link such as …/photos/1234.jpg, the system issues a link with a signature and an expiry built in: this one file, for the next few minutes. Changing any part of the link breaks the signature, and after the expiry it stops working. A link that is forwarded, scraped or left in a log goes dead on its own, and no one can guess the next file's address.

The two halves cover different things. Stripping controls what is in the file when it leaves; signed URLs control who can fetch it, and for how long. The original stays in the private bucket, as the system of record; only a cleaned copy travels, briefly.

Two cautions:

Why a GPU neocloud is too big to bind tightly

Two terms first, because the contrast is the point:

What it isIn business termsWhat you send it
MicroserviceA small service with one job and its own API, owned by one team: price a cart, check consent, resize an imageA specialist department with a service window: you hand over one form, it hands back one answer, and it only ever sees that formA request: small, labeled, checked by the gate, gone when it is answered
NeocloudA cloud provider that mostly rents GPUs for AI, such as CoreWeave, Lambda, Crusoe or Nebius, as opposed to a hyperscaler that sells everythingA heavy-equipment yard: you rent its machines by the hour or on a long contract, and bring your materials to its siteYour materials: whole datasets, long-running jobs, and the credentials to reach them

Everything in this article works because each request is small, short-lived, labeled and checked. A microservice fits that shape. A neocloud dependency breaks all four:

  1. It needs bulk data, not a request. Training and batch inference need whole datasets copied into the provider's storage ahead of time. A dataset can't carry a per-request consent label, and once copied it can't be recalled.
  2. It needs standing access, not a per-call grant. Jobs run for hours or weeks with credentials to datasets, checkpoints and model registries. In stack-and-heap terms, everything about it is heap.
  3. Your perimeter doesn't reach it. SCPs and RCPs, VPC Service Controls and Azure Policy govern your own cloud accounts. A neocloud has its own identity system, control plane, administrators and logs; you rely on its operations and a contract.
  4. Its fast path skips the usual controls. GPU clusters link machines with RDMA networking, InfiniBand or RoCE, to move data between GPUs outside the normal network stack. Network policy, service-mesh mTLS and egress gateways are built for ordinary IP traffic.
  5. Its software is large, native and privileged. Drivers, CUDA libraries and container runtimes are C and C++ running with high privilege. In CVE-2024-0132, a malicious container image could escape the NVIDIA Container Toolkit to the host; the first patch was incomplete and a second CVE followed, fixed in version 1.17.4. On a shared GPU cluster, an escape crosses tenants.
  6. Its outputs are derived data without a clean owner. Checkpoints and weights can memorize training data, and embeddings can be inverted to recover text. A deletion request can't reach inside trained weights.
  7. Its economics keep data there. Capacity is reserved by long contract, and egress fees discourage moving data back out.

The NVIDIA container escape, explained. A CVE (Common Vulnerabilities and Exposures) is a public ID for a known security flaw, so everyone refers to the same bug. A container is a sealed box a program runs in, kept away from the machine underneath and from everything else on it; an escape is the program breaking out of the box to reach the host.

In business terms: each customer's crate is supposed to stay locked in its own bay of the equipment yard. A clerk checks each crate before hooking it up to the shared machinery. A crate built to change its contents right after the check walks out of its bay holding keys to the yard: the machines, and every other customer's crates. On a shared GPU cluster that is the risk: one escape reaches other tenants' data and credentials, below anything a gate can bind. Run toolkit 1.17.4 or later, keep untrusted images off shared GPU hosts, and put sensitive work on dedicated nodes or virtual machines, so an escape reaches only your own host.

So the answer isn't a tighter leash on the neocloud. It is sending it less, later and already reduced:

A microservice is bound by the gate on every call. A neocloud is bound by what you choose to send it, which is why it belongs on the top rung and only for the work nothing lower can do.

The castle vault and the pockets of coins. A neocloud is a castle vault: everything in one place, behind one huge wall that must never fail, trusting anyone already inside. The Webflow, Cloudflare and Xano design is coins in small pockets, carried between people who know each other.

Castle vault (a neocloud)Pockets of coins (Webflow, Cloudflare, Xano)
What's heldEverything, in one placeA little in each place
PerimeterOne huge wall that must never failMany small walls, each easy to see around
One breach costsThe whole vaultOne pocket
Who is trustedAnyone inside the wallsOnly parties who know each other: signed labels, allowlists, mutual TLS, the gate
Built forBulk storage and heavy machinerySmall amounts moving quickly between known parties

Two things keep the image honest. Smaller is not risk-free: each pocket is still someone's, since Cloudflare's edge sees traffic in plaintext, Webflow serves the page and Xano holds the record. The risk is bounded, not gone. And there is still a vault, only a smaller one kept close to home: one Xano instance per region holding what that region needs, while the coins in circulation are the reduced, signed, short-lived payloads. Many pockets mean many small configurations to keep consistent, which is why the allowlists and rules live in one versioned policy bundle instead of being set up place by place.

A neocloud is a vault that is only as safe as its biggest wall. This design keeps small vaults near home and moves small, signed pockets of coins between parties who know each other.

Impact on enterprise infrastructure

ConcernHyperscaler-centredDistributed, with a system-of-record gateWhat changes for the enterprise
Data residencyChosen by region; replication is a configuration you have to auditOne Xano instance per jurisdiction; only routing keys cross bordersResidency becomes a deployment fact, not a policy document
LatencyGood near the region, worse elsewhereDecisions at the edge; writes travel to the home regionThe hot path and the write path are budgeted separately
ConsistencyStrong inside a regionThe ledger is strong; edge projections are eventual and fail closedStale data is denied rather than served
Egress costCharged per GB leaving the cloudR2 has no egress fees; gated payloads are smallMoving data out costs less and is easier to justify
Failure modeRegional outage stops everything in the regionA region outage stops its subjects; the rest keep workingOutages are scoped per jurisdiction
Audit scopeEverything in the account is in scopeGate, ledger and signing keys are the audited coreA SOC 2 boundary you can draw on one page
AI exposureModels sit next to all the dataModels reach records only through tools behind the gatePrompt injection can't exfiltrate what the gate won't release
LicensingUsage-based, committed spendXano licence per instance (Enterprise); Workers usageEach new jurisdiction carries a fixed cost
SkillsKubernetes, IAM, networking teamsWorkers, Xano and gate design; little cluster operationPlatform headcount moves from operating clusters to writing policy
Media and renderOriginals, transforms and render farms sit in one account next to everything elsePreviews and real-time 3D on the device; transforms at the edge; originals private; GPUs rented per jobFewer copies of large files that contain people, and a GPU bill that follows the work
Vendor concentrationOne provider holds compute, data and AIEdge, system of record and model provider are separateNo single contract can switch the whole system off

The trade is real. The distributed pattern only pays off if the gate is the single way out. A team that adds a direct database export "just for analytics" has rebuilt the hyperscaler's blast radius without its tooling.

Decision checklist

Sources