Loopwise Pages
How Loopwise Pages deploys work — the deployment lifecycle, preview surfaces, why assets redirect to *.loopwise.page, how a page's route wins over built-in routes, propagation timing, and the real size limits.
Loopwise Pages lets you deploy a directory of static files (HTML/CSS/JS, plus a Vite build for SPAs) to a path on an organization's storefront — a landing page, a microsite, or a full single-page app. This page is the contract for how a deployment behaves: how it goes live, where you can preview it, why asset requests redirect off-origin, what wins when a page's path collides with a built-in route, how long changes take to propagate, and the size limits that are actually enforced.
You deploy either with the Loopwise CLI (loopwise pages push) or with the
deploy_static_page / prepare_deployment MCP tools. The lifecycle below is the
same for both, but note the size limits differ between the single-shot and
multiphase paths (see Size limits).
Deployment lifecycle
A deployment is an immutable snapshot. Uploading a new bundle never changes
what visitors see on its own — but how it goes live depends on is_preview. A
non-preview deployment (the CLI and MCP default) goes live on completion, with
no separate promote_deployment step. A preview deployment stays hidden behind the
current live one until you promote it.
- Prepare / push — the CLI's
pages pushwalks the directory and uploads it; the multi-phase MCP flow isprepare_deployment→upload_asset× N →complete_deployment.deploy_static_pagecollapses all of that into one call for small bundles. - Preview vs live — deploying with
--preview(CLI) oris_preview: true(MCP) marks the deployment as a preview. The page keeps serving its previous live deployment at itspath_prefix, so a preview alone never changes what visitors see. - Promote —
promote_deploymentcuts live traffic over to a preview deployment. Rollback is the same operation pointed at an earlier deployment — each one is a complete snapshot. The switch itself is a single pointer update, but it is not visible everywhere at once: allow for the propagation window below.
Loopwise Pages is available to every organization — there is no plan gating on deploying or serving a page.
Preview surfaces
A deployment can be reached from two different hosts, and they behave differently. Pick the one that matches what you're testing.
| Surface | URL shape | Auth / cart / member content | Cache |
|---|---|---|---|
| Tenant origin | <tenant-origin>/_preview/<scriptName>/?pt=… | Full — same-origin to the storefront BFF, so login, cart, and member-gated reads work | Not cached (no-store) |
| Loopwise origin | https://<scriptName>.loopwise.page | Anonymous reads only — cross-origin to the tenant | Edge-cacheable |
The tenant-origin preview carries a signed pt token and is the URL you share
for QA. The MCP deploy tools return it as preview_url, with the
loopwise.page cross-origin fallback as preview_url_loopwise_origin. Use the
tenant-origin surface whenever you need to test anything that depends on the
member session (login, cart, gated content); use the loopwise.page surface for
a shareable, anonymous preview before the organization has its own domain.
Both preview surfaces render the bundle without the storefront header/footer
— that is true for embedded pages too. (The tenant-origin preview also injects a
<base href="/_preview/<scriptName>/"> into HTML that has no author-provided
<base>, so relative asset paths resolve — it is not a byte-for-byte copy.) A
preview without header/footer is not evidence that embedded rendering is broken;
an embedded page only renders inside the organization's chrome once a non-preview
deployment is live.
Why assets redirect to *.loopwise.page
Every tenant host — *.kaik.io, *.myteachify.com, and a school's own custom
domain — is served today through the Next.js storefront path. The asset 302
redirect and the root-page /-only match described here apply on all of them.
On a live deployment served over the Next.js storefront path (the shared
*.myteachify.com / *.kaik.io origin, and any custom domain), the document is
served same-origin, and requests for static asset files — those with a
recognized asset extension (.css, .js, images, fonts, etc.) — 302-redirect
to https://<scriptName>.loopwise.page/…. The redirect only changes where the
asset bytes come from (the Cloudflare CDN); it is a 302 (not 301) because
the scriptName changes on every deployment, and it carries
Cache-Control: no-store so no intermediary pins a redirect to a now-stale
scriptName. Only extensionless paths — including the routes that serve your
HTML documents (for example / or /about) — and paths whose extension is not
on the static-asset list (for example .csv) are served same-origin. A path with
a recognized extension is redirected regardless of what it holds: .html, .htm,
.json, .xml, and .txt are on the static-asset list too, so navigating
directly to /about.html lands on loopwise.page and makes the document
itself cross-origin. Link between pages with extensionless routes to keep the
document same-origin.
The redirect does not change a script's execution origin. A <script> on a
live tenant-origin document executes in that document's origin regardless of
where its bytes were fetched from, so a root-relative fetch('/…') resolves
against the tenant host and carries the member session — authenticated calls work
on live pages with no special handling.
The cross-origin caveat applies only to the loopwise.page document surface:
if you open the page itself at https://<scriptName>.loopwise.page, the document
origin is loopwise.page, so calls back to the tenant are cross-origin and
anonymous. The tenant-origin preview (/_preview/<scriptName>/…) keeps the
document same-origin (and proxies its relative assets with no-store), which is
why it is the surface for testing authenticated flows.
Use the loopwise.page host only for anonymous, shareable previews. For
anything that depends on the member session (login, cart, gated content), keep
the document on the tenant origin — the live URL or the tenant-origin
preview (/_preview/<scriptName>/) — and use the
Loopwise SDK for API
calls. Do not open the document on loopwise.page and expect an authenticated
session.
Route priority: what wins over built-in routes
A page mounted at a path_prefix shadows the storefront's built-in route at
that path. Three rules decide who serves a request:
-
Tenant URL redirects run first — except on origin-bound paths. For requests the storefront middleware handles, a configured redirect normally takes precedence over both a deployed page and a built-in route. The exception is a set of machine-readable, origin-bound paths that redirects never rewrite:
/mcpand/mcp/…,/oauth/…,/.well-known/…,/skills/…,/account/graphql, and the discovery files/llms.txt,/llms-full.txt,/openapi.json,/auth.md,/pricing.md./api/…is exempt for a different reason: the middleware matcher excludes it (api(?:/|$)), so those requests never enterfindAndApplyRedirectat all — a redirect can never win for an/api/…path. -
A deployed page shadows built-in routes — but can only mount outside the reserved prefixes. You cannot create a page under a reserved system prefix: the backend rejects the mount at write time with "is reserved by the platform and cannot be used" (
Page#path_prefix_not_reserved), so there is no way to end up with a silently unreachable mount. The reserved prefixes are:/api,/admin,/oauth,/auth,/cable,/graphql,/assets,/locales,/mcp,/account,/developer-portal,/developers,/_next,/_preview,/lw-preview,/_lw,/lw-pages,/.well-known -
The root path matches only
/. A page mounted atpath_prefix: "/"replaces the homepage. It matches only/, so built-in routes such as/coursesand/checkoutkeep working and are never captured by a root page.
So a page mounted at /faq serves /faq in place of any built-in /faq, but a
request to a reserved prefix such as /api/... or /oauth/... always reaches the
platform. A path that is not reserved (for example /login) can be mounted by a
page and will then shadow the built-in route — only the root page is restricted to
an exact / match.
The reserved list lives in two places that are kept in step but do not
mirror exactly: the backend (Page::RESERVED_PATH_PREFIXES) is authoritative
for what you can mount, while the frontend
(lib/loopwise-pages/excluded-prefixes.ts) omits /_lw on purpose — that
namespace is handled by the frontend's mount-independent asset branch instead
of being skipped.
Shadowing also moves the agent-facing surfaces
Shadowing a built-in route swaps out more than the HTML page. When a page mounts
at, say, /faq, the platform stops treating that path as its own:
- The markdown twin no longer serves built-in content. A request for
/faq.md307s to your deployed/faq; a request to/faqwithAccept: text/markdownis served straight from your deployment by the Loopwise route match (no redirect). Either way an agent lands on the same page humans see, not the built-in FAQ markdown. llms.txtdrops the built-in twin. The per-tenantllms.txt/llms-full.txtstop advertising the shadowed path's built-in.mdtwin, because it would only redirect back onto your page.- Built-in structured data is gone. The built-in FAQ page emits
FAQPageJSON-LD; a deployed page renders none unless you include it. If you rely on rich results or want agents to read structured data, embed your own JSON-LD in the page's HTML — a standalone (full-page) deployment's<script type="application/ld+json">is served through untouched.
The twins reconciled in llms.txt are the concrete built-ins (/, /faq,
/courses, /lecturers, /search) plus any per-slug family whose fixed
ancestor you mount — mounting /courses also drops /courses/:slug, because the
built-in course-detail markdown is no longer served under that prefix. What a
.md request does depends on where it sits relative to the mount:
- The mount's own twin (
/faq.mdfor a/faqmount, or/courses/foo.mdwhen/courses/foois itself the mounted leaf)307s to the HTML path. - Anything inside a mounted subtree (
/courses/foo.mdunder a/coursesmount,/faq/index.mdunder a/faqmount) is served straight from your deployment like every other path you own there, including.mdfiles you deploy yourself — no built-in twin is involved, so there is nothing to redirect.
What llms.txt can't decide for a single-slug mount is whether to drop the whole
/courses/:slug pattern, so it keeps the pattern unless the fixed ancestor
itself is mounted.
Propagation timing
Promoting a deployment (or deploying a non-preview) is not instantaneous
end-to-end. The route table and the CDN each hold the previous state briefly. The
edge dispatch worker starts routing within ~30 seconds (Cloudflare KV), but a
custom production domain is served through a CloudFront-cached lookup. Promote and
rollback actively invalidate that lookup entry, so it normally refreshes within
~1–2 minutes; if the invalidation is delayed or retried (transient CloudFront
throttling backs off progressively, up to ~10 minutes), the previous route can
stay visible for up to the CloudFront TTL (~10 minutes). Allow that longer
window before judging the result, and verify from a fresh or
incognito session so you're not seeing a locally cached response. When verifying
a live deploy, request <tenant-origin><path_prefix> directly (for example with
curl).
Size limits
The limit that applies depends on how you deploy. Both paths cap a deployment at
10,000 files, cap any single file at 25 MiB (Cloudflare Workers Assets
rejects larger files on every path), and require an index.html at the bundle
root.
| Deploy path | Limit |
|---|---|
Single-shot — CLI pages push, MCP deploy_static_page | 100 MB request body, 25 MiB per file. The whole bundle is sent as one JSON request; binary files are base64-encoded and count ~33% larger than their raw size. |
Multiphase — MCP prepare_deployment → upload_asset → complete_deployment | 100 MB raw total, 25 MiB per file (raw staged bytes, no base64 inflation). |
For a large binary-heavy bundle, prefer the multiphase flow — the single-shot 100 MB is measured on the base64-expanded request, so its effective raw capacity is roughly 75 MB.
Images: the MCP tools expose an image pipeline (upload_image →
confirm_upload) that returns a CDN URL — reference that URL in your HTML
instead of bundling the bytes, which keeps the request small and applies
metadata stripping. The CLI has no image command, so pages push bundles image
files directly; keep them within the single-shot request-body limit above.
Custom CSS & JavaScript
The contract for customizing a Loopwise storefront — stable DOM selectors, lifecycle events, SCSS semantics, the @import trap, how custom JavaScript is processed, and cache propagation.
Enterprise SSO Integration
A comprehensive guide for integrating your organization's identity provider with Loopwise using OAuth 2.0, allowing users to log in with their existing enterprise credentials.