Teitunnel

Guides

Project files

Declare a project's routes, shares, Snapshots and local domains in a teitunnel.yml checked into its repository, and bring them up with teitunnel up or one click in the app: previewed first, idempotent, never holding a secret.

A project file is a teitunnel.yml at the root of a repository. It says what the project needs from Teitunnel: its routes, the shares to start while you work on it, the Snapshots it publishes and its local domains. Check it into git and everyone on the team gets the same setup with one command, each with their own addresses.

Applying it goes through the same plan as every other change: you see exactly what will change in Cloudflare and on this computer before anything happens. Applying it again changes nothing ("Nothing to change."), so it's safe to run every time you start work, and in CI.

Projects: the shop project's teitunnel.yml with a warning and what it declares (routes, a share, a Snapshot and a local domain), each Applied or Missing, and Apply

An example

# yaml-language-server: $schema=https://teitunnel.teispace.com/schema/teitunnel.v1.json
version: 1
project: shop            # default: the name in package.json, Cargo.toml… or the folder's
account: Teispace        # only needed when several accounts are connected

routes:
  - hostname: shop.yourhost.com
    origin: 3000
  - hostname: api.yourhost.com
    origin: http://localhost:4000
    path: ^/v1
    login: [team@yourhost.com, "@yourhost.com"]   # require a Cloudflare Access login
    originRequest:
      httpHostHeader: localhost:4000
      connectTimeout: 10

shares:
  - port: 5173
    hostname: "{branch}-{project}.yourhost.com"  # feat-login-shop.yourhost.com
    expires: 2h
    hostHeader: localhost:5173
  - port: 8080                                   # a random trycloudflare.com address

snapshots:
  - name: docs
    source: { folder: dist }                     # or { build: . } to build first
    hostname: docs.yourhost.com                  # without it: your workers.dev address
    spa: true
    password: { env: DOCS_PASSWORD }             # never the password itself
    expires: 30d

localDomains:
  - name: shop.localhost
    port: 3000
    wildcard: true

Start one

cd ~/code/shop
teitunnel project init     # writes teitunnel.yml from what this machine runs for the project
teitunnel project check    # every problem, with its line and column (no account needed)

init fills the file from the routes and shares this machine already runs for the project; --force replaces an existing file, -a picks the account. check needs no Cloudflare account at all, so it works in a pre-commit hook or a CI lint step.

Apply it

From the terminal

In the repository, or any folder below it (Teitunnel looks in this folder and the ones above it, up to the repository's root):

teitunnel project diff     # what applying would change; nothing changes
teitunnel up               # apply it (after showing the plan), then run the tunnels and shares
teitunnel project apply    # the same, without running this machine's other tunnels
teitunnel project down     # stop the project's shares
teitunnel project down --remove-routes   # also remove the routes it created
teitunnel project list     # the projects this machine knows

up and project apply show one combined plan (each route's steps, the Snapshots to publish, the shares to start) and ask before applying. Shares run as long as the command does and stop with Ctrl-C.

FlagMeaning
-f, --file PATHAnother project file, or a folder with one.
-a, --accountThe account; default: the file's account.
-y, --yesApply without asking, for scripts.
--replaceAlso allow replacing DNS records Teitunnel didn't create.
--strictDon't start a share when the exposure check finds a leak.
--jsonWith diff and list: JSON output.
--no-projectWith up: ignore the file in this folder.

In the app

  1. Open Projects in the sidebar and choose Open Project… (the folder with the teitunnel.yml), or drop the folder on the window.
  2. What the project declares lists each route, share, Snapshot and local domain as Applied, Differs or Missing, and the file's problems with their line and column ("2 problems in teitunnel.yml").
  3. Apply… opens Apply shop: exactly what changes in Cloudflare and on this computer, grouped under Routes, Snapshots, Shares and Local domains. Nothing happens until you choose Apply.

The app watches the file. When you edit it, a banner ("teitunnel.yml changed") offers Review Changes. Shares started from the app run until you stop them or quit Teitunnel.

What happens on Cloudflare

  • Routes are added or updated through the normal plan: DNS record, tunnel configuration, and an Access login when login is set. Each route is planned again just before it's applied, and refused if its steps no longer match what you saw ("Cloudflare changed since the plan was shown. Review the plan again."). If one fails, the ones after it aren't applied ("Stopped at api.yourhost.com.").
  • Snapshots are published the first time and get a new version when their files changed ("Publish a new version of Snapshot docs if its files changed").
  • Shares on a hostname are temporary routes, removed when the share stops. Shares without one get a random trycloudflare.com address.
  • Local domains stay on this computer; nothing changes in Cloudflare.

project down --remove-routes removes only the routes applying the project created. A route that was already there before stays, and DNS records Teitunnel didn't create are never deleted. Every change is recorded in Activity and can be undone there.

Keys

KeyWhat it is
versionRequired. Always 1 for this format.
projectThe project's name, for the {project} placeholder. Default: the name in package.json, Cargo.toml, pyproject.toml, composer.json or go.mod, else the folder's name.
accountThe Cloudflare account (name or id), when several are connected.
routes[]Permanent routes (below).
shares[]Shares that run while teitunnel up or the app runs them (below).
snapshots[]Static copies published to your account (below).
localDomains[]Local HTTPS names for ports on this machine (below).

routes[]

Key
hostnameRequired. A hostname on one of the account's domains; placeholders allowed.
originRequired. A port (3000), host:port, or a URL (http, https, tcp, ssh, rdp, smb).
pathOnly requests whose path matches this regex, e.g. ^/api.
tunnelOne of this machine's tunnels, by name. Default: the default tunnel. An existing route stays on its tunnel; tunnel only picks where a new route goes.
loginRequire a Cloudflare Access login: an email address, @domain, github:org or github:org/team, or a list of them.
signInHow people log in: github or google (the account's login method of that kind), or any (the default; needs login).
skipLoginPaths that skip the login, e.g. [/webhooks], for webhook senders (needs login; at most 10).
originRequestcloudflared's origin settings: httpHostHeader, originServerName, matchSNItoHost, noTLSVerify, caPool, http2Origin, disableChunkedEncoding, connectTimeout, tlsTimeout, tcpKeepAlive, keepAliveTimeout, keepAliveConnections, noHappyEyeballs, proxyType.

shares[]

Key
port or urlRequired, one of them: a port on this machine, or the service's URL.
hostnameA hostname on one of your domains (a temporary route). Without it, a random trycloudflare.com address.
expiresEnds by itself after 90s, 30m, 2h or 7d (a bare number is minutes; at most 90 days).
hostHeaderauto (default: dev servers that need their own address get it), false to pass the visitor's through, or a value such as localhost:5173.
loginRequire a login; needs a hostname.
inspectRecord its traffic in the inspector. Default true.

snapshots[]

Key
nameRequired. Up to 40 characters.
sourceRequired. { folder: PATH } published as it is, or { build: PATH } built first; relative to teitunnel.yml and inside the project.
hostnameA hostname on one of your domains. Default: the account's workers.dev address.
spaServe index.html for unknown paths.
passwordA secret reference (below).
loginRequire a login; needs a hostname.
expiresDelete it after this long, in whole days, e.g. 30d.

localDomains[]

Key
nameRequired. name.localhost, name.test or name.local.
portRequired. The port on this machine.
wildcardSubdomains (*.shop.localhost) go to the same port.

Keys this version of Teitunnel doesn't know (such as protection, reserved for a later version) are reported as warnings ("… isn't known to this version of Teitunnel, so it's ignored.") and skipped, so a file written for a newer version still applies what this one understands. A file for another version needs a newer Teitunnel.

Placeholders

Hostnames can use {branch} (the git branch, read from .git/HEAD; worktrees work, no git process runs), {user} (your login name) and {project}. Each becomes a DNS label: feature/Login_Fix becomes feature-login-fix. Everyone on the team gets their own address from the same file. Outside a git repository {branch} has no value, and the file says so ("The branch placeholder has no value here (the branch needs a git repository).").

Editor autocompletion

The first line, # yaml-language-server: $schema=https://teitunnel.teispace.com/schema/teitunnel.v1.json, gives editors with YAML language support (VS Code's YAML extension, JetBrains IDEs, Zed, Neovim with yaml-language-server) completion and checks for every key. The schema is published at /schema/teitunnel.v1.json and kept in sync with the parser by a test.

Never a secret

A project file is checked into git, so it can't hold a secret. A Snapshot's password is a reference:

password: { env: DOCS_PASSWORD }         # an environment variable of whoever applies it
password: { keychain: docs-preview }     # an entry in the system's keychain, Teitunnel's service

Store the keychain entry once, under the service com.teispace.teitunnel with the reference's name as the account:

security add-generic-password -s com.teispace.teitunnel -a docs-preview -w

Teitunnel refuses a file with a literal secret under a key like password, token or apiKey, and any value that looks like a credential wherever it is (a Cloudflare API token or key, a tunnel token, a JSON web token, a private key, GitHub, Stripe, Slack or AWS keys), with the line and column to fix. A Snapshot's password is set when it's first published; later applies keep it (to change it, use teitunnel snapshot update <name> --password).

Good to know

  • Local domains are added to this computer's local domains (marked as the project's) and served by the app, or by teitunnel local-domain serve. An existing one keeps its HTTPS and inspection settings.
  • Before a new route or share makes a service public, the exposure check looks at it for leaks.
  • A project file is at most 256 KB.
  • For pull request previews, the GitHub Action and GitLab template publish Snapshots and shares without a project file.

Troubleshooting

Edit on GitHub

Last updated on

On this page