Deploy in Seconds
A TKO app can be as simple as a single HTML file. No bundler, no server runtime, no build step — just upload to any static hosting provider and you’re live.
The simplest deploy
Section titled “The simplest deploy”Save one of these as index.html:
<!doctype html><html lang="en"> <head> <meta charset="utf-8" /> <title>Hello, TKO</title> <script type="importmap"> { "imports": { "@tko/build.reference": "https://esm.sh/@tko/build.reference" } } </script> </head> <body> <!-- Reusable markup. Native ko- bindings; no data-bind needed. --> <template id="ko-greeting-template"> <input ko-textInput="name" /> <p>Hello, <strong ko-text="name"></strong>.</p> </template>
<!-- Mount one or many — each instance gets its own state. --> <ko-greeting></ko-greeting>
<script type="module"> import ko from '@tko/build.reference'
// Component class — extends ko.Component, auto-registers as <ko-greeting> // (kebab-case derived from class name). class KoGreeting extends ko.Component { // Where TKO finds the markup for this component. static get template () { return { element: 'ko-greeting-template' } }
name = ko.observable('TKO') } KoGreeting.register()
ko.applyBindings({}, document.body) </script> </body></html>Native ko- bindings. Reusable component with a <template> for markup and a viewModel for state. Drop <ko-greeting></ko-greeting> anywhere, any number of times — each instance gets its own observable. No compile step. Ship it.
<!doctype html><html lang="en"> <head> <meta charset="utf-8" /> <title>Hello, TKO</title> <script type="importmap"> { "imports": { "@tko/build.reference": "https://esm.sh/@tko/build.reference", "esbuild-wasm": "https://esm.sh/esbuild-wasm@0.27.4/esm/browser.min.js" } } </script> </head> <body> <!-- Reusable markup. Native ko- bindings; no data-bind needed. --> <template id="ko-counter-template"> <h1>Count: <span ko-text="count"></span></h1> <button ko-click="increment">Increment</button> </template>
<!-- Two independent counters — shared template, separate state. --> <ko-counter></ko-counter> <ko-counter></ko-counter>
<script id="app" type="text/tsx"> import ko from '@tko/build.reference'
// Component class — extends ko.Component, auto-registers as <ko-counter> // (kebab-case derived from class name). class KoCounter extends ko.Component { // Where TKO finds the markup for this component. static get template () { return { element: 'ko-counter-template' } }
count = ko.observable(0) increment = () => this.count(this.count() + 1) } KoCounter.register()
ko.applyBindings({}, document.body) </script>
<script type="module"> import * as esbuild from 'esbuild-wasm'
await esbuild.initialize({ wasmURL: 'https://esm.sh/esbuild-wasm@0.27.4/esbuild.wasm', })
const { code } = await esbuild.transform( document.getElementById('app').textContent, { loader: 'tsx', jsx: 'transform', jsxFactory: 'ko.jsx.createElement', jsxFragment: 'ko.jsx.Fragment', }, )
const url = URL.createObjectURL(new Blob([code], { type: 'text/javascript' })) await import(url) </script> </body></html>Templates and code live apart. The <template id="counter-template"> is reusable markup — drop <ko-counter></ko-counter> as many times as you like, each instance gets its own Counter viewModel. Template content is plain HTML with ko- bindings; TKO doesn’t care whether it came from JSX or HTML.
Your TypeScript lives in <script type="text/tsx"> — the browser ignores unknown script types so the source survives verbatim. A tiny <script type="module"> bootstrap loads esbuild-wasm from the CDN, transforms the source, wraps it in a blob URL, and import()s it as a real ES module. import ko from '@tko/build.reference' resolves through the import map just like any module.
Trade-off: first load fetches ~3MB of esbuild-wasm (cached after). Great for demos, internal tools, and apps where time-to-compile matters less than zero-build deployment.
Try it first in the ESM playground — same code, live editor. Use the arrow icon on the code block to open either variant.
GitHub Pages
Section titled “GitHub Pages”Free, deploys from a git push.
- Create a repo and add your
index.html - Go to Settings → Pages → Source and select your branch
- Your site is live at
https://username.github.io/repo/
Or use the gh CLI:
gh repo create my-app --public --clone# add index.htmlgit add index.html && git commit -m "init" && git pushgh browse --settings # enable Pages under Settings → PagesCloudflare Pages
Section titled “Cloudflare Pages”Free tier, global CDN, automatic HTTPS.
- Push your files to a GitHub or GitLab repo
- Connect the repo at dash.cloudflare.com → Pages → Create
- No build command needed — just set the output directory to
/(or wherever your HTML lives)
Or deploy directly from the CLI:
npx wrangler pages deploy . --project-name my-appGoogle Cloud Storage
Section titled “Google Cloud Storage”Good for projects already on GCP.
gcloud storage buckets create gs://my-app.example.comgcloud storage buckets update gs://my-app.example.com --web-main-page-suffix=index.htmlgcloud storage cp index.html gs://my-app.example.com/Add a load balancer or use Firebase Hosting for automatic HTTPS and CDN.
Firebase Hosting
Section titled “Firebase Hosting”Free tier, automatic HTTPS, global CDN.
npm install -g firebase-toolsfirebase init hosting # select your project, set public dir to "."firebase deployNetlify
Section titled “Netlify”Free tier, drag-and-drop or git-based deploys.
- Go to app.netlify.com/drop
- Drag your project folder onto the page
- Live in seconds
Or via CLI:
npx netlify-cli deploy --dir . --prodWhy this works
Section titled “Why this works”TKO loads over the browser’s native ES module loader via an import map. No server-side rendering, no Node.js, no build artifacts. The entire deploy is one file.
As your app grows you can add a bundler, but you don’t have to. Many production TKO apps are a handful of HTML files and a CSS stylesheet.