Tibi

Deploy

Two paths, depending on whether your app needs the SSR runtime. The static path drops onto Cloudflare Pages (or any static host) with one command. The container path runs tibi serve anywhere Docker runs, fronted by your platform of choice.

Pick your path

  • Static (all routes are .tibi files with no per-request dynamism, or dynamic routes whose params resolve at build time) → Cloudflare Pages via tibi deploy cf-pages.
  • SSR (live channels via __channels, cron jobs, OAuth gating, dynamic params resolved per request) → container deploy using Dockerfile.tibi-serve.
  • Self-host (your own VM with a reverse proxy) → just run tibi serve.

Not sure which? Start static. If your build summary shows - skipped /something (dynamic — no result.__params) for routes you actually need live, that's the signal to flip to the container path.

Static → Cloudflare Pages

Built into the CLI:

$ tibi deploy cf-pages --project my-app

That runs tibi build + wrangler pages deploy dist/ --project-name my-app. Wrangler owns CF authentication via its own wrangler login flow — Tibi never touches your tokens. Install wrangler first if you don't have it:

$ npm install -g wrangler $ wrangler login

You can drop the --project flag on every invocation by setting it in .tibi-config:

[deploy.cloudflare_pages] project = "my-app" branch = "preview" # optional — wrangler infers from git when omitted

Useful flags:

  • --branch <name> — passes --branch through to wrangler so you can deploy preview environments.
  • --skip-build — skip the tibi build step (handy when CI built dist/ in a previous stage).
  • --dist <path> — override the upload directory (default dist).

What tibi build emits

Every .tibi page renders to a static HTML file; the SSG also emits a few Cloudflare-Pages-native files:

  • dist/<path>/index.html — one per static route, plus one per __params entry for dynamic routes (see below).
  • dist/404.html — rendered from pages/_404.tibi if present. Pages serves this for any path that doesn't match a route.
  • dist/_redirects — one line per [[redirects]] block in .tibi-config, in the format Pages expects (<from> <to> <status>). Author order is preserved; top-to-bottom first-match.

The build summary calls out every emission so you can verify what shipped:

$ tibi build built 8 page(s) into dist + / -> dist/index.html (10 234 bytes) + /about -> dist/about/index.html (3 102 bytes) … + _redirects -> dist/_redirects + 404.html -> dist/404.html

Pre-rendering dynamic routes

A page whose URL pattern contains a dynamic segment (/blog/[slug].tibi → /blog/{slug}) can opt into build-time rendering by exporting result.__params from its frontmatter — an array of objects whose keys match the dynamic segments:

---js const fs = require('fs'); const posts = JSON.parse(fs.readFileSync('data/posts.json')); result = { __params: posts.map(p => ({ slug: p.slug })), }; --- <template> <article> <h1>{param_slug}</h1> </article> </template>

The SSG iterates each __params entry, substitutes the keys into the URL pattern, and writes one HTML file per entry (dist/blog/<slug>/index.html). Catchall patterns (/files/{*rest}) accept path-shaped values (rest: "a/b/c" → dist/files/a/b/c/index.html).

Pages with no __params (or an empty array) keep the pre-1.0.38 behaviour and get skipped — the build summary tells you which routes didn't ship, with a reason.

The data source is whatever the frontmatter language can reach: a JSON file in data/, a directory walk, even a build-time API call. Anything declarative can be expressed as a constant __params; we deliberately don't ship a separate [[routes]] params config block because the frontmatter form is strictly more powerful and keeps the data next to the page that consumes it.

Other static hosts (Netlify / Vercel / S3 / nginx)

Run tibi build in CI (or locally) and upload dist/. The bundled package.json has a preinstall hook that curl-pipes the Tibi installer, so every PaaS that runs npm install before the build gets the binary for free. _redirects and 404.html follow conventions Cloudflare Pages and Netlify both honour; Vercel/S3 ignore them but the rest of the site still serves correctly.

SSR → container deploy

Apps that need the SSR runtime (live __channels, [[cron]] jobs, OAuth gating via strands-auth, per-request dynamic params) need a long-running process. Tibi ships a reusable Dockerfile at the repo root:

$ docker build -f Dockerfile.tibi-serve -t my-app . $ docker run -p 9017:9017 my-app

The same Dockerfile works on:

  • Cloudflare Containers (beta) — point the project at the built image; Cloudflare's edge fronts it. The natural pairing with Cloudflare Pages when one of your routes needs SSR but the rest is static.
  • Fly.io — fly launch, point at the Dockerfile, done.
  • Railway / Render — same pattern; both auto-detect Dockerfile.tibi-serve if you set it as the dockerfile path.
  • Coolify / Dokku / your own Docker host — works unchanged.

Self-host

Any Linux box with the tibi binary on PATH + tibi serve behind a reverse proxy (nginx / Caddy / Traefik). The bundled installer drops the binary onto $HOME/.local/bin:

$ curl https://tibi.fi/install.sh | sh $ cd my-app && tibi serve

Same binary the container deploy ships. The Coolify docker-compose used by tibi.fi itself is a working example of the SSR-behind-proxy pattern; the docker-compose.yml at the repo root is the canonical reference.

Plugins in dist/

If your project uses Rust plugins, tibi build pre-compiles them into dist/plugins-bin/ for runtime hosts that don't have rustc. Those binaries ride along to Cloudflare Pages too but never execute there — Pages is a static host. Harmless if a little wasteful in image size; no action needed.