This is the finalized specification for Tako. It describes the system as designed and implemented. Keep this in sync with code changes - when you modify code, update the corresponding sections here.
Tako is a deployment and development platform consisting of:
takoCLI - Local tool for development, deployment, server/secret managementtako-server- Remote server binary that manages app processes, routing, and rolling updates- SDKs - Runtime helpers for JavaScript/TypeScript (
tako.sh), Go (tako.sh), and Rust (tako)
Built in Rust (2024 edition). Uses Pingora (Cloudflare's proxy) for production-grade performance.
Performance: On par with Nginx, faster than Caddy. Built in Rust leveraging Pingora.
Simplicity: Opinionated defaults, minimal configuration, convention over configuration.
Reliability: Strong test coverage, graceful edge case handling, users can delete files/folders safely with recovery paths.
Extensibility: Support JavaScript runtimes (Bun, Node) and Go as native app runtimes, with runtime behavior isolated in plugins.
App names must be URL-friendly (DNS hostname compatible):
- Allowed: lowercase letters (a-z), numbers (0-9), hyphens (-)
- Must start with: lowercase letter
- Must end with: lowercase letter or number
- Maximum length: 63 characters
- Examples:
my-app,api-server,web-frontend
This ensures names work in DNS ({app-name}.test by default), URLs, and environment variables.
name is optional in tako.toml. If omitted, Tako resolves app name from the selected config file's parent directory name.
Using top-level name is recommended for stability. Remote server identity is {name}/{env}, so the same app name can be deployed to multiple environments on one server. Renaming name later creates a new app identity/path; delete the old deployment manually.
Default application configuration file for build, variables, routes, and deployment.
App-scoped commands use ./tako.toml by default. Passing -c/--config <file> selects a
different config file and treats that file's parent directory as the project directory.
The config file's parent directory is the app directory (there is no separate app_dir field).
name = "my-app" # Optional but recommended stable identity used by deploy/dev
main = "server/index.mjs" # Optional override; required only when preset does not define `main`
runtime = "bun@1.2.3" # Optional override and version pin; defaults to detected adapter/version
container = "Dockerfile" # Optional container release file; omit for native releases
package_manager = "bun" # Optional override; auto-detected from package.json or lockfiles
preset = "tanstack-start" # Optional app preset; provides `main`, `assets`, and `dev` defaults
app_root = "src" # Optional JS app root for channels/workflows; default: "src"
dev = ["vite", "dev"] # Optional custom dev command override
start = ["./app"] # Optional native artifact start command for deploys
assets = ["dist/client"] # Optional asset directories for deploy artifact
release = "bun run db:migrate" # Optional release command (run once on the leader server before rolling update)
[images]
remote_patterns = ["https://cdn.example.com/uploads/**"] # Optional public remote image allowlist
# local_patterns = ["/images/**"] # Optional override; local public paths default to ["/**"]
# sizes = [320, 640, 960, 1200, 1920]
# qualities = [75]
# formats = ["webp"]
[build]
run = "vinxi build" # Build command
install = "bun install" # Optional pre-build install command
# cwd = "packages/web" # Optional working directory relative to project root
# OR use multi-stage builds (mutually exclusive with [build]):
# [[build_stages]]
# name = "shared-ui"
# cwd = "packages/ui"
# install = "bun install"
# run = "bun run build"
# exclude = ["**/*.map"]
[vars]
API_URL = "https://api.example.com" # Base variables (all environments)
[vars.production]
API_URL = "https://api.example.com"
[vars.staging]
API_URL = "https://staging-api.example.com"
[envs.production]
route = "api.example.com" # Single route, or use 'routes' for multiple
servers = ["la", "nyc"]
storages = { uploads = "prod_uploads" }
backup = { storage = "prod_uploads" } # Optional private app data backups
idle_timeout = 300 # Optional, default: 5 minutes
[envs.staging]
routes = [
"staging.example.com",
"www.staging.example.com",
"example.com/api/*"
]
servers = ["staging"]
storages = { uploads = "staging_uploads" }
idle_timeout = 120
[storages.prod_uploads]
provider = "s3"
bucket = "app-prod-uploads"
endpoint = "https://s3.example.com"
region = "us-east-1"
public_base_url = "https://cdn.example.com/uploads"
[storages.staging_uploads]
provider = "s3"
bucket = "app-staging-uploads"
endpoint = "https://s3.example.com"
region = "us-east-1"
[workflows]
workers = 0
concurrency = 10
[workflows.email]
run = ["./worker", "email"]
workers = 1
[servers.la.workflows]
workers = 2
[servers.la.workflows.email]
workers = 4Variable merging order (later overrides earlier):
[vars]- base[vars.{environment}]- environment-specific- Auto-set by Tako at runtime:
ENV={environment}in both dev and deploy,TAKO_BUILD={version}on deploys,TAKO_DATA_DIR=<app data dir>in dev and native deploys,TAKO_APP_ROOTfor JS apps, plus runtime env vars (e.g.NODE_ENVfor all JS runtimes,BUN_ENVfor Bun)
Variable values may be TOML strings, numbers, booleans, or datetimes. Tako converts them to strings before injection because process environment values are strings. Arrays and tables are not valid variable values.
ENV is reserved. If you set ENV in [vars] or [vars.{environment}], Tako ignores it and prints a warning. LOG_LEVEL (and any other log-verbosity env var your framework reads) is owned by you — set it in [vars] / [vars.<env>] if you want it per environment.
Build/deploy behavior:
nameintako.tomlis optional.- App name resolution order for deploy/dev/logs/secrets/delete:
- top-level
name(when set) - sanitized selected-config parent directory name fallback
- top-level
- Remote deployment identity on servers is
{app}/{env}. Setnameexplicitly to keep the{app}segment stable across deploys. - Renaming app identity (
nameor directory fallback) is treated as a different app; remove the previous deployment manually if needed. app_rootis a JS-only app source root relative totako.toml. It defaults tosrc; use.for root-level JS app files. Tako discovers<app_root>/channels/and<app_root>/workflows/there. It does not changemain,assets, generated declaration placement, build paths, or deploy packaging roots.mainintako.tomlis an optional runtime entrypoint override written to deployedapp.json. It accepts file paths and module specifiers (e.g.@scope/pkg).- If
mainis omitted intako.toml, deploy/dev check the manifest main field (e.g.package.jsonmain), then fall back to presetmain. - For JS adapters (
bun,node), when presetmainisindex.<ext>orsrc/index.<ext>(ext:ts,tsx,js,jsx), deploy/dev resolve in this order: existingindex.<ext>, then existingsrc/index.<ext>, then presetmain. - If neither
tako.toml main, manifest main, nor presetmainis set, deploy/dev fail with guidance. - Top-level
runtimeis optional; when set tobun,node, orgo, it overrides adapter detection for default preset selection intako deploy/tako dev. Add@<version>(for examplebun@1.2.3) to pin the deploy runtime version. Without a pin, deploy auto-detects with<runtime> --version.tako initpins the locally-installed version by default. When top-levelstartis set, deploy can package a native artifact without detecting or installing a runtime. - Top-level
containeris optional. When set, production deploys package source for a container release instead of the native runtime artifact flow. The path is relative to the config file's parent directory and must not be absolute or contain... - Native release packaging fields are invalid with
container:main,start,assets,[build], and[[build_stages]]. The container file and.dockerignoreown production build inputs and outputs for container releases.tako devstill uses the configured dev command, preset dev command, or native runtime default; it does not build or run the container file locally. - Container releases use Podman on the target server.
tako servers addinstalls Podman, and server upgrade installs it when missing. During remote prepare,tako-serverbuilds the image from the uploaded app directory, tags it astako/{app}-{env}:{version}, and runs HTTP instances from that image without overriding the imageENTRYPOINTorCMD. The container receivesHOST=0.0.0.0,PORT=3000,ENV,TAKO_BUILD, user vars, andTAKO_BOOTSTRAP_DATA. The app must use the Tako SDK so secrets, storage bindings, internal status, and the health-probe token use the same contract as native releases. - Container HTTP instances do not receive the fd 3 bootstrap, fd 4 readiness handshake, internal socket, or
TAKO_DATA_DIRin v0. A configured workflowruncommand starts a separate container process from the same image; Tako replaces the image entrypoint withrun[0], passesrun[1..]as arguments, mounts the internal socket, and sends the bootstrap envelope throughTAKO_BOOTSTRAP_DATA. Container releases support one workflowruncommand in v0. - Top-level
package_manageris optional; when set (e.g."npm","pnpm","yarn","bun"), it overrides auto-detection frompackage.jsonpackageManagerfield or lockfiles. - Top-level
presetis optional. Presets are metadata-only (name,main,assets,dev) providing entrypoint, asset, and dev-command defaults. They do not contain build, install, or start commands. - Top-level
devis optional; when set (e.g.["vite", "dev"]), it overrides both preset and runtime default dev commands fortako dev. - Top-level
startis optional; when set (e.g.["./app"]), it is written to the native deploy manifest andtako-serverruns that command directly from the app directory. An exact{main}argument is replaced with the resolvedmainpath for runtime-style commands. This is the native artifact mode for compiled binaries such as Rust apps. The process must still use a Tako SDK listener so fd 3 bootstrap, fd 4 readiness, status checks, secrets, storages, and rolling updates work. Whenstartis omitted, deploy falls back to the selected runtime/preset launch command and prints a warning. - Top-level
assetsis optional; lists asset directories to include in the deploy artifact (e.g.["dist/client"]). Asset roots are presetassetsplus top-levelassets(deduplicated). [images]is optional public image optimizer config. Local public paths are allowed by default withlocal_patterns = ["/**"]; settinglocal_patternsreplaces that default. Remote images are denied unless their full URL matchesremote_patterns. Patterns are glob-like URL strings, not regular expressions:*matches one path segment,**matches the rest of a path, and remote hosts may use a leading wildcard such ashttps://*.example.com/uploads/**. Remote patterns without a protocol match bothhttpandhttps.- Public image sizes default to
[320, 640, 960, 1200, 1920], qualities default to[75], and formats default to["webp"]. Theformatsorder is the negotiation preference when a request omitsf. - Top-level
releaseis optional. When set, deploy runs the command once on the leader server (first entry in[envs.<env>].servers) inside the new release directory after extract + production install but before any rolling update. [envs.<env>].releaseoverrides the top-level value for that environment. An empty string (release = "") explicitly clears the inherited top-level command for that env.- The release command runs as
sh -c "<command>"with cwd set to the new release directory, the app's per-app Unix identity on root-managed production servers, and the same env an HTTP instance receives (merged[vars]+[vars.<env>]+ secrets +TAKO_BUILD+TAKO_DATA_DIR+ENV+ runtime defaults). Secrets are injected as env vars (release commands are one-shot; the fd 3 mechanism is reserved for long-running app/worker processes). - Hard timeout: 10 minutes per release-command invocation. On timeout, the process is killed and deploy fails.
presetsupports:- runtime-local aliases:
tanstack-start,nextjs(resolved under selected runtime, e.g.runtime = "bun") - pinned runtime-local aliases:
tanstack-start@<commit-hash>,nextjs@<commit-hash>
- runtime-local aliases:
- namespaced preset aliases in
tako.toml(for examplejs/tanstack-start) are rejected; choose runtime via top-levelruntimeand keeppresetruntime-local. github:preset references are not supported intako.toml.- Preset definitions live in
presets/<language>.toml(for examplepresets/javascript.toml), where each preset is a section ([tanstack-start], etc.). Each section containsname(optional, fallback: section name),main,assets, anddev(custom dev command). Tako caches fetched preset manifests locally.tako devprefers cached or embedded preset data and only fetches from GitHub when nothing local is available; deploy refreshes unpinned aliases from GitHub and falls back to cached content on fetch failure. tanstack-startpreset defaultsmain = "dist/server/tako-entry.mjs",assets = ["dist/client"], anddev = ["vite", "dev"]. Themainfile is emitted bytako.sh/viteduringvite buildand wraps the SSR bundle with tako endpoint handling.nextjspreset defaultsmain = ".next/tako-entry.mjs"anddev = ["next", "dev"].vitepreset defaultsdev = ["vite", "dev"]for projects using Vite as their dev server.- Presets may declare runtime-local overrides as nested sections (
[<preset>.<runtime>]) inside the family manifest. Only thedevfield can be overridden —main,assets, andnamealways come from the base section. For example,presets/javascript.tomloverrides theviteandtanstack-startdev commands for Bun ([vite.bun],[tanstack-start.bun]) becausebunx --bundrops fds > 2, which breaks the fd-4 readiness handshake. - Official preset definitions live in the
tako-sh/presetsGitHub repo (overridable via thePACKAGE_REPOSITORY_URLenv var for testing). Fetched branch manifests are cached locally for roughly one hour; on fetch failure, Tako falls back to any previously cached copy or the manifests embedded in the CLI binary. GitHub preset fetches useGH_TOKENwhen set, falling back toGITHUB_TOKEN. - Deploy restores local JS build caches from workspace-root
.turbo/and app-root.next/cache/into the temporary build workspace when present, then excludes those cache directories from the final deploy artifact. - Runtime behavior (install commands, launch args, entrypoint resolution) lives in runtime plugins (
tako-runtime/src/plugins/), not in presets. Explicitstartcommands bypass runtime plugin launch args and production install. tako initinstalls the selected runtime's SDK via the runtime package-manageraddcommand:tako.shfor JS/Go.- Server membership is declared per environment with
[envs.<name>].servers. - Storage bindings are declared per environment with
[envs.<name>].storages = { app_name = "resource_name" }. The key is the stable app-facing binding exposed astako.storages.<app_name>; the value references a top-level[storages.<resource_name>]S3 resource or the built-inlocalresource. - App data backups are enabled per environment with
backup = { storage = "resource_name" }. The resource must be a declared private S3-compatible[storages.<resource_name>]resource. Backup storage is not exposed astako.storages; only[envs.<env>].storagescreates SDK bindings. - SSL certificate issuance is selected per environment with optional
ssl = "letsencrypt"orssl = "cloudflare"under[envs.<name>]. Omitted meansletsencrypt. Cloudflare SSL uses Cloudflare Origin CA certificates and requires encrypted credentialssl.cloudflarefromtako credentials set ssl.cloudflare --env <name>with Cloudflare Zone / SSL and Certificates / Edit permission. Let’s Encrypt exact routes need no credentials by default; when an environment hasssl.cloudflare, exact routes use Cloudflare DNS-01 instead of HTTP-01. Let’s Encrypt wildcard routes require the same credential because they must use DNS-01 for certificate validation. - Tako-owned provider credentials are stored per environment in
.tako/secrets.jsonbytako credentials set <name> --env <name>. Supported credentials aressl.cloudflarefor certificate operations andpostgres_urlfor shared channel/workflow storage. Deploy sends provider credentials to servers as server-only runtime credentials; they are not exposed to app code. Deploy rejects expired app secrets, storage credentials, and required provider credentials before build/deploy work starts and warns when any of them expire within 30 days. Each target server verifies required Cloudflare credentials during remote prepare, before release commands, route changes, or app startup. Let’s Encrypt DNS-01 routes require a Cloudflare API token with Zone Read and DNS Write on the matching zone; both user-owned and account-owned tokens are accepted when they have the required permissions. Deploy verifies zone read access from the server's egress IP, while DNS record writes are not probed before deploy. Cloudflare SSL requires Zone / SSL and Certificates / Edit. It verifies user-owned tokens when Cloudflare's token verification endpoint supports the token format; account-owned tokens are validated by the Origin CA API when certificates are issued. - Client source-IP handling is per environment through optional
source_ipunder[envs.<name>]. Omitted means automatic Cloudflare detection, then explicitly configured trusted proxy headers, then direct peer fallback. - Top-level
[storages.<resource>]config contains non-secret S3-compatible storage metadata.providerdefaults tos3;provider = "local"is invalid in config. S3 resources requirebucket,endpoint, andregion;endpointand optionalpublic_base_urlmust use HTTPS. localis a built-in storage resource name, not a top-level[storages.local]table. Bind an app storage to it withstorages = { uploads = "local" }. Indevelopment, a storage binding may also reference any undeclared resource name and it defaults to local storage. In deploy environments, every bound resource must be declared unless the resource name islocal; local storage can deploy only to single-server environments.- The same server name may be assigned to multiple non-development environments in one project. Each environment deploys to its own server-side app identity and filesystem path under
/opt/tako/apps/{app}/{env}. developmentis fortako dev;serversdeclared there are ignored by deploy validation.- Deployed app instances bind to
127.0.0.1on an OS-assigned port. The SDK signals readiness totako-serverby writing the bound port to fd 4 (file descriptor 4) once listening. The server then routes traffic to that loopback endpoint. tako devresolves the dev command with this priority:devintako.toml(user override, e.g.dev = ["custom", "cmd"])- Preset
devcommand (e.g. vite preset usesvite dev) - Runtime default: JS runtimes run through the SDK HTTP entrypoint (
bun run node_modules/tako.sh/dist/entrypoints/bun-server.mjs {main}, ornode --experimental-strip-types node_modules/tako.sh/dist/entrypoints/node-server.mjs {main}), and Go usesgo run .
tako devmarks an app running only after the app writes its bound loopback port to fd 4. Direct Vite dev commands (for exampleviteorvite dev) must use thetako.sh/viteplugin for fd-4 readiness; if the command looks like Vite and no readiness signal arrives, the CLI reports a Vite-specific plugin hint. Tako does not parse Vite stdout URLs as readiness.- The dev entrypoints host the HTTP server. Workflow workers run as a separate, scale-to-zero subprocess managed by tako-dev-server's embedded
WorkflowManager— same architecture as production, butworkers: 0with a 3s idle timeout so the worker only exists while there's real work. The SDK wrapsexport default function fetch()orexport default { fetch }into a proper HTTP server onPORT; worker stdout/stderr is tee'd into the CLI log stream withscope: "worker". - Process exit detection:
tako devpollstry_wait()every 500ms to detect when the app process exits. On exit, the route goes idle (proxy stops forwarding) and the next HTTP request triggers a restart. A route is activated only after fd-4 readiness succeeds. tako devresolves unpinned official preset aliases from cached or embedded preset data when available and only fetches from themasterbranch as a last resort.tako deployresolves unpinned official preset aliases from themasterbranch on each deploy; if the refresh fails, it falls back to cached content.- Deploy sends app vars + runtime vars to
tako-serverin thedeploycommand payload (non-secret env vars inapp.json); secrets and storage bindings are sent separately and stored encrypted in SQLite. Native releases pass secrets and storage bindings to HTTP instances and workflow workers via fd 3 (file descriptor 3) at spawn time — the server writes JSON to a pipe and the child process reads fd 3 before any user code runs. Container releases pass the same bootstrap envelope throughTAKO_BOOTSTRAP_DATAbecause the fd bootstrap pipe does not cross the container boundary in v0. [build]section hasrun(build command),install(optional pre-build install command),cwd(optional working directory relative to project root; absolute paths and..are rejected), plusinclude/excludefor artifact filtering.[build]is a shortcut for a single-stage[[build_stages]]list.[build]and[[build_stages]]are mutually exclusive: having bothbuild.runand[[build_stages]]is an error.[build].include/[build].excludecannot be used alongside[[build_stages]]; use per-stageexcludeinstead.- Build stage resolution precedence (first non-empty wins):
[[build_stages]]→[build](normalized to a single stage) → runtime default. The runtime default is the runtime plugin's build command:bun/npm/pnpm/yarn run --if-present buildfor JS runtimes, andCGO_ENABLED=0 go build -o app .for Go. If a Go project hascmd/worker/main.go, the Go default also buildsCGO_ENABLED=0 go build -o worker ./cmd/worker. When top-levelstartis set, runtime defaults are skipped; use[build]or[[build_stages]]to produce the artifact. When nothing resolves, the build phase is a no-op. - App-level custom build stages can be declared in
tako.tomlunder[[build_stages]](top-level array):name(optional display label)cwd(optional, relative to app root;..is allowed for monorepo traversal but guarded against escaping the workspace root; symlink escapes outside the source root fail deploy)install(optional command run beforerun)run(required command)exclude(optional array of file globs to exclude from the deploy artifact)
- Build uses a build dir approach: copies the project from source root into
.tako/build(respecting.gitignore), symlinksnode_modules/directories from the original tree, runs build commands, then archives the result withoutnode_modules/. - Build and source archives preserve symlinks as symlinks instead of following them. Directory symlinks are not expanded into the artifact, and source hashes track each symlink target so symlink changes invalidate the artifact cache.
- During
tako deploy, source files are bundled from source root (gitroot when available, otherwise app directory). - Deploy always force-excludes
.git/,.tako/,.env*, andnode_modules/from the deploy archive. Additional exclusions come from[build].excludeand.gitignore. - After extracting a native deploy artifact,
tako-serverruns the runtime plugin's production install command (e.g.bun install --production) before starting instances unless the manifest has an explicitstartcommand. After extracting a container deploy artifact,tako-serverbuilds the configured container file with Podman instead. - When
runtimeincludes@<version>, deploy uses that version directly. Otherwise, runtime version resolution runs the runtime probe tool's--versiondirectly, falling back tolatest. - Deploy saves the resolved runtime version into
app.json(runtime_versionfield) when a runtime is selected. Runtime-less native artifact releases with explicitstartomitruntime_version. - Built target artifacts are cached locally under
.tako/artifacts/using a deterministic cache key that includes source hash, target label, resolved preset source/commit, build commands, include/exclude patterns, asset roots, and app subdirectory. - Cached artifacts are checksum/size verified before reuse; invalid cache entries are automatically discarded and rebuilt.
- Non-dry-run
tako deployacquires a project-local.tako/deploy.lockbefore local server checks/build/deploy work begins. If another deploy already holds the lock, the second CLI exits immediately with the owning PID. - After build, deploy verifies the resolved runtime
mainfile exists in the build workspace before artifact packaging; missing files fail deploy with an explicit error. - On every deploy, local artifact cache is pruned automatically (best-effort): keep the 90 most recent target artifacts (
{version}.tar.zstunder.tako/artifacts/and its per-target subdirectories) and remove orphan target metadata files. - Artifact include patterns are resolved in this order:
build.include(if set)- fallback
**/*
- Artifact exclude patterns:
[build].excludeentries. - Asset roots are preset
assetsplus top-levelassets(deduplicated), merged into apppublic/after build in listed order (later entries overwrite earlier ones).
Instance behavior:
- Desired instances are runtime app state stored on each server, not
tako.tomlconfig. - New app deploys start with desired instances
1on each server. The first request after deploy hits a hot instance — no cold start. Opt into scale-to-zero withtako scale 0 --env <environment>from a project directory, ortako scale 0 --server <server> --app <app>/<env>outside one. tako scalechanges the desired instance count per targeted server, and that value persists across server restarts, deploys, and rollbacks.- Each app has a server-side maximum instance count. New deploys default that maximum to two app instances per available CPU. Explicit scale or deploy requests above the effective server maximum fail instead of silently capping. During server restore, invalid persisted counts above the maximum are clamped and logged so startup stays safe.
- Desired instances
0: On-demand with scale-to-zero. Deploy keeps one warm instance running so the app is immediately reachable after deploy. Instances are stopped after idle timeout.- Once scaled to zero, the next request triggers a cold start and waits for readiness up to startup timeout (default 30 seconds). If no healthy instance is ready before timeout, proxy returns
504 Gateway Timeoutwith a generic body. - If cold start setup fails before readiness, proxy returns
502 Bad Gatewaywith a generic body. - Startup timeout diagnostics are recorded in the app log stream, including captured startup stdout/stderr when the process produced output before readiness.
- While a cold start is already in progress, requests are queued up to 1000 waiters per app (default). If the queue is full, proxy returns
503 Service Unavailablewith a generic body. - If warm-instance startup fails during deploy, deploy fails.
- Once scaled to zero, the next request triggers a cold start and waits for readiness up to startup timeout (default 30 seconds). If no healthy instance is ready before timeout, proxy returns
- Desired instances
N(N > 0): keep at leastNinstances running on that server. idle_timeout: Applies per-instance (default 300s / 5 minutes)- Instances are not stopped while serving in-flight requests.
- Explicit scale-down drains in-flight requests first, then stops excess instances.
Global user-level settings and server inventory. Stored in the platform config directory (~/Library/Application Support/tako/ on macOS, ~/.config/tako/ on Linux). NOT in the project.
[[servers]]
name = "la"
host = "1.2.3.4"
port = 22 # Optional, defaults to 22
http_port = 80 # Optional public HTTP port, defaults to 80
https_port = 443 # Optional public HTTPS port, defaults to 443
arch = "x86_64"
libc = "glibc"
[[servers]]
name = "nyc"
host = "5.6.7.8"
arch = "aarch64"
libc = "musl"[[servers]] entries are managed by tako servers add/rm/ls. All names and hosts must be globally unique.
Detected server build target metadata (arch, libc) and public proxy ports (http_port, https_port) are stored directly in each [[servers]] entry.
SSH authentication:
-
takoauthenticates using local SSH keys from~/.ssh(common filenames likeid_ed25519,id_rsa, etc.). -
If a key file is passphrase-protected,
takowill prompt for the passphrase when running interactively. Pass--ssh-passphrase {passphrase}for one-line commands and non-interactive runs. -
If no suitable key files are found or usable,
takofalls back tossh-agentviaSSH_AUTH_SOCK(when available). -
tako devuses a fixed local HTTPS listen port (47831). -
On macOS,
tako devuses a dedicated loopback alias (127.77.0.1) plus a launchd-managed dev proxy so public URLs stay on default ports (:443for HTTPS,:80for HTTP redirect). -
On Linux,
tako devuses the same loopback alias (127.77.0.1) with iptables redirect rules (443→47831, 80→47830, 53→53535) to achieve portless URLs without a proxy binary. One-timesudosets up the rules, a systemd oneshot service persists them across reboots. On NixOS, aconfiguration.nixsnippet is printed instead of imperative setup.
CLI prompt history is stored separately at history.toml (not in config.toml).
Per-environment encrypted secrets (JSON format, AES-256-GCM encryption):
{
"production": {
"key_id": "0123456789abcdef",
"backup_keys": [
{
"id": "backup-key-0123456789abcdef",
"value": "encrypted_value"
}
],
"app": {
"DATABASE_URL": {
"value": "encrypted_value",
"expires_on": "2027-01-01"
},
"API_KEY": {
"value": "encrypted_value"
}
},
"storages": {
"prod_uploads": {
"access_key_id": {
"value": "encrypted_value",
"expires_on": "2027-01-01"
},
"secret_access_key": {
"value": "encrypted_value",
"expires_on": "2027-01-01"
}
}
},
"credentials": {
"ssl.cloudflare": {
"value": "encrypted_value",
"expires_on": "2027-01-01"
}
}
},
"staging": {
"key_id": "fedcba9876543210",
"app": {
"DATABASE_URL": {
"value": "encrypted_value_different"
}
}
}
}Each environment has a key_id (16 hex characters), optional backup_keys, an app map for app secrets, an optional storages map for encrypted storage credentials keyed by storage resource name, and an optional credentials map for encrypted provider credentials keyed by credential name. App secret names, backup key ids, storage resource names, and credential names are plaintext. Each encrypted secret field stores a value and may store expires_on; value is encrypted with AES-256-GCM, while expires_on is plaintext date metadata. Backup key value fields are encrypted 32-byte backup encryption keys protected by the environment key. Expiry values are dates (YYYY-MM-DD); in N days is normalized to the UTC date N days from now. Leaving expiry blank, omitting --expires-on, or passing never leaves expires_on out of .tako/secrets.json.
Deploy validates expiry before build/deploy work starts. Expired app secrets, expired S3 storage credentials used by the target environment, and expired provider credentials required by the selected certificate provider fail deployment with an update command. Credentials that expire within 30 days produce a deployment warning but do not block the deploy. Required Cloudflare credentials are checked by each target server during remote prepare: Let’s Encrypt DNS-01 routes verify Cloudflare API token zone read access from the server's egress IP and require DNS Write for certificate issuance, while Cloudflare SSL verifies user-owned token status when supported by Cloudflare's verification endpoint.
tako init ensures the app's .tako/ directory stays ignored while .tako/secrets.json remains trackable:
- inside a git repo, it updates the repo root
.gitignorewith app-relative rules - outside a git repo, it creates or updates
.gitignorein the app directory
Encryption keys are stored outside the project:
- By default, environment-specific keys are cached under Tako's data directory as
keys/{key_id}, wherekey_idis the environment key id stored in.tako/secrets.json. - On macOS, interactive key creation and key import offer
Use iCloud Keychain?. Choosing yes stores and reads the key directly from the signedTako.appCLI using a synchronizable Keychain item. The installer symlinkstakotoTako.app/Contents/MacOS/tako; no helper process or socket is used. If a local key file already exists and iCloud Keychain is unavailable during a read, Tako uses the local key file. If the signed app entitlement is unavailable while saving a key to iCloud Keychain, Tako fails withiCloud Keychain requires the signed Tako app. Reinstall Tako and try again.and does not write a local key file or update.tako/secrets.json.
When the first secret is set for an environment, Tako generates a random environment key. Keys are shared with other machines via tako secrets key export and tako secrets key import. Teams that prefer a memorized shared secret can initialize an environment key with tako secrets key import --passphrase --env {environment} before setting secrets.
Storage bindings are split between tako.toml and .tako/secrets.json. tako.toml stores app-facing binding names and non-secret provider metadata. .tako/secrets.json stores only encrypted S3 credentials under each environment's storages map. There is no separate .tako/storages.json file.
[envs.production]
route = "app.example.com"
servers = ["la"]
storages = { uploads = "prod_uploads" }
[storages.prod_uploads]
provider = "s3"
bucket = "app-uploads"
endpoint = "https://<account>.r2.cloudflarestorage.com"
region = "auto"
public_base_url = "https://cdn.example.com/uploads"Storage binding and S3 resource names may contain lowercase letters, numbers, hyphens, and underscores. The resource name local is built in and must not be declared under [storages]; bind to it directly with storages = { uploads = "local" }. For S3 resources, tako storages add prompts for access credentials and optionally when they expire; --expires-on can provide the expiry directly. Deploy fails before build/deploy work starts if the selected environment's S3 credentials are expired and warns when they expire within 30 days. When credentials are current or expiry is unknown, deploy decrypts them locally, combines them with the selected tako.toml resource metadata, sends the runtime bindings over the signed management path, and tako-server stores the resulting bindings encrypted in server SQLite. Fresh native app and worker processes receive bindings through the fd 3 bootstrap envelope as storages; fresh container app processes receive the same envelope through TAKO_BOOTSTRAP_DATA.
tako storages credentials <resource> --env <environment> sets or rotates encrypted S3 credentials for a declared top-level storage resource without adding an app binding. This is the recommended setup path for backup-only storage resources. The same S3 bucket may be used for app storage and backups; backup objects are automatically written under Tako's reserved _tako/backups/ prefix.
Backups are opt-in per environment:
[envs.production]
route = "app.example.com"
servers = ["la", "nyc"]
backup = { storage = "r2" }
[storages.r2]
provider = "s3"
bucket = "app-data"
endpoint = "https://<account>.r2.cloudflarestorage.com"
region = "auto"Backup storage reuses declared [storages.<resource>] metadata and .tako/secrets.json S3 credentials. It must be S3-compatible and private: provider = "local" and public_base_url are rejected. A backup-only resource can share a bucket with app storage because Tako writes backups under _tako/backups/{app}/{env}/{server}/. The {server} segment comes from the deployed server identity/name so multiple servers in one environment do not overwrite each other's archives.
Backup archives are encrypted before upload with AES-256-GCM. When backups are enabled, tako deploy and tako backups now create backup_keys for the environment if none exist. The keys are encrypted in .tako/secrets.json with the existing environment key; users still export/import only the environment key. The last backup_keys entry is used for new backups. Each backup manifest records its backup_key_id, so older backups can still be restored while their key remains in .tako/secrets.json and is synced to the server.
When enabled, Tako backs up durable per-app state under {data_dir}/apps/{app}/{env}/data/:
app/— app-owned data exposed asTAKO_DATA_DIRtako/workflows.sqlite— Tako-owned durable workflow queue/state
Workflow storage uses local SQLite at {data_dir}/apps/{app}/{env}/data/tako/workflows.sqlite unless the environment has credential postgres_url, in which case the server uses Postgres schema tako_workflows keyed by deployed app id. If a deployed environment targets more than one server and the JS app has <app_root>/workflows/, deploy fails before build/deploy work starts unless postgres_url is set or every workflow definition opts into per-server local execution with defineWorkflow(..., { local: true, ... }). If a Go app has cmd/worker/main.go and targets more than one server, deploy requires postgres_url because Go workflows do not have a source-level local-only annotation. Local workflows use each server's own local queue and cron schedule set; cron runs once per server and enqueued work is not globally deduplicated.
Channel replay uses local SQLite at {data_dir}/apps/{app}/{env}/data/tako/channels.sqlite unless the environment has credential postgres_url, in which case the server uses Postgres schema tako_channels keyed by deployed app id. Multi-server channel deploys require postgres_url because a publish on one server must be visible to subscribers and reconnecting clients on every server. Channel messages are inserted durably before fanout; production subscribers poll the selected replay store for new retained rows.
Tako does not back up channel replay storage (tako/channels.sqlite and its SQLite companion files). Channel replay is a bounded reconnect buffer, not canonical app history, so restored apps start with empty channel replay storage. SQLite files are snapshotted with SQLite's online VACUUM INTO mechanism before archiving, and -wal/-shm companion files are not included separately. Archives are compressed as tar.zst, encrypted before upload, and stored with a SHA-256 manifest for the encrypted object plus a remote JSON index. Retention defaults to 30 days. The server creates a backup after a successful deploy and then runs due backups roughly every 24 hours while tako-server is running. Backup failures after deploy are reported in the finalize response/logs but do not roll back an otherwise successful deploy.
Public route certificates use Let’s Encrypt by default. Exact Let’s Encrypt routes do not need provider credentials by default and use HTTP-01. If the environment has credential ssl.cloudflare, exact routes use Cloudflare DNS-01 instead. Let’s Encrypt wildcard routes always use Cloudflare DNS-01 and require ssl.cloudflare.
[envs.production]
route = "*.example.com"
servers = ["la"]
ssl = "letsencrypt"
source_ip = "direct"Set up Let’s Encrypt wildcard certificate credentials with:
tako credentials set ssl.cloudflare --env productionThe command prompts for a Cloudflare API token and optionally asks when the token expires. --expires-on can provide the expiry directly. The token is encrypted in .tako/secrets.json under the selected environment's credentials object as ssl.cloudflare. For Let’s Encrypt DNS-01, use a Cloudflare user or account API token with Zone Read and DNS Write for the matching Cloudflare zone, and include each target server's egress IP in any token IP restriction. Deploy validates that Let’s Encrypt wildcard routes have an unexpired credential when expiry is known, warns when the credential expires within 30 days, decrypts the selected environment's token locally, sends the SSL binding during remote prepare, verifies Cloudflare zone read access from the target server, and tako-server stores it encrypted in server SQLite for that deployed app after the final deploy command starts. Exact Let’s Encrypt routes use the same DNS-01 path when this credential is present.
Cloudflare DNS-01 is only used to prove domain control for Let’s Encrypt certificates. It does not require Cloudflare proxy mode. For wildcard second-level subdomains such as *.app.example.com, use DNS-only/direct records that point at the Tako server so tako-server terminates TLS itself.
Set ssl = "cloudflare" on an environment to use Cloudflare Origin CA certificates instead:
[envs.production]
route = "app.example.com"
servers = ["la"]
ssl = "cloudflare"
source_ip = "cloudflare-proxy"Set up the Cloudflare provider credential with:
tako credentials set ssl.cloudflare --env productionThe command stores the encrypted Cloudflare API token plus optional expires_on metadata in .tako/secrets.json under that environment's credentials object. Use a Cloudflare user or account API token with Zone / SSL and Certificates / Edit for the matching zone. Deploy validates provider credential expiry locally, decrypts the token locally, sends the SSL binding during remote prepare for server-side validation, and tako-server stores it encrypted in server SQLite for that deployed app after the final deploy command starts. User-owned Cloudflare tokens are checked with Cloudflare's token verification endpoint when supported; account-owned tokens are accepted and validated by Origin CA API calls when certificates are issued.
Cloudflare SSL issues Origin CA certificates through Cloudflare's Origin CA API. These certificates are intended for Cloudflare-proxied traffic; direct browser connections to the origin will not trust them. Cloudflare SSL can issue wildcard route certificates without DNS-01 credentials.
[envs.<env>].source_ip controls how Tako derives the client IP that is used for per-IP rate limits and upstream X-Forwarded-For.
[envs.production]
route = "app.example.com"
servers = ["la"]
source_ip = "direct"Allowed values:
- Omitted or
"auto": if the direct peer IP belongs to Cloudflare andCF-Connecting-IPis present, use that header; otherwise, if servertrusted_proxy.client_ip_headersis configured and the peer IP is intrusted_proxy.trusted_cidrs, use the first configured trusted header that contains a valid client IP; otherwise use the direct peer IP. "direct": always use the direct peer IP."cloudflare-proxy": strict Cloudflare mode; requests must come from Cloudflare IP ranges and include a validCF-Connecting-IPheader. Other requests are rejected with403 Forbidden."trusted-proxy": strict generic proxy mode for nginx, HAProxy, Caddy, Traefik, and similar front proxies. Requests must come from loopback or from a CIDR listed in servertrusted_proxy.trusted_cidrs, and must include a valid client IP inX-Forwarded-FororForwardedunless the server config sets an explicittrusted_proxy.client_ip_headerslist. Other requests are rejected with403 Forbidden.
Generated tako.toml files omit source_ip by default. tako-server keeps Cloudflare IP ranges in memory, starts from a bundled fallback list, overlays a valid last-known-good cache from the server data directory, and refreshes the list every 24 hours while running when any route uses source_ip = "auto" or source_ip = "cloudflare-proxy". Successful refreshes are written back to disk.
Install the CLI on your local machine:
curl -fsSL https://tako.sh/install.sh | shThe hosted installer installs tako, tako-dev-server, and tako-dev-proxy from the same archive and requires a valid SHA-256 checksum before extraction. On macOS, it installs Tako.app, verifies the signed app and helper binaries, symlinks tako to the signed CLI inside the app bundle, and installs libvips with Homebrew when Homebrew is available.
Upgrade local CLI:
tako upgradetako upgrade upgrades only the local CLI installation and requires a valid SHA-256 checksum before extraction.
On macOS, tako upgrade preserves the installer layout: it installs the verified Tako.app bundle atomically, keeps tako as a symlink to Tako.app/Contents/MacOS/tako, and installs tako-dev-server and tako-dev-proxy next to that symlink.
Rolling release model:
latestis a single moving GitHub release updated by CI on eachmasterpush. There are no versioned releases and no stable/canary distinction; every build is a rolling build while Tako's protocol is v0.- CLI and server artifacts report
<base>-<sha7>in--versionoutput, where<base>is the package version (always0.0.0) and<sha7>is the 7-character source commit. - The npm-published SDK (
tako.sh) uses0.0.0-<sha7>version strings and is published under thelatestdist-tag on every push. - GitHub-backed update checks and release downloads use
GH_TOKENwhen set, falling back toGITHUB_TOKEN. Tokens are sent only asAuthorization: Bearer ...request headers, not in URLs.
--version: Print version and exit (format:<base>-<sha7>).-v, --verbose: Show verbose output as an append-only execution transcript with timestamps and log levels.--ci: Deterministic non-interactive output (no colors, no spinners, no prompts). Can be combined with--verbose.--json: Emit structured JSON on stdout. This is a global flag accepted before or after any subcommand. JSON mode implies CI-style non-interactive rendering for progress, prompts, and diagnostics.--dry-run: Show what would happen without performing any side effects. Skips server connections, file uploads, config writes, and remote commands. Prints⏭ ... (dry run)for each skipped action. Production deploy confirmation is auto-skipped. Supported by:deploy,servers add,servers remove,delete, and side-effecting backup commands (backups now,backups download,backups restore).-c, --config {config}: Use an explicit app config file instead of./tako.toml. If the provided path does not end with.toml, Tako appends it automatically. App-scoped commands treat the selected file's parent directory as the project directory. This allows multiple config files in one folder.--ssh-passphrase {passphrase}: Use the provided passphrase for encrypted local SSH private keys during SSH authentication and signed remote-management requests.
CLI output modes:
- Normal mode (default): Concise interactive UX with rich prompts and inline progress rendering. Commands that already know their plan may render a persistent task tree that shows waiting work up front (
○with...labels), updates running tasks in place, keeps completed tasks visible, and may render reporter-specific error lines under failed task rows. - Verbose mode (
--verbose): Append-only execution transcript. Each line:HH:MM:SS LEVEL message. It only prints work as it starts or finishes; upcoming tasks are not pre-rendered. Prompts render as transcript-style (still interactive). DEBUG-level messages are shown. - CI mode (
--ci): No ANSI colors, no spinners, no interactive prompts. It stays transcript-style and emits only current work plus final results. If a required prompt value is missing, fails with an actionable error message suggesting CLI flags or config. - JSON mode (
--json): Same non-interactive rendering as CI for progress and diagnostics, with one machine-readable command result on stdout for finite commands. Progress, trace, warning, and error text stays on stderr. Commands with command-specific data include that data in the final result; commands without a specialized schema use{ "ok": true, "command": "<command>" }. Command failures print{ "ok": false, "error": { "message": "<message>" } }on stdout and the human-readable error on stderr. - CI + Verbose (
--ci --verbose): Detailed append-only transcript with no colors or timestamps. - On
Ctrl-C, Tako clears any active prompt or spinner it controls, leaves one blank line, printsOperation cancelled, and exits with code 130.
All status/progress/log output goes to stderr. Only actual command results (URLs, machine-readable data) go to stdout.
tako logs --tail --json is a streaming exception: it emits one structured log event per stdout
line until interrupted instead of a final result object.
Config selection is global for app-scoped commands:
- default:
./tako.toml - override:
-c path/to/config(recommended shorthand;.tomlsuffix is optional) - project directory: parent directory of the selected config file
App-scoped commands that honor -c/--config: init, dev, logs, deploy, releases,
backups, delete, secrets, storages, generate, and scale when it is using project context.
Create tako.toml template with helpful comments.
tako initTemplate behavior:
- Leaves only minimal starter options uncommented:
name[envs.production].route- top-level
runtime(including a version pin from the locally-installed runtime via<runtime> --version) - top-level
presetonly when a non-base preset is selected (for base adapter presets and custom mode, it remains commented/unset)
- Updates
.gitignoreso the app's.tako/*stays ignored while.tako/secrets.jsonremains trackable (repo-root.gitignorewhen inside git, app-local.gitignoreotherwise) - Includes commented examples/explanations for supported
tako.tomloptions:name,main, top-levelruntime/preset/assets/dev,[build](run,install,cwd,include,exclude), and[[build_stages]](with per-stagecwdandexclude)[vars][vars.<env>][envs.<env>]route declarations (route/routes), server membership (servers), and idle scaling policy (idle_timeout)
- Includes a docs link to
https://tako.sh/docs/tako-toml. - Writes the selected config file (default
./tako.toml). - Prompts for required app
name(default from selected-config parent directory-derived app name). - Prompts for required production route (
[envs.production].route) with default{name}.example.com. - If the production route is a wildcard route, prompts to set up Cloudflare DNS for wildcard HTTPS. When accepted, init optionally asks when the token expires and encrypts the Cloudflare API token in
.tako/secrets.jsonunder the production environment'scredentialsobject asssl.cloudflare. - Detects adapter (
bun,node,go, fallbackunknown) and prompts for runtime selection. - For JS runtimes, prompts for
app_rootafter runtime selection. The default issrcfor new projects,srcorappwhen existing Tako JS files live there, and.when existingtako.d.ts, legacytako.gen.ts,channels/, orworkflows/live next totako.toml. The generated config omitsapp_rootwhen the selected root is the defaultsrc, and writes it only for non-default roots. - After generating
tako.toml, init installs the SDK package via the selected runtime's package-manageraddcommand (for JS:bun add tako.sh, etc.; for Go:go get tako.sh). - In interactive mode, init fetches runtime-family preset names from official family manifest files (
presets/<language>.toml) and showsFetching presets...while loading. - For built-in base adapters, init defaults to:
- Bun:
bun - Node:
node - Go:
go
- Bun:
- Init prints the full "Detected" summary block only in verbose mode; default output keeps setup concise and action-oriented.
- If no family presets are available after fetch, init skips preset selection and uses the runtime base preset.
- When "custom preset reference" is selected, init leaves top-level
presetunset (commented) but still writes top-levelruntime. - For
main, init behavior is:- if adapter inference finds an entrypoint and it differs from preset default
main, write it as top-levelmain; - if inferred
mainmatches preset default (or preset default exists and no inference is available), omit top-levelmain; - prompt only when neither adapter inference nor preset default
mainis available.
- if adapter inference finds an entrypoint and it differs from preset default
If the selected config file already exists:
- Interactive terminal:
tako initasks for overwrite confirmation. - Non-interactive terminal: skips overwrite, leaves the existing file untouched, and prints
Operation cancelled.
Show all commands with brief descriptions.
Show version information (same as --version flag).
Refresh generated files for the current project: tako.d.ts for JS/TS apps (project-specific type augmentation for tako.sh) and tako_secrets.go for Go apps. tako gen and tako g are aliases. For JS/TS projects, tako generate keeps an existing tako.d.ts in app/, src/, or the project root. When creating the file for the first time, it uses an existing legacy tako.gen.ts location when present, otherwise app/, then src/, then the project root. The JS/TS declaration file includes user-defined variable names from [vars] and [vars.<env>] on process.env and import.meta.env, typed as strings. Legacy tako.gen.ts files are removed on regeneration. If a JS/TS project already has <app_root>/channels/ or <app_root>/workflows/ directories, tako generate also scaffolds demo.ts in empty dirs and adds missing default defineChannel(...) or defineWorkflow(...) exports to existing definition files that have no default export yet. Generated channel stubs use the file stem as the initial wire name, but tako generate does not rewrite existing explicit names.
Upgrade the local tako CLI binary to the latest available build.
CLI upgrade strategy:
- Homebrew install detection: runs
brew upgrade tako - Default/fallback: downloads the hosted CLI archive and installs it directly with the same layout as
scripts/install-tako.sh
Start (or connect to) a local development session for the current app, backed by a persistent dev daemon.
--variant(alias--var) runs a DNS variant of the app (e.g.--variant foo→myapp-foo.test).--tunnelstarts the session with a temporary public tunnel URL enabled.tako devis a client: it ensurestako-dev-serveris running, then registers the selected config file with the daemon.- On macOS,
tako devalso ensures the socket-activatedtako-dev-proxyhelper is installed and loaded for loopback-only:80/:443ingress. - On Linux,
tako devensures iptables redirect rules and a loopback alias (127.77.0.1) are configured for portless HTTPS. On NixOS, it prints aconfiguration.nixsnippet instead of imperative setup. - When running from a source checkout,
tako devprefers the repo-localtarget/debug|release/tako-dev-serverbinary. - When running from a source checkout on macOS,
tako devcan also build the repo-localtako-dev-proxybinary when the helper needs installation or repair. - If no local daemon binary exists,
tako devfalls back totako-dev-serveronPATH. - If that fallback binary is missing:
- source checkout flow reports a build hint (
cargo build -p tako-cli --bin tako-dev-server) - installed CLI flow reports a reinstall hint (
curl -fsSL https://tako.sh/install.sh | sh)
- source checkout flow reports a build hint (
- If daemon startup fails,
tako devreports the last lines from{TAKO_HOME}/dev-server.log. tako devwaits up to ~15 seconds for the daemon socket after spawn before reporting startup failure.- The daemon performs an upfront bind-availability check for its HTTPS listen address and exits immediately with an explicit error when that address is unavailable.
- On macOS,
tako devregisters the app with the daemon (selected config path is the unique key, state is persisted in SQLite at{TAKO_HOME}/dev-server.sqlite).- App statuses:
running(actively serving),idle(process stopped, routes retained for wake-on-request),stopped(unregistered, routes removed). - The app starts immediately when
tako devstarts (1 local instance) and transitions to idle after 30 minutes of no attached CLI clients.- After an idle transition, the next HTTP request triggers wake-on-request: the daemon spawns the app process and routes the request once the app is healthy.
- Idle shutdown is suppressed while there are in-flight requests.
- When
Ctrl+cis pressed, Tako unregisters the app (sets status to stopped, removes routes, kills the process). - Pressing
b(background) hands the running process off to the daemon and exits the CLI. The daemon monitors the process and keeps routes active. - Running
tako devagain with the same selected config file attaches to the existing session if the app is running or idle. - Dev logs are written to a shared per-app/per-config stream at
{TAKO_HOME}/dev/logs/{app}-{hash}.jsonl. - Dev channel replay is process-local: the daemon keeps one in-memory replay store per registered app and resets it when
tako-dev-serverrestarts. - Each persisted log record stores a single
timestamptoken (hh:mm:ss) instead of split hour/minute/second fields. - When a new owning session starts, Tako truncates that shared stream before writing fresh logs for the new session.
- Attached clients replay the existing file contents, then follow new lines from the same stream.
- App lifecycle state (
starting,running,stopped, app PID, and startup errors) is persisted to the same shared stream, so attached sessions reconstruct the same status/CPU/RAM view as the owning session. - The CLI prints
App startedonce the daemon has confirmed the app is live. Active routes are shown in the status footer, not in this message.
- The daemon supports multiple concurrent apps and maintains hostname-based routing for
*.test(and*.tako.testas a fallback).- When LAN mode is enabled from the interactive UI (
l), the same registered dev routes are also reachable via.localaliases. Hostnames are rewritten only at the suffix, so subdomains, wildcard hosts, and path-prefixed routes keep the same shape.- Concrete hostnames are advertised to the LAN via mDNS (Bonjour on macOS, Avahi on Linux) so phones and tablets resolve them by name.
- Wildcard routes (e.g.
*.app.test) cannot be advertised via mDNS — the protocol only supports concrete records. They still match at the proxy, so devices with their own DNS server for the subdomain can reach them, but plain mDNS clients (phones) cannot. Tako surfaces a warning under the LAN mode route list pointing to the wildcard routes and suggesting an explicit subdomain route (e.g.api.app.test) as the fix.
- Tunnel mode creates a temporary public HTTPS URL through the Tako tunnel service and forwards it to the app's local dev route. The public hostname uses
{app}-{id}.tako.website;{id}is derived from the app name and the local Tako Identity public key, so the same app gets the same URL when the same identity is available. On macOS, Tako tries to store the identity in iCloud Keychain and falls back to local storage when synced Keychain access is unavailable. Other platforms use local identity storage. The tunnel service issues a nonce and only creates the tunnel after the client signs that nonce with the Tako Identity key, so no login or namespace claim is required.tako dev --tunnelenables tunnel mode during startup.- Pressing
tin the interactive UI toggles tunnel mode for the current app. - Starting a tunnel for the same app and identity replaces any previous active tunnel for that URL.
- One Tako Identity can have up to five active tunnel URLs connected at the same time. Reconnecting or replacing the same app URL does not consume another slot. Starting a sixth active tunnel accepts the new tunnel and closes the oldest active tunnel for that identity; the closed client turns tunnel mode off and prints why it was closed.
- Tunnel URLs do not have a fixed session TTL. Tunnel mode stays enabled until the user turns it off or the app is unregistered. If the local tunnel connection is lost, Tako keeps the URL reserved, marks the tunnel as reconnecting, retries with bounded exponential backoff, and prints a log line when reconnecting starts and when the tunnel reconnects. When a tunnel turns off, Tako prints a log line explaining whether it was turned off by the user, stopped with the app, or closed by the service.
- Inactive or disconnected tunnel URLs return a Tako-styled HTML error page for browser requests, JSON for clients that accept
application/json, and plain text for other clients.
- When LAN mode is enabled from the interactive UI (
- When running in an interactive terminal,
tako devprints a branded header (logo + version + app info) once at startup, then streams logs and status updates directly to stdout.- Native terminal features (scrollback, search, copy/paste, clickable links) are preserved — no alternate screen is used.
- Log levels are
DEBUG,INFO,WARN,ERROR, andFATAL; the level token is colorized using pastel colors (electric blue, green, yellow, red, and purple respectively). - The timestamp token (
hh:mm:ss) is rendered in a muted color. - Log lines are prefixed as
hh:mm:ss LEVEL [scope] message.- Common scopes:
tako(local dev daemon) andapp(the app process). - For app-process output, Tako infers the level from leading tokens like
DEBUG,INFO,WARN/WARNING,ERROR, andFATAL(including bracketed forms such as[DEBUG]), and mapsTRACEtoDEBUG.
- Common scopes:
- App lifecycle state changes (starting, stopped, errors) are printed inline as
── {status} ──lines in the log stream. - The status panel always shows
routes,lan, andtunnelrows.routeslists local HTTPS routes.lanshows an enable hint when off, an active URL with a disable hint when on, or a retry hint after failure.tunnelshows an enable hint when off, a starting/failed state while toggling, an active URL with a disable hint when on, and a reconnecting state with the same URL during transient tunnel service disconnects. - When LAN mode is enabled, Tako prints a QR block for installing the local CA certificate on another device. When tunnel mode is enabled, Tako prints a short public URL block for visibility. When tunnel mode reconnects or turns off, Tako prints a
takolog line with the reconnect status or close reason. - Keyboard shortcuts (interactive terminal only):
rrestart the app processltoggle LAN mode (expose the same routes via.localaliases on the local network)ttoggle tunnel mode (temporary public URL)bbackground the app (hand off to daemon, CLI exits)Ctrl+cstop the app and quit
- When stdout is not a terminal (piped or redirected),
tako devfalls back to plainprintln-style output with no color or raw mode. tako devwatchestako.toml,.tako/secrets.json,<app_root>/channels/,<app_root>/workflows/, and parent directories that can containtako.d.ts(app/,src/, and the project root) so the generated JS/TS declaration file is recreated if removed or edited.- It restarts the app when effective dev environment variables, secrets, channel definitions, or workflow definitions change.
- It updates dev routing without restarting when
[envs.development].route(s)changes.
- Source hot-reload is runtime-driven (e.g. Bun watch/dev scripts); Tako does not watch arbitrary source files for auto-restart.
- HTTPS is terminated by the local dev daemon using certificates issued by the local CA (SNI-based cert selection).
tako devensures daemon TLS files exist at{TAKO_HOME}/certs/fullchain.pemand{TAKO_HOME}/certs/privkey.pembefore spawning the daemon.- The daemon reuses existing TLS files when present.
tako devlistens on127.0.0.1:47831in HTTPS mode.- When
[envs.development].routesis not configured, Tako registershttps://{app}.test:47831/on non-macOS andhttps://{app}.test/on macOS. When the user configures explicit.test/.tako.testroutes, those managed dev routes replace the default entirely — the default{app}.testhost is not added, leaving that slug free for other apps. External development routes (for example a hostname forwarded by Cloudflare Tunnel) are additive: if no managed dev route is configured, Tako still registers the default{app}.testroute alongside the external routes. Both.testand.tako.testDNS zones resolve simultaneously (.tako.testremains available as a DNS fallback; the proxy still only routes hosts that are actually registered).- In LAN mode, managed
.test/.tako.testdev routes are additionally served via.localaliases (for exampleapp.test/api/*also answers onapp.local/api/*). External development routes are routable by the local proxy but are not rewritten to.local, advertised with mDNS, or resolved by Tako DNS. - On macOS, Tako configures split DNS by writing
/etc/resolver/testand/etc/resolver/tako.test(one-time sudo), pointing to a local DNS listener on127.0.0.1:53535. If/etc/resolver/testalready exists and was not created by Tako, Tako skips it and warns about the conflict (.tako.teststill works). - On Linux, systemd-resolved routes both
~testand~tako.testto the local DNS listener. - The dev daemon answers
Aqueries for active*.testand*.tako.testhosts.- On macOS, it maps to a dedicated loopback address (
127.77.0.1) used by the dev proxy. - On non-macOS, it maps to the dedicated loopback alias (
127.77.0.1).
- On macOS, it maps to a dedicated loopback address (
- On macOS,
tako devautomatically installs and repairs a launchd-managed dev proxy when missing (one-time sudo prompt):- Tako also installs a boot-time launchd helper that ensures the dedicated loopback alias (
127.77.0.1) exists before the proxy is re-registered - launchd owns listening sockets only on
127.77.0.1 127.77.0.1:443 -> 127.0.0.1:47831127.77.0.1:80 -> 127.0.0.1:47830(HTTP redirect to HTTPS)
- Tako also installs a boot-time launchd helper that ensures the dedicated loopback alias (
- The dev proxy is socket-activated and may exit after a long idle window; launchd reactivates it on the next request.
- If the dev proxy later appears inactive,
tako devexplains that it is reloading or reinstalling the launchd helper before prompting for sudo. - On macOS, Tako always requires this dev proxy and always advertises
https://{app}.test/(no explicit port). - After applying or repairing the dev proxy, Tako retries loopback 80/443 reachability and fails startup if those endpoints remain unreachable.
- On macOS, Tako probes HTTPS for the app host via loopback and fails startup if that probe does not succeed.
- If the daemon is reachable on
127.0.0.1:47831buthttps://{app}.test/still fails, Tako reports a targeted hint that the local launchd dev proxy is not forwarding correctly. tako devuses routes from[envs.development]when configured; otherwise it defaults to{app}.test.- Dev routes may use any valid hostname. Tako only manages DNS and
.localLAN aliases for.testand.tako.testroutes. - Wildcard dev routes participate in proxy routing, but cannot be advertised with mDNS in LAN mode.
- If configured dev routes contain no managed
.test/.tako.testroutes, Tako keeps the default{app}.testroute and treats the configured external routes as additional host aliases. - Unknown managed local DNS hosts (
.testand.tako.test) return a helpful 421 response that lists registered dev routes. Unknown.localLAN hosts and unknown external hosts return a genericMisdirected Request421 response and do not enumerate registered routes.
- Dev routes may use any valid hostname. Tako only manages DNS and
- The HTTPS daemon listen port for
tako devis fixed at47831.
- In LAN mode, managed
Local CA architecture:
- Root CA generated once on first run; the public cert is stored at
{TAKO_HOME}/ca/ca.crt, the private key is stored beside it as{TAKO_HOME}/ca/ca.keywith mode0600, and system trust is installed once via sudo. - The CA cert/key pair is scoped per
{TAKO_HOME}to avoid cross-home key/cert mismatches. - Leaf certificates generated on-the-fly for each app domain
- Public CA cert available at
{TAKO_HOME}/ca/ca.crt(forNODE_EXTRA_CA_CERTS) - On first run (or whenever not yet trusted),
tako devinstalls the root CA into the system trust store (may prompt for your password) - Before the sudo prompt,
tako devexplains why elevated access is needed and what will change. - No browser security warnings once the CA is trusted
Environment variables:
- Loads from
[vars]+[vars.development]in tako.toml; non-string TOML scalar values are stringified ENV=developmentNODE_ENV=development, plus runtime-specific vars (BUN_ENV=developmentfor Bun)
Stop a running dev app.
- Without arguments: stops the app for the selected config file (default
./tako.toml). - With
name: stops the app with that name. --all: stops all registered dev apps.
List all registered dev apps.
Alias: tako dev ls.
The list output includes the current public tunnel URL when tunnel mode is enabled for an app.
Print a local diagnostic report and exit.
- Reports dev daemon listen info, macOS dev proxy status, and local DNS status.
- On macOS, includes a preflight section with clear checks for:
- dev proxy install status
- dev boot-helper load status
- dedicated loopback alias status
- launchd load status
- TCP reachability on
{loopback-address}:443and{loopback-address}:80
- If the local dev daemon is not running (missing/stale socket), doctor reports
status: not runningwith a hint to starttako dev, and exits successfully.
Show global deployment status from configured servers, with one server block per configured host and one app block per running build nested under each server:
✓ la (v0.1.0) up
┌ dashboard (production) running
│ instances: 2/2
│ build: abc1234
└ deployed: 2026-02-08 11:48:19
────────────────────────────────────────
! nyc (v0.1.0) up
┌ worker (unknown) running
│ instances: 1/1
│ build: old5678
└ deployed: 2026-02-08 11:40:10
┌ worker (unknown) deploying
│ instances: -/-
│ build: new9012
└ deployed: -
Shows server connectivity/service lines and per-build app blocks with heading lines in app-name (environment) state form.
Each app block uses a tree connector (┌ heading, │ detail continuation, └ final deployed line).
Environment is inferred from the deployed app id (app/env); otherwise app status uses unknown.
App state text is color-coded (running success, idle muted, deploying/stopped warning, error error).
Each app block includes instance summary (healthy/total), build, and deployed timestamp (formatted in the user's current locale and local time, without timezone suffix).
tako status and tako servers status print a single snapshot and exit.
With --json, status prints:
{
"ok": true,
"command": "status",
"servers": [
{
"name": "la",
"host": "la.example.com",
"port": 22,
"description": "Primary",
"service_status": "active",
"server_version": "0.0.0-abc1234",
"server_uptime": "12 days",
"process_uptime": "3 hours",
"routes": [{ "app": "dashboard/production", "pattern": "dashboard.example.com" }],
"apps": [
{
"app_name": "dashboard",
"env_name": "production",
"status": {
"service_status": "active",
"server_version": "0.0.0-abc1234",
"app_status": {},
"deployed_at_unix_secs": 1770000000,
"error": null
}
}
],
"error": null
}
]
}Status flow helpers:
tako statusandtako servers statusdo not requiretako.tomland can run from any directory.- Uses global server inventory from
config.toml. - Queries each server through signed Tailscale HTTP remote management.
- If no servers are configured and the terminal is interactive, status offers to run the add-server wizard.
- If no deployed apps are found, status reports that explicitly.
Aliases: tako servers status, tako servers info.
View or stream logs from all servers in an environment.
- Environment defaults to
production. - Environment must exist in the selected config file.
- Fetches from all mapped servers in parallel.
- Includes app stdout/stderr and app-scoped Tako server diagnostics from the app log files.
JS/TS production HTTP entrypoints route
console.*, uncaught exceptions, and unhandled rejections into the same app log stream before exiting. - Human-readable output formats log-file lines into timestamp, level, source, and message
columns. App process sources render as
{instance}; app-scoped server diagnostics render astako. App stderr renders asERROR; structured app JSON logs are flattened into the same view, with embedded newlines kept inside one log entry and indented under the message column. Structuredfields.error.stackvalues render as indented continuation lines. Raw app stderr object dumps are grouped under the first stderr row when they are split across physical log-file lines. Interactive terminals colorize levels and scopes, use the app runtime color for app process sources when known, and render trailing metadata fields such asinstance=...as dim italic text. - Prefixes each line with
[server-name]when multiple servers are present. - Remote fetch/connect failures are reported as command failures; they are not treated as empty logs.
- Remote logs are read over signed HTTP management. History uses one bounded raw-byte fetch from
the app's
previous.logandcurrent.log; streaming mode polls the same endpoint with byte offsets and starts by showing up to the last 10 current-log lines. - With global
--json, history mode emits a single final result object containingok,command,app,environment,days, andlogs. Each entry inlogsis a structured log record. Structured app/worker JSON records are preserved and annotated withsourceandinstance_id;sourceis the app instance id, worker name (defaultrenders asworker), ortakofor app-scoped server diagnostics. Raw app process output and app-scoped Tako diagnostics are wrapped into JSON records with alevelfield. When logs come from multiple servers, records also includeserver.
History mode (default):
- Shows the last
Ndays of logs (default: 3). - Applies
--daysto timestamped app log-file lines. - Consecutive identical messages are deduplicated with a
└─ repeated N times through <timestamp>marker. Same-day repeats showHH:MM:SS;Nincludes the first displayed row. - All lines across servers are sorted by timestamp.
- Displays in
$PAGERif interactive, withless -Ras the default. Diff-only pagers such asdeltafall back toless -R; piped output and--jsonwrite stdout.
Streaming mode (--tail):
- Streams logs continuously until interrupted (
Ctrl+c). --tailconflicts with--days.- With global
--json, streaming mode emits one structured log event per stdout line rather than a final result object. - Consecutive identical messages are deduplicated with a
└─ repeated N times through <timestamp>marker. Same-day repeats showHH:MM:SS;Nincludes the first displayed row.
Logs flow helpers:
- For
production, if no servers are configured and the terminal is interactive, logs offers to run the add-server wizard.
tako servers add [host|admin-user@host] [--name {name}] [--description {text}] [--port {ssh-port}] [--http-port {port}] [--https-port {port}] [--install] [--admin-user {user}]
Add server to global config.toml ([[servers]]).
- With
host: adds directly from CLI args and defaults the server name to the host's first DNS label (my-server.tailnet.ts.netbecomesmy-server). IP addresses and hosts that do not produce a valid server name require--name. - With
admin-user@host: treats the prefix as the admin SSH user for install/repair and stores onlyhost. - Without
host(interactive terminal): launches a guided wizard (host, SSH port, optional SSH passphrase when a default key is encrypted, HTTP/HTTPS ports when installing or starting a stopped install, required server name, optional description) with a finalLooks good?confirmation. ChoosingNorestarts the wizard. - If the derived server name already exists, interactive mode prompts for another name. Non-interactive mode fails and asks for
--name. - The add-server wizard supports
Tabautocomplete suggestions for host/name/port from existing servers and persisted CLI history.- For name/port prompts, suggestions related to the selected host (and selected name for ports) are prioritized first, then global suggestions are shown.
- Successful adds record host/name/SSH-port history in
history.tomlfor future autocomplete. --descriptionstores optional human-readable metadata inconfig.toml(shown intako servers list).--http-portand--https-portset the public proxy ports used whenservers addinstallstako-server; omitted values default to80and443. They are distinct from--port, which remains the SSH port.- Re-running with the same name/host/SSH-port/public-port tuple is idempotent (reports already configured and succeeds).
Tests SSH connection before adding. Connects as the tako user.
During SSH checks, tako servers add also detects and stores target metadata (arch, libc) and the running public proxy ports (http_port, https_port) in the matching [[servers]] entry in config.toml.
If --no-test is used, SSH checks and target detection are skipped; deploy later fails for that server until target metadata is captured by re-adding the server with SSH checks enabled.
If --install is used and tako-server is missing or tako@host is not available, tako servers add connects as the admin SSH user (default root, override with --admin-user) and runs the server installer over SSH in bootstrap-only mode first. Passing admin-user@host is shorthand for setting that admin user and enabling install/repair when needed. Interactive installs prompt for editable HTTP and HTTPS port fields prefilled with 80 and 443 unless --http-port/--https-port were passed.
Before the first service start, add-server install flows use default listener settings. Tako starts the service through the installer service-start path, rechecks tako@host, enrolls the same key for signed remote management, verifies signed HTTP access, and only then writes config.toml.
In the interactive wizard and direct host flow, if tako-server is missing or tako@host cannot be reached, Tako asks whether to install now, prompts for public HTTP/HTTPS ports, and prompts for the admin SSH user.
If tako-server is installed but not yet started (for example after an explicit bootstrap-only install), tako servers add asks whether to start it. In non-interactive install/repair mode (--install or admin-user@host), the service-start step runs automatically with default listener settings.
Remove server from config.toml ([[servers]]).
When name is omitted in an interactive terminal, tako opens a server selector.
In non-interactive mode, name is required.
Confirms before removal. Warns that projects referencing this server will fail.
Aliases: tako servers rm [name], tako servers delete [name].
List all configured servers from global config (config.toml) as a table:
- Name
- Host
- Port
- Optional description
Alias: tako servers ls.
If no servers are configured, tako servers list shows a hint to run tako servers add.
Reload tako-server without downtime by default. --force performs a full service restart and may cause brief downtime for all apps.
Use default reload for normal config refresh and control-plane restarts. Use --force for recovery when graceful reload is not appropriate.
Service-manager reload/restart behavior:
- Default path:
systemctl reload tako-serveron systemd hosts, orrc-service tako-server reloadon OpenRC hosts. Reload sendsSIGHUP; the current process spawns a replacement process, the new process takes over the management socket and listener ports, then the old process drains and exits. --forcepath:systemctl restart tako-serveron systemd hosts, orrc-service tako-server restarton OpenRC hosts.- On systemd hosts, installer configures
KillMode=mixedandTimeoutStopSec=30min, allowing the main server process to receive the first stop signal while child processes still get time to handle graceful shutdown before forced termination. - On OpenRC hosts, installer configures
retry="TERM/1800/KILL/5"in the init script so restart/stop waits up to 30 minutes before forced termination.
tako-server persists app runtime registration (app config and routes) in SQLite under the data directory and restores it on startup so app routing/config survives reloads, restarts, and crashes. Env vars are stored in app.json in the release directory; secrets are stored encrypted (AES-256-GCM) in the same SQLite database using a per-device key. Fresh native app and worker processes receive secrets through the fd 3 bootstrap envelope at spawn time; container instances receive the same envelope through TAKO_BOOTSTRAP_DATA. Secret updates store the new encrypted values, then drain/restart workflow workers and roll HTTP instances so fresh processes receive the new values; secrets never touch disk as plaintext. Each deployed app also gets a persistent runtime data tree under {data_dir}/apps/{app}/{env}/data/:
app/— app-owned data exposed to the process asTAKO_DATA_DIRtako/— Tako-owned per-app internal state
On root-managed production servers, release directories and data/app/ are owned by the per-app Unix user/group and are not readable by other deployed app identities by default. data/tako/ remains owned by tako-server and is not exposed to app code. Native app and worker processes keep fd 3 bootstrap, fd 4 readiness for HTTP instances, and 127.0.0.1 loopback routing, then apply a conservative umask, NoNewPrivileges where supported, dropped ambient capabilities where supported, and per-app resource controls. Container app instances run through Podman with only a loopback host-port publish. Linux cgroup v2 is used when available to assign native app processes to a per-app cgroup with default memory, CPU, and process-count limits; file-descriptor limits are applied per process.
Deleting an app removes the entire {data_dir}/apps/{app} tree after the app is drained and stopped.
During single-host upgrade orchestration, tako-server may enter an internal upgrading server mode that temporarily rejects mutating management commands (release preparation/finalization, deploy, backup_now, restore_backup, scale, stop, delete, rollback, update-secrets) until the upgrade window ends.
Upgrade mode transitions are guarded by a durable single-owner upgrade lock in SQLite so only one upgrade controller can hold the upgrade window at a time.
Upgrade tako-server on one or all configured servers via service-manager reload. When server-name is omitted, all servers are upgraded.
- CLI verifies
tako-serveris active on the host. - CLI installs the new server binary on the host.
- CLI verifies the signed
tako-server-sha256s.txtrelease manifest with an embedded public key, selects the expected SHA-256 for the target archive, and the remote host verifies that SHA-256 before extracting the archive into/usr/local/bin/tako-server. WhenTAKO_DOWNLOAD_BASE_URLis set for a custom release source, CLI skips signature verification for that custom manifest and still verifies the archive checksum after download. - before replacing the active binary, the remote host checks the extracted binary for missing runtime libraries and installs the libvips runtime only when the new binary needs it
- custom
TAKO_DOWNLOAD_BASE_URLoverrides must usehttps://; non-HTTPS overrides are rejected unlessTAKO_ALLOW_INSECURE_DOWNLOAD_BASE=1is set explicitly for local testing - CLI metadata downloads and remote host archive downloads use
GH_TOKENwhen set, falling back toGITHUB_TOKEN, for GitHub-hosted release URLs only
- CLI verifies the signed
- CLI acquires the durable upgrade lock (
enter_upgrading) and sets server mode toupgrading. - CLI signals the primary service with:
systemctl reload tako-serveron systemd hosts, orrc-service tako-server reloadon OpenRC hosts. Both paths sendSIGHUPfor graceful reload, start a replacement process before the old process exits, and run with root privileges (root login or sudo-capable user).
- CLI waits for the primary management socket to report ready.
- CLI releases upgrade mode (
exit_upgrading).
tako servers upgrade requires a supported service manager on the host (systemd or OpenRC).
Failure behavior:
- If failure happens before the reload signal, CLI performs best-effort cleanup (exits upgrade mode).
- Upgrade keeps the previous on-disk
tako-serverbinary until the replacement process reports ready. If readiness does not arrive, the previous binary is restored. - If the reload was sent but the socket did not become ready within the timeout, CLI warns that upgrade mode may remain enabled until the primary recovers.
Remove tako-server and all data from a remote server.
- If
nameis omitted in an interactive terminal, prompts to select from configured servers. - Displays what will be removed (services, binaries, data, sockets, service files) and asks for confirmation (skipped with
-y). - SSHes into the server with root/sudo privileges and:
- Stops and disables
tako-serverandtako-server-standbyservices (systemd and OpenRC). - Removes systemd service files, drop-ins, and OpenRC init scripts.
- Runs
systemctl daemon-reloadon systemd hosts. - Removes binaries:
/usr/local/bin/tako-server,tako-server-service,tako-server-install-refresh. - Removes data directory (
/opt/tako/) and the management socket directory (/var/run/tako/).
- Stops and disables
- Removes the server from the local
config.tomlserver list.
Set a Tako-owned provider credential for a deployed app environment. Alias: tako creds set ....
{name}is a provider credential name. Credential names are lowercased before validation, soPOSTGRES_URLis stored aspostgres_url. Supported values:ssl.cloudflareandpostgres_url. Interactive terminals can select a supported credential when{name}is omitted.--env {environment}selects the deployment environment.developmentis rejected because provider credentials are only used by deploy. Interactive terminals can choose an existing non-development environment when omitted; the selector offersproductionby default and does not create new environments.--expires-on {when}optionally records the date when the credential expires. Interactive runs prompt when the flag is omitted. Non-interactive runs may omit it.YYYY-MM-DDis stored as-is;in N daysis normalized to the UTC date N days from now;never, a blank prompt, or an omitted flag stores noexpires_onfield. Timestamp values are rejected.
The command ensures the environment has an encryption key and stores the encrypted value plus optional expires_on metadata in .tako/secrets.json under that environment's credentials object. Provider credentials are not exposed to app code, are not included in generated secret types, and are not pushed by tako secrets sync. Deploy sends required provider credentials to servers only through the specific deployment binding that needs them, such as Cloudflare SSL, Let’s Encrypt DNS-01 certificate issuance, or shared runtime state storage. postgres_url selects shared Postgres channel/workflow storage. Deploy fails before build/deploy work starts if a required credential is missing or expired, warns if it expires within 30 days, and fails during remote prepare if the target server cannot read a Cloudflare zone needed for Let’s Encrypt DNS-01 or cannot validate a supported Cloudflare SSL token.
Remove a Tako-owned provider credential from one environment. Alias: tako creds rm ....
Aliases: tako credentials remove ..., tako credentials delete ..., tako credentials del ....
List supported Tako-owned provider credential names and whether each configured environment has a value set. Values are never printed. Running tako credentials without a subcommand shows the same overview.
Aliases: tako credentials ls, tako credentials show, tako creds list.
Remove the local Tako CLI and all local data.
- Gathers removal targets:
- User-level: config directory, data directory, CLI binaries (
tako,tako-dev-server,tako-dev-proxy). - System-level (requires sudo): platform-specific services and config installed by
tako dev:- macOS: dev proxy LaunchDaemons (
sh.tako.dev-proxy,sh.tako.dev-bootstrap),/Library/Application Support/Tako/,/etc/resolver/test,/etc/resolver/tako.test, CA certificate in system keychain, loopback alias127.77.0.1. - Linux: systemd service (
tako-dev-redirect.service), systemd-resolved drop-in (tako-dev.conf), CA certificate in system trust store, iptables NAT redirect rules, loopback alias127.77.0.1.
- macOS: dev proxy LaunchDaemons (
- User-level: config directory, data directory, CLI binaries (
- If nothing exists, reports "nothing to remove" and exits.
- Displays what will be removed (including system items that require sudo) and asks for confirmation (skipped with
-y). - Best-effort stops the dev server (unregisters all dev apps).
- Removes system-level items via
sudo(best-effort), then removes user-level directories and binaries. - Reports success or partial removal if some items could not be deleted.
Set/update secret for an environment.
When --env is omitted in an interactive terminal, Tako opens an environment wizard. The first step shows the default environments (development, production), any environments already declared in tako.toml or .tako/secrets.json, and a New environment option. Choosing New environment prompts for the environment name in the next wizard step. In non-interactive mode, --env is required.
After the environment is resolved, Tako prompts for the secret value with masked input in an interactive terminal, or reads a single line from stdin in non-interactive mode. It also asks when the secret expires; pressing Enter skips expiry. --expires-on {when} can provide the expiry directly. YYYY-MM-DD is stored as-is; in N days is normalized to the UTC date N days from now; never, a blank prompt, or an omitted flag stores no expires_on field. Timestamp values are rejected. If the secret already exists in the selected environment during an interactive run, Tako asks for overwrite confirmation before prompting for the new value. Stores encrypted value plus optional plaintext expires_on metadata locally in .tako/secrets.json. Tako does not write .tako/secrets.json until the environment wizard, value prompt, and optional expiry prompt have completed.
Uses the environment's cached key from iCloud Keychain through the signed Tako.app CLI, or from Tako's data directory at keys/{key_id}. If a local key file exists and iCloud Keychain is unavailable during a read, Tako uses the local key file. If the environment has no key yet, Tako creates a random key. On macOS interactive runs, Tako offers iCloud Keychain storage, which requires the signed app bundle. If the entitlement is unavailable while saving a new key to iCloud Keychain, the command fails before writing .tako/secrets.json.
When --sync is provided, immediately syncs secrets to all servers in the target environment after the local change, triggering a rolling restart of running instances. Deploy checks app secret expiry before build/deploy work starts, fails if a selected environment secret is expired, and warns if a selected environment secret expires within 30 days.
Alias: tako secrets add ....
Remove secret from environment.
Removes from local .tako/secrets.json. Omitting --env removes the secret from all environments.
When --sync is provided, immediately syncs secrets to servers after the local change. If --env is specified, syncs to that environment; otherwise syncs to all environments.
Aliases: tako secrets remove ..., tako secrets delete ..., tako secrets del ....
List all secrets with presence table across environments.
Shows which secrets exist in which environments. Warns about missing secrets. Never displays values.
Aliases: tako secrets ls, tako secrets show.
Sync local secrets to servers.
Source of truth: local .tako/secrets.json.
By default, sync processes all environments declared in tako.toml.
When --env is provided, sync processes only that environment.
For each target environment, sync decrypts with the cached key from iCloud Keychain through the signed Tako.app CLI, or from Tako's data directory at keys/{key_id}. If a local key file exists and iCloud Keychain is unavailable during a read, Tako uses the local key file.
Shows a spinner with the total number of target servers while syncing, and reports the elapsed time on completion.
Sync flow helpers:
- If no servers are configured and the terminal is interactive, sync offers to run the add-server wizard.
- Environments with no mapped servers are skipped with a warning.
- Sync sends
update_secretstotako-server; it does not write remote.envfiles. Secrets updates reconcile the app's workflow runtime and rolling-restart HTTP instances so fresh native processes receive the new bootstrap envelope via fd 3 and fresh container instances receive it throughTAKO_BOOTSTRAP_DATA.
Export a self-contained key bundle to clipboard.
Reads the environment's cached key from iCloud Keychain through the signed Tako.app CLI, or from Tako's data directory at keys/{key_id}, requires macOS user authentication on macOS when reading from Keychain, and copies a single exported key string to the clipboard. If a local key file exists and iCloud Keychain is unavailable during a read, Tako uses the local key file. The string is base64url-encoded JSON containing version, id, and key, so it can be imported without specifying an environment.
When --env is omitted in an interactive terminal, Tako opens the environment wizard. In non-interactive mode, --env is required.
Import a self-contained exported key string.
In interactive mode, asks for the key source:
Exported key: prompts for an exported key string with masked input. The payload contains the key id, so no environment is needed;--envcan name the target environment and validates that the bundle matches it.Passphrase: prompts for an environment and passphrase. Tako derives the environment key from the passphrase and the environment key id. If the environment does not have a key id yet, Tako creates one and saves it to.tako/secrets.jsonafter the passphrase flow completes. If existing secrets cannot be decrypted with the passphrase, interactive mode prompts again withInvalid passphrase; non-interactive mode fails without saving the key.
In non-interactive mode, tako secrets key import --env {environment} reads a single exported key string from stdin by default. Omitting --env still imports the key and matches an existing environment by key id when possible. Pass --passphrase --env {environment} to import a passphrase from stdin. Imported keys are stored under Tako's data directory at keys/{id} by default. On macOS interactive runs, Tako offers iCloud Keychain storage, which requires the signed Tako.app CLI. If the entitlement is unavailable while saving to iCloud Keychain, the import fails before writing a local key file or updating .tako/secrets.json. If the current project has an environment matching the imported id, reports that environment name; otherwise reports the imported id.
Attach an object storage binding to this app.
tako storages add uploads \
--env production \
--resource prod_uploads \
--provider s3 \
--bucket app-uploads \
--endpoint https://<account>.r2.cloudflarestorage.com \
--region auto \
--public-base-url https://cdn.example.com/uploadsOptions:
--env {environment}defaults toproduction.--resource {name}sets the backing S3 resource name. It defaults to the binding name fors3; forlocal, omit it or set it tolocal.--provider {s3|local}defaults tos3.--bucket {bucket}is required fors3.--endpoint {https-url}is required fors3. R2 is configured asprovider = "s3"with the R2 S3-compatible endpoint.--region {region}defaults toauto.--access-key-id {value}and--secret-access-key {value}are optional fors3; interactive runs prompt when omitted.--expires-on {when}optionally records the date when S3 credentials expire; interactive S3 runs prompt when omitted, and non-interactive S3 runs may omit it.YYYY-MM-DDis stored as-is;in N daysis normalized to the UTC date N days from now;never, a blank prompt, or an omitted flag stores noexpires_onfield. Timestamp values are rejected.--force-path-stylesigns path-style object URLs instead of virtual-hosted bucket URLs fors3.--public-base-url {https-url}enables public object URLs for helpers that requestpublic: trueons3bindings.
The command writes the environment binding to tako.toml. For s3, it also writes top-level resource metadata and encrypted credentials plus optional expires_on metadata to .tako/secrets.json under the selected environment's storages map. For local, it writes the binding to the built-in local resource and no [storages.local] table; local storage has no user-configured path or credentials. Deploy validates that every non-development storage binding references either a declared S3 resource or the built-in local resource, that S3 resources have unexpired credentials when expiry is known, warns when S3 credentials expire within 30 days, checks that credentials do not exist for unbound resources, and rejects local storage on multi-server deploy environments. There is no separate storage sync command.
Set or rotate encrypted credentials for an existing top-level S3 storage resource without attaching it to the app runtime.
tako storages credentials r2 \
--env production \
--access-key-id ... \
--secret-access-key ...Options:
--env {environment}defaults toproduction.--access-key-id {value}and--secret-access-key {value}are optional; interactive runs prompt when omitted.--expires-on {when}records optional credential expiry metadata with the same rules astako storages add.
The command requires [storages.<resource>] to exist and to be S3-compatible. It writes only .tako/secrets.json; it does not add or change [envs.<env>].storages, so the resource is not exposed as tako.storages.<name> unless a separate app binding exists.
Manage app data backups for the current project.
tako backups now --env production
tako backups list --env production
tako backups status --env production
tako backups download b123 --env production --server la --output ./backup.tar.zst.enc
tako backups restore b123 --env production --server la --yestako backups now [--env <env>] [--server <server>]creates a backup immediately on the selected server(s).tako backups list [--env <env>] [--server <server>]lists remote backup index entries, newest first.tako backups status [--env <env>]reports whether backups are enabled per mapped server, plus last/next backup timing and retention.tako backups download <backup-id> [--env <env>] [--server <server>] [--output <path>]downloads one encrypted backup object. If the environment maps to multiple servers,--serveris required. The default output path is<backup-id>.tar.zst.enc. The output file must not already exist.tako backups restore <backup-id> [--env <env>] [--server <server>] [--yes|-y]stops the selected server's app, replaces its data tree with the archive contents, reconciles workflows, and restarts according to the app's desired instance count. If the environment maps to multiple servers,--serveris required.
All backup commands default to production, require project context, and target the remote deployment id {app}/{env}. Backup commands use signed HTTP remote management, not SSH.
Build and deploy application to environment's servers.
When --env is omitted, deploy targets production.
Deploy target environment must be declared in tako.toml ([envs.<name>]) and must define route or routes.
development is reserved for tako dev and cannot be used with tako deploy.
In interactive terminals, deploying to production requires an explicit confirmation when the environment is implicit. Passing --env production, --yes, or -y skips that confirmation because the target is explicit.
Deploy flow helpers:
- If no servers are configured and the terminal is interactive, deploy offers to run the add-server wizard before continuing.
- For
production, if[envs.production].serversis empty:- with one global server: deploy selects it and writes it to
[envs.production].serversintako.toml - with multiple global servers (interactive terminal): deploy asks you to pick one, then writes it to
[envs.production].servers
- with one global server: deploy selects it and writes it to
- Interactive deploy progress:
- after config/server/build planning is known, interactive pretty output renders tasks and sub tasks instead of a static plan box
- waiting tasks render as muted
○ - deploy renders
Connecting to <server>as a single sub task when there is one target server; with multiple target servers it renders aConnectingtask with one sub task per server - if there is only one obvious build task, deploy renders it as a single
Buildingsub task line - pending pretty task rows render with a
...suffix - succeeded sub tasks hide the
✔icon (render with a blank icon slot) only when their parent task also succeeded; when the parent failed, was cancelled, or is still running, succeeded sub tasks keep their✔so the completed work stays visible. Failed (✘), cancelled (⊘), skipped (⏭), running (spinner), and pending (○) sub tasks always keep their icons - cancelled and skipped rows render fully muted (icon, label, and detail); accent color is reserved for live or successfully completed rows
- after planning completes, deploy starts the pretty
ConnectingandBuildingsections together - deploy does not keep startup metadata summaries inside the live tree
- deploy renders one task per target server, with sub tasks for
Uploading,Preparing, andStarting - deploy adds a blank line after each top-level pretty task section (
Connecting,Building, eachDeploying to ...) - sub task failures may render their related error detail on an indented line below the failed sub task
- if a connection check or build step fails, deploy aborts the remaining incomplete pretty task-tree rows and marks them as
Abortedinstead of leaving them pending - verbose and CI deploy output stay transcript-style and only print work as it is happening
- When
releaseis configured for the resolved env, deploy adds a release sub-step under each server'sPreparingtask. The leader's row readsRunning release command; followers' rows readWaiting for release commandand resolve once the leader finishes. On leader failure, followers' rows are markedCancelledwith aleader faileddetail.
Steps:
- Pre-deployment validation (secrets present and unexpired, S3 credentials present and unexpired for selected storage bindings and configured backup storage, provider credentials present and unexpired when Cloudflare SSL is selected, Let’s Encrypt routes include wildcards, or shared channel/workflow storage is required, multi-server channel deploys require
postgres_url, multi-server JS workflow deploys requirepostgres_urlunless every workflow definition setslocal: true, multi-server Go workflow deploys requirepostgres_url, warnings for app/S3/provider credentials expiring within 30 days, server target metadata present/valid for all selected servers) - Resolve source bundle root (git root when available; otherwise app directory)
- Resolve app subdirectory from the selected config file's parent directory relative to source bundle root
- Resolve deploy runtime
main(mainfromtako.toml; otherwise manifest main such aspackage.jsonmain; otherwise presetmain, with JS index fallback order:index.<ext>thensrc/index.<ext>forts/tsx/js/jsxwhen applicable) - Resolve app preset (top-level
presetintako.toml), fetching unpinned official aliases frommaster - Prepare build dir: copy project from source root into
.tako/build(respecting.gitignore), symlinknode_modules/directories from original tree, and preserve other symlinks as symlinks - Run build commands in build dir:
- Resolve stage list by precedence:
[[build_stages]]→[build](single-stage form) → runtime default stage → no-op - Run resolved stages in declaration order (
installthenrunper stage) - Merge configured assets into app
public/ - Verify resolved runtime
mainexists in the built app directory - Save resolved runtime version into
app.json(runtime_versionfield) for server-side version pinning
- Resolve stage list by precedence:
- Archive build dir (excluding
node_modules/) as deploy artifact- Source and build archives preserve symlinks as links instead of following them; directory symlinks are not expanded into the artifact
- Version format: clean git tree =>
{commit}; dirty git tree =>{commit}_{source_hash8}; no git commit =>nogit_{source_hash8} - Best-effort local artifact cache prune runs before builds (retention: 90 target artifacts; orphan target metadata is removed)
- Package filtered artifact tarball using include/exclude rules and store in local cache
- On all servers in parallel: upload artifact, extract, and prepare runtime
- Require
tako-serverto be pre-installed and running on each server - Ask the server for a release upload plan over signed HTTP. If the release is not already present, upload the target-specific artifact to
POST /release-artifact; the server verifies the declared size and SHA-256 digest, extracts the artifact into the release directory, and links release logs to the app's shared log directory. - Query server for the app's current secrets hash; if it matches the local secrets hash, skip sending secrets (server keeps existing). If hashes differ (or app is new), include decrypted secrets in the deploy command.
- Include the decrypted SSL binding in the deploy command. If the binding has a Cloudflare token,
tako-serverstores it encrypted for certificate operations for that deployed app. Otherwise, the server clears stored SSL credentials for that app. - Include the decrypted backup binding and backup keys when
[envs.<env>].backupis configured. If backup is omitted,tako-serverclears any previous backup config for that deployed app. tako-serveracquires a per-app deploy lock in memory, reads non-secret runtime/app config from releaseapp.json, creates per-app runtime data directories, and either runs the native runtime plugin's production install command or builds the container image- If another deploy for the same app environment is already running on that server, the deploy command fails immediately with a retry message
- Require
- Run release command on the leader server (when configured):
- If
releaseis configured for the resolved env, the leader server runssh -c "<command>"once inside the new release directory. Followers'Preparingtask blocks until the leader publishes its result. - On success, all servers proceed into rolling update (existing behavior).
- On failure (non-zero exit, timeout, or signal), deploy aborts on
every server. The existing partial-release cleanup removes the new
release directory on each server. The
currentsymlink is not updated; old instances keep serving.
- If
- Rolling update and finalize on all servers:
tako-serverperforms first start or rolling update- Update
currentsymlink and clean up old releases (>30 days or over 50 total releases) - If backups are enabled, create a post-deploy app data backup on each finalized server
Version naming:
- Clean git tree:
{commit_hash}(e.g.,abc1234) - Dirty working tree:
{commit_hash}_{content_hash}(first 8 chars each) - No git commit/repo:
nogit_{content_hash}(first 8 chars)
Source deploy contract:
- Deploy archive source is the app's source bundle root (git root when available; otherwise selected-config parent directory).
- Deploy target app path is the selected config file's parent directory relative to the source bundle root.
- Build uses a build dir: copies project from source root into
.tako/build(respecting.gitignore), symlinksnode_modules/from the original tree (build tools read but don't modify), runs build commands in the build dir, then archives the result excludingnode_modules/. Source and build archives preserve symlinks as symlinks; directory symlinks are not followed, and source hashes track symlink targets for cache invalidation. - These paths are always force-excluded from the deploy archive:
.git/,.tako/,.env*,node_modules/. Additional exclusions come from[build].excludeand.gitignore. - Servers receive prebuilt native artifacts or source-backed container artifacts and do not run native app build stages during deploy. After extracting a native artifact,
tako-serverruns the runtime plugin's production install command (e.g.bun install --production) before starting instances unless the manifest has an explicitstartcommand. After extracting a container artifact,tako-serverbuilds the configured container image with Podman. Production install runs with the release env plus minimal process env (PATH,HOMEwhen available); it does not inherit arbitrarytako-serverservice environment variables. Whentako-serveris root, each deployed app id ({app}/{env}) gets a deterministic Unix user and primary group (tako-<hash>). Native production install, release commands, HTTP instances, and workflow workers for that app run as that per-app user, with the sharedtako-appgroup retained only as a supplementary group for access to Tako's internal socket. If the per-app identity or shared group cannot be resolved or created, roottako-serverfails closed instead of running app code as root. - Build logic runs in the build dir against the resolved stage list (precedence:
[[build_stages]]→[build]→ runtime default). Each stage runsinstallthenrunin declaration order. - Deploy uses the version pin from
runtime = "<id>@<version>"when set. Otherwise it resolves runtime version by running<tool> --versiondirectly, falling back tolatest. Explicitstartreleases without a runtime skip runtime version probing. - Artifact include precedence: in simple build mode,
build.include->**/*. In multi-stage mode,**/*is used (stages control output viaexcludepatterns only). - Asset roots are preset
assetsplus top-levelassets(deduplicated), merged into apppublic/after build with ordered overwrite. - Target artifacts are cached locally by deterministic key and reused across deploys when build inputs are unchanged.
- Cached artifacts are validated by checksum/size before reuse; invalid cache entries are rebuilt automatically.
- Deploy artifacts include the canonical
app.jsonused bytako-serverat runtime. - Release
app.jsoncontains resolved runtime metadata (runtime,main,package_manager), non-secret env vars, JSapp_root, environment idle timeout, and optional release metadata (commit_message,git_dirty) used bytako releases list. - Deploy does not write a release
.envfile; non-secret env vars live in releaseapp.json, and secrets are stored encrypted in SQLite on the server. Native spawns receive runtime vars (TAKO_BUILD,TAKO_DATA_DIR, andTAKO_APP_ROOTfor JS apps) when spawning HTTP instances and workflow workers. Container HTTP and workflow spawns receiveTAKO_BUILD, app vars,TAKO_BOOTSTRAP_DATA,HOST=0.0.0.0, andPORT=3000in v0; workflow containers also receive a mounted internal socket. - Deploy queries each server's secrets hash before sending the deploy command. If the hash matches the local secrets, secrets are omitted from the payload and the server keeps its existing secrets. This avoids unnecessary secret transmission and ensures new servers or servers with stale secrets are automatically provisioned.
- Deploy sends backup storage only through the backup binding. It is stored encrypted server-side and is never included in the fd 3 app bootstrap envelope unless it is also listed under
[envs.<env>].storages. - Deploy requires valid
archandlibcmetadata in each selected[[servers]]entry. - Deploy does not probe server targets during deploy; missing/invalid target metadata fails deploy early with guidance to remove/re-add affected servers.
- Deploy pre-validation still fails when target environment is missing secret keys used by other environments.
- Deploy pre-validation warns (but does not fail) when target environment has extra secret keys not present in other secret environments.
Deploy lock (server-side):
tako-serverserializes deploys per deployed app id ({name}/{env}) using an in-memory lock- A second deploy command for the same app environment on the same server fails immediately with
Deploy already in progress for app '{app}'. Please wait and try again. - No
.deploy_lockdirectory is written to disk - Restarting
tako-serverclears the lock; the interrupted deploy fails and can be retried without manual lock cleanup - The same per-app deploy lock guards the release command; a concurrent deploy attempt for the same app sees the existing "Deploy already in progress" error.
Rolling update (per server):
- Start new instance
- Wait for health check pass (30s timeout)
- Add to load balancer
- Gracefully stop old instance (drain connections, 30s timeout)
- Repeat until all instances replaced
- Update
currentsymlink to the new release directory - Clean up releases older than 30 days or over 50 total releases
Rolling update target counts use the app's current desired instance count stored on that server (not old+new combined counts).
When the stored desired instance count is 0, rolling deploy still starts one warm instance for the new build so traffic is immediately served after deploy.
On failure: Automatic rollback - kill new instances, keep old ones running, return error to CLI.
App start command (current):
- Release
app.jsonis required for app startup. - tako-server derives the start command from the runtime plugin, resolving the SDK entrypoint from the app dir or parent dirs:
bun:bun run <resolved-entrypoint> <app.json.main>node:node --experimental-strip-types <resolved-entrypoint> <app.json.main>go:<app.json.main>(compiled binary runs directly — no runtime binary or SDK entrypoint wrapper needed)- if the entrypoint is missing, warm-instance startup fails with an explicit error
- Unknown runtime values in
app.jsonare rejected with an explicit unsupported-runtime error.
Partial failure: If some servers fail while others succeed, deployment continues. Failures are reported at the end.
Disk space preflight: Before uploading artifacts, tako deploy asks each target server to check free space under its configured data directory.
- Required free space is based on archive size plus unpack headroom.
- If free space is insufficient, deploy fails early with required vs available sizes.
Failed deploy cleanup: If a deploy fails after creating a new release directory, tako deploy automatically removes that newly-created partial release directory before returning an error.
Deployment target:
- If
[envs.<env>].serversexists intako.toml→ deploy to those servers - If deploying to
productionwith no[envs.production].serversmapping:- exactly one server in
config.toml[[servers]]→ use it and persist it into[envs.production].servers - multiple servers in
config.toml[[servers]](interactive terminal) → prompt to select one and persist it into[envs.production].servers
- exactly one server in
- If no servers exist in
config.toml[[servers]]→ fail with hint to runtako servers add <host> - Otherwise, require explicit
[envs.<env>].serversmapping in tako.toml
List release/build history for the current app across mapped environment servers.
- Environment defaults to
production. - Environment must exist in
tako.toml([envs.<name>]). - Server targeting follows
[envs.<name>].serversfor the selected environment. - Output is release-centric and sorted newest-first:
- line 1: release/build id + deployed timestamp
- when deployed within 24 hours, append a muted relative hint in braces (for example
{3h ago})
- when deployed within 24 hours, append a muted relative hint in braces (for example
- line 2: commit message + cleanliness marker (
[clean],[dirty], or[unknown])
- line 1: release/build id + deployed timestamp
[current]marks the release currently pointed to by servercurrentsymlink.- Commit metadata (
commit_message,git_dirty) comes from releaseapp.jsonwhen available; older releases may show[unknown]or(no commit message).
Roll back the current app/environment to a previously deployed release/build id.
- Environment defaults to
production. - In interactive terminals, rollback to
productionrequires explicit confirmation unless--yes(or-y) is provided. - Rollback is executed per mapped server in parallel.
- tako-server performs rollback by reusing current app routes/env/secrets/scaling config and switching runtime path/version to the target release, then running the standard rolling-update flow.
- Partial failures are reported per server; successful servers remain rolled back.
Change the desired instance count for a deployed app.
instancesis the desired instance count per targeted server.- In project context, Tako resolves the app name from the selected config file (or selected-config parent directory fallback when top-level
nameis unset). - In project context, app-scoped server commands target the remote deployment identity
{app}/{env}. - Outside project context,
--appis required. Use--app <app> --env <env>or pass the full deployment id as--app <app>/<env>. - When
--serveris omitted,--envis required and Tako scales every server listed in[envs.<env>].servers. - When
--serveris provided, Tako scales only that server. - In project context,
tako scale --server <server>defaults toproduction. - When both
--envand--serverare provided, the server must belong to that environment. - Scale uses persisted runtime app state on the server, so the desired instance count survives deploys, rollbacks, and server restarts.
- Scale fails if the requested count is above the app's effective server maximum.
- Scaling to
0drains and stops excess instances after in-flight requests finish (or drain timeout).
Delete a deployed app from one specific environment/server deployment target.
Target selection behavior:
tako deleteremoves exactly one deployment target, not every server in an environment.- In an interactive terminal, when Tako needs more information it first loads deployment state with a
Getting deployment informationspinner. - In project context (selected config file present), Tako resolves the app name from the selected config:
- with neither
--envnor--server, Tako prompts with deployed targets likeproduction from hkg - with
--envonly, Tako prompts for a matching server - with
--serveronly, Tako prompts for a matching environment - with both
--envand--server, Tako skips discovery and goes straight to confirmation
- with neither
- Outside project context, Tako discovers deployed targets across configured servers and includes app selection when needed because there is no local app context.
- In non-interactive mode,
--yes,--env, and--serverare all required. Outside project context, those flags must still identify a single deployed target; otherwise the command fails with guidance to rerun interactively from the app directory or with-c.
Validation:
- In project mode,
--envmust be declared in the selected config file ([envs.<name>]). --servermust name a configured server fromconfig.toml[[servers]].developmentis reserved fortako devand cannot be used withtako delete.
Delete confirmation:
- Interactive terminals require explicit confirmation unless
--yes(or-y) is provided. - The confirmation prompt always names the app, environment, and server being removed.
- Non-interactive terminals require
--yes.
Steps:
- Discover deployed app ids from configured servers over signed HTTP remote management.
- Send
deleteover signed HTTP to the selectedtako-serverfor the remote deployment id{app-name}/{env-name}. tako-serverdrains and stops the app, removes runtime state, and removes{data_dir}/apps/{app-name}/{env-name}from disk.
- Interactive single-target deletes show a spinner while the selected server is being cleaned up.
- Delete is idempotent for absent app state (safe to re-run for cleanup).
Aliases: tako rm, tako remove, tako undeploy, tako destroy.
Apps specify routes at environment level (not per-server). Routes support:
- Exact hostname:
api.example.com - Wildcard subdomain:
*.api.example.com - Hostname + path:
api.example.com/api/* - Wildcard + path:
*.example.com/admin/*
Validation rules:
- Routes must include hostname (path-only routes such as
"/api/*"are invalid) - Exact path routes normalize trailing slash (
example.com/apiandexample.com/api/are equivalent) - Each
[envs.{env}]can have eitherrouteorroutes, not both [envs.{env}]accepts environment behavior keys (route/routes,servers,idle_timeout, andrelease); env vars belong in[vars]/[vars.{env}]- Each non-development environment must define
routeorroutes - Empty route lists are invalid for non-development environments
- Development routes may use any valid hostname. Tako manages DNS and
.localLAN aliases only for.testand.tako.testroutes; external dev routes must be pointed at the dev proxy by the user.
Apps with routes:
- Each app specifies its routes
- Requests matched to most specific route (exact > wildcard, longer path > shorter)
- For static asset requests (paths with a file extension),
tako-serverserves files directly from the deployed apppublic/directory when present. - For path-prefixed routes (for example
example.com/app/*), static asset lookup also tries the prefix-stripped path (for example/app/assets/main.js->/assets/main.js) so public assets work on subpaths. - Conflict detection during deploy prevents overlapping routes
- Requests without a matching route return
404
Wildcard subdomains:
*.example.comroutes to app, app handles tenant logic based on subdomain
- Parse incoming request (Host header, path)
- Match against deployed apps' routes
- Select most specific match
- Route to app's round-robin load balancer
- Return 404 if no match
Users run a server setup script (or equivalent manual steps) to:
- Create dedicated OS users/groups:
takofor SSH access and runningtako-server, plus the sharedtako-appgroup used by app and worker processes to reach the internal socket - Install
tako-serverto/usr/local/bin/tako-server - Install and enable a host service definition for
tako-server:- systemd unit on systemd hosts
- OpenRC init script on OpenRC hosts
- Create and permissions required directories:
- Data dir:
/opt/tako - Socket dir:
/var/run/tako
- Data dir:
- Prepare only installer-managed ownership:
- The installer sets ownership/mode on the data root, socket root, and files it writes directly.
- The installer does not recursively change ownership under deployed app releases; deploy-time release creation owns those trees.
Recommended: run the hosted installer script on the server (as root):
sudo sh -c "$(curl -fsSL https://tako.sh/install-server.sh)"Custom public proxy ports can be supplied as installer args:
curl -fsSL https://tako.sh/install-server.sh | sudo sh -s -- --http-port 8080 --https-port 8443Installer SSH key behavior:
- If
TAKO_SSH_PUBKEYis set, installer uses it and skips prompting. - If unset and a terminal is available, installer prompts for a public key to authorize for user
tako(includingsudo sh -c "$(curl ...)"and common piped installs such ascurl ... | sudo sh) and re-prompts on invalid input until a valid SSH public key line is provided. - If terminal key input cannot be read, installer attempts to reuse the first valid key from the invoking
SUDO_USER~/.ssh/authorized_keys; if unavailable, installer continues without key setup and prints a warning with aTAKO_SSH_PUBKEYrerun hint. - If unset and no terminal is available, installer attempts the same invoking-user key fallback before warning and continuing without key setup.
- CLI SSH connections require host key verification against
~/.ssh/known_hosts(or configured SSH keys directory); unknown/changed host keys are rejected. - Installer detects host target (
arch+libc) and downloads matching artifact nametako-server-linux-{arch}-{libc}(supported:x86_64/aarch64withglibc/musl). - Installer ensures
nc(netcat) is available so CLI management commands can talk to/var/run/tako/tako.sock. - Installer installs the host libvips runtime used by the built-in image optimizer before starting
tako-server. - Installer ensures basic networking tools are available for server operation.
- Installer creates the
takoOS user and the sharedtako-appgroup.tako-serverruns astako; root-managed app and worker processes run as per-app Unix users created from the deployed app id and keeptako-apponly as a supplementary group. If a roottako-servercannot resolve or create the per-app identity or shared group, it refuses to run production install, release commands, HTTP instances, or workflow workers as root. - Installer prepares the
/opt/takoand/var/run/takoroots without recursively traversing existing app releases. - Installer installs restricted maintenance helpers and scoped sudoers policy so the
takoSSH user can perform non-interactive server upgrade/reload operations. - When the installer installs or accepts
TAKO_SSH_PUBKEY, it also enrolls that public key for signed remote management in/opt/tako/management-authorized-keys. - Installer supports systemd and OpenRC hosts.
- Installer defaults to service-start mode (
TAKO_RESTART_SERVICE=1): it installs/refreshes the binary, users, directories, helpers, sudoers policy, and service definition, then enables and starts or reloadstako-server. - Bootstrap-only mode (
TAKO_RESTART_SERVICE=0) installs or refreshes files without enabling, starting, or reloadingtako-server.tako servers add --installandadmin-user@hostadd/repair flows run bootstrap-only mode first, then run service-start mode to start the service with default listener settings. - Installer writes
tako-server --http-port {port} --https-port {port}into the host service definition. Service-start mode prompts forHTTP port [80]:andHTTPS port [443]:when a terminal is available; bootstrap-only mode never prompts for ports and uses existing service ports or80/443unless--http-port/--https-portorTAKO_HTTP_PORT/TAKO_HTTPS_PORTare provided. Reinstalls use the existing service ports as prompt defaults when present. - If service public ports change, installer performs a full service restart so the service manager command line takes effect. Otherwise, active services use the normal zero-downtime reload path.
- For normal service installs, installer configures remote management on the server's Tailscale IP. It detects that address with
tailscale ip -4or usesTAKO_MANAGEMENT_HOSTwhen set. If no Tailscale IP is available, install fails with:Remote management requires Tailscale so Tako can keep server control traffic private by default. - Installer configures service capability support for privileged binds, app-user switching, and stopping app processes after switching users:
- systemd:
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID CAP_KILL,CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID CAP_KILL - non-systemd/OpenRC hosts: installer applies
setcap cap_net_bind_service,cap_setuid,cap_setgid,cap_kill=+ep /usr/local/bin/tako-server; install fails if the capability cannot be granted
- systemd:
- Installer configures a high file-descriptor limit for the
tako-serverservice so the proxy and spawned app processes can handle high connection counts without inheriting a low default shell limit. - Installer configures graceful stop semantics:
- systemd:
KillMode=mixed,TimeoutStopSec=30min - OpenRC:
retry="TERM/1800/KILL/5"
- systemd:
- Installer verifies
tako-serveris active after service start; if startup fails, installer exits non-zero and prints available service diagnostics.
Reference scripts in this repo:
scripts/install-tako-server.sh(source for/install-server.sh, alias/server-install.sh)scripts/install-tako.sh(source for/install.sh)
Runtime binary download engine:
tako-serverdownloads runtime binaries directly from upstream releases using download specs in runtime plugins (no external version manager dependency).- Supports zip and tar.gz archive formats with SHA-256 checksum verification.
- Downloaded binaries are cached at
{data_dir}/runtimes/{tool}/{version}/. - GitHub-backed runtime version checks and runtime downloads use
GH_TOKENwhen set, falling back toGITHUB_TOKEN. - Supports musl detection for Alpine and other musl-based systems.
- If the runtime plugin has no download spec, the binary must be available on PATH.
Default behavior (no custom installer ports needed):
- HTTP: port 80
- HTTPS: port 443
- Remote management HTTP: port 9844, bound only to a private Tailscale address by normal server installs
- Data:
/opt/tako - Socket:
/var/run/tako/tako.sock - SSL: Let’s Encrypt ACME by default; Cloudflare Origin CA per deployed app when selected
- Renewal check interval: Every 12 hours (renews certificates 30 days before expiry)
- HTTP requests redirect to HTTPS (
307, non-cacheable) by default. - Exception:
/.well-known/acme-challenge/*stays on HTTP. - When HTTPS uses a non-default public port, deploy summaries include that port in printed route URLs and HTTP redirects target the configured HTTPS port.
- Forwarded HTTPS metadata (
X-Forwarded-ProtoandForwarded: proto=https) is honored only from loopback peers, Cloudflare peers, or peers listed in servertrusted_proxy.trusted_cidrs. Direct clients cannot bypass HTTPS redirects by spoofing those headers. - For trusted forwarded peers, requests for private/local hostnames (
localhost,*.localhost, single-label hosts, and reserved suffixes like*.local) are treated as already HTTPS when proxy proto metadata is missing, so local dev proxy setups do not enter redirect loops. - Proxied upstream responses are not stored in a shared edge cache by default. Static assets and image optimization have separate cache behavior.
- Deployed proxied app responses are compressed at the browser-facing proxy layer when the client advertises Brotli or gzip and the upstream response is safe to transform. Brotli is preferred over gzip when negotiation quality is tied; zstd is not advertised by default.
- Compression applies to successful text, JSON, JavaScript, CSS, XML, WASM, and SVG responses with a known
Content-Lengthof at least 1024 bytes. Tako removes the originalContent-Length,ETag, andContent-MD5, setsContent-Encoding, and ensuresVary: Accept-Encodingis present for responses whose body can vary by client encoding support. - Compression skips HEAD requests, upgrades/WebSockets, SSE, unknown-length or streaming responses, already encoded responses,
Cache-Control: no-transform, small responses, and unsupported binary content such as common image, video, audio, archive, and font payloads. Skipped eligible responses still getVary: Accept-Encodingwhen another client encoding would change the emitted body. - Local development proxy traffic stays uncompressed by Tako so debugging response bodies remains straightforward.
- Each proxied or Tako-handled app request gets a request ID.
tako-serverreuses a safe incomingX-Request-IDwhen present, otherwise generates one, forwards it to app upstreams, and includes it in app-scoped proxy diagnostics visible throughtako logs. - App-scoped proxy diagnostics include request ID, host, method, path, matched route, app id, instance id when selected, status, HTTPS state, handler path (
proxy,static,image,channel,redirect, oracme), handler/cache result, total latency, route lookup time, cold-start wait time when a scale-to-zero request wakes an app, upstream response-header latency when proxied, and response compression fields for algorithm, skip reason, whetherVarywas required, and uncompressed/compressed byte counts when available. - Per-IP rate limiting: maximum 2048 concurrent active requests per client IP by default; excess requests receive
429.TAKO_MAX_REQUESTS_PER_IPcan override the server process default for controlled benchmarks or deliberately tuned deployments. - Maximum HTTP request body size: 128 MiB; larger requests receive
413. - Maximum channel WebSocket frame payload size: 128 MiB; larger frames are rejected and the socket closes.
- Production browser-facing
tako-server5xx responses use generic reason-phrase bodies such asInternal Server Error,Bad Gateway,Service Unavailable, orGateway Timeout; detailed app-scoped startup, proxy, channel storage, and static file diagnostics are recorded in the app log stream instead of returned in response bodies. - After a request matches an app route,
/_tako/*is reserved for Tako-owned public endpoints./_tako/channels/<name>serves durable channels,/_tako/imageserves public optimized images, and/_tako/storages/<binding>/<key>serves signed local storage GET/PUT requests. Other request paths are served as static assets when a matching file exists inpublic/, then proxied to the app.
Public image optimization:
- JavaScript code can call
imageUrl(source, opts?)fromtako.shto build a public optimizer URL. It is synchronous and does not sign. Omitted options usewidth: 1200, default quality75, and negotiated output from[images].formats. - The helper returns a canonical query URL:
/_tako/image?src=<source>&w=<width>[&q=<quality>][&f=<format>]. It emitsqonly when the requested quality differs from75. - JavaScript code can call
imageSrcSet(source, opts)fromtako.shto build responsive public image sources for plain<img>usage. It returns{ src, srcSet, sizes }, usesimageUrlfor each candidate, and supportslayout = "constrained" | "full-width" | "fixed"plus optional explicitwidthsandsizes.constrainedderivessizes = "(min-width: <width>px) <width>px, 100vw",full-widthderivessizes = "100vw", andfixedderivessizes = "<width>px". Generated widths must be allowed public optimizer widths. - Public optimizer requests fail closed.
srcandware required;qandfare optional; duplicate or unknown query params are rejected. Width must be in[images].sizes, quality must be in[images].qualities, and format must be in[images].formats. Whenfis omitted, Tako chooses the first configured format supported by the requestAcceptheader, falling back to the first configured format. The defaults are sizes[320, 640, 960, 1200, 1920], qualities[75], and formats["webp"]. - Local sources are path-only URLs such as
/assets/hero.jpg. Local paths are allowed by default withlocal_patterns = ["/**"]; setting[images].local_patternsreplaces that default. In deploys, local paths resolve from the app's deployedpublic/directory first, then from the matched app backend; in dev, local paths are fetched from the active app backend. - Remote sources must be
httporhttpsURLs and must match[images].remote_patterns. Remote patterns are glob-like URL strings, not regular expressions. If the pattern omits a protocol, Tako allows bothhttpandhttps. Remote URLs reject unsupported schemes, userinfo, fragments, recursive image optimizer URLs, private/local hosts and IPs, private/local DNS results, and redirects. - Public image responses use
Cache-Control: public, max-age=31536000, immutable. Responses whose output format is negotiated fromAcceptalso useVary: Accept, and image response ETags are derived from the emitted bytes and content type. This browser/CDN response cache policy is separate from Tako's origin source and transform caches. - In deploys,
tako-servervalidates the request before using origin caches. Source bytes are kept in a short-lived in-memory cache scoped by app, release root, and source identity, so one page requesting the same source with multiple transform parameters reuses the source load. The source cache is bounded to 10 seconds, 64 MiB, and 256 entries, and concurrent loads for the same source key are deduplicated in process. - Successful transformed variants are cached on local disk under the system temp directory, such as
/tmp/tako-image-cache. Transform cache keys include the app name, release root, source bytes, and transform options, so changed source files and different app releases produce new variants. The transform cache is best-effort, local to each server, and pruned after writes by age and size: entries older than 30 days are removed, then the oldest remaining files are removed until the cache is within its filesystem-based cap. The cap is 5% of the filesystem containing the cache directory, clamped between 1 GiB and 4 GiB; if filesystem size cannot be read, the cap falls back to 2 GiB. Concurrent cache misses for the same transform key are deduplicated in process so only one worker computes that variant while the other requests wait for the result. - In deploys,
tako-serverruns libvips resize and encode work through a managed internal pool of isolated child worker processes. The pool is shared by apps on the same server, keeps workers warm for recent transform traffic, and scales idle workers back to zero. Workers run with conservative concurrency and reduced OS priority where supported so image transforms do not take the main proxy and app process budget by default. Cache hits and duplicate in-flight misses do not enter the worker queue. New cache misses wait for an available worker slot until the bounded queue is full; once full, new transform attempts fail with503 Service Unavailable. The worker execution timeout starts only after the transform begins. If an image worker crashes or times out, the server process stays up and the transform failure fallback rules apply. Transform fallbacks emit app-scoped warnings that are visible throughtako logs. - The optimizer uses libvips for resize and encode work. It enforces source byte and decoded image limits, preserves aspect ratio, does not upscale, and strips source metadata from transformed output. EXIF orientation is applied to the pixels before output is encoded, but EXIF, XMP, ICC profiles, comments, and other source metadata are not retained. Public requests downscale to at most
width. Current transforms accept JPEG, PNG, GIF, WebP, and AVIF sources by file signature, notContent-Typealone. Animated GIF and WebP sources preserve animation for width-only resizes,containresizes, center-cover crops, and smart-cover crops when emitted as WebP. AVIF output is available for still transforms; animated sources that request AVIF fall back to WebP because the current libvips HEIF save path supports multipage AVIF output, not browser-timed AVIF animation. The optimizer emits WebP by default; AVIF is available when requested and allowed by[images].formats. - If a verified source is loaded successfully but resize/encode work fails,
tako-serverserves the original source bytes when the source response has animage/*content type. Fallback responses useCache-Control: private, no-storeso a transient transform failure cannot be stored as the long-lived public optimized variant. Source validation, source-size, decoded-size, and worker queue saturation failures do not fall back. - Failed image optimizer responses use non-shared error caching (
Cache-Control: private, no-store).
Object storage runtime bindings:
- Storage bindings are configured with
tako storages addor[envs.<env>].storagesintako.tomland delivered to JavaScript apps ontako.storages.<name>.tako generateaugments theTakoStoragesinterface fromtako.toml; development names are preferred when present, otherwise generation uses the union of all environments. await tako.storages.uploads.createDownloadUrl(key, options?)creates a private signedGETURL. Fors3, it uses SigV4. Forlocal, it returns a signed app-local/_tako/storages/uploads/<key>URL served from the app data directory.expiresInSecondsdefaults to3600and is capped at604800. Download options may set S3 response content type/disposition overrides. Passingpublic: truereturnspublic_base_url + keywhen ans3binding has apublic_base_url.await tako.storages.uploads.createUploadUrl(key, options?)creates a private signedPUTURL. Fors3,contentTypesigns thecontent-typeheader, so upload clients must send the same header. Forlocal, the SDK returns the same app-local object route and signs thePUTmethod into the token.await tako.storages.uploads.createImageUrl(key, imageOptions?)returns a private signed object URL when no image transform options are supplied. With{ public: true }and ans3storagepublic_base_url, it returns the public optimizer URL for that object. Private storage image transforms are a separate feature; for now transform options without public storage access fail with guidance to usecreateDownloadUrl.await tako.storages.uploads.createImageSrcSet(key, imageSrcSetOptions)returns public responsive image sources for a storage object when called with{ public: true }and the binding haspublic_base_url. Private storage image srcsets are a separate feature; for now they fail with guidance to usecreateDownloadUrl.- Object keys must be non-empty relative keys and cannot start with
/. S3-compatible endpoints must use HTTPS. R2 is configured asprovider = "s3"with the R2 endpoint and usuallyregion = "auto".
/opt/tako/config.json — server-level configuration:
{
"server_name": "prod",
"trusted_proxy": {
"proxy_protocol": false,
"trusted_cidrs": ["127.0.0.1/32"],
"client_ip_headers": ["x-forwarded-for", "forwarded"]
}
}server_name— identity label for Prometheus metrics (defaults to hostname if absent).trusted_proxyremains an advanced server-level escape hatch for PROXY protocol deployments and non-loopback trusted front proxies. It is not configured by the CLI. App/environment-level source-IP behavior is selected throughsource_ip.trusted_cidrsis required whenproxy_protocol = trueorclient_ip_headersis set. Supported header names arecf-connecting-ip,x-forwarded-for, andforwarded. The same trusted-peer boundary controls whether forwarded HTTPS metadata affects redirects and upstream request headers.- Written by the installer for server identity. Read by
tako-serverat startup.
Server identity: tako-server creates a stable Ed25519 identity at {data_dir}/identity.key and writes the public key to {data_dir}/identity.pub. The private key is mode 0600, is preserved across restarts/upgrades, and is removed only by full server uninstall. hello and server_info include the OpenSSH SHA-256 fingerprint so the CLI can identify the server during add/probe flows.
Remote management: Remote management requires Tailscale so Tako can keep server control traffic private by default. Normal server installs configure tako-server to listen for private HTTP management traffic on port 9844 on the Tailscale address. The HTTP management API uses the same typed Command -> Response protocol as the Unix socket:
POST /rpcwith JSON command bodies handles small management commands.- HTTP
/rpcaccepts unsignedhelloandserver_infoprobes. All other commands require a signed request from an enrolled SSH key. Requests include the enrolled key fingerprint, timestamp, nonce, and SSH signature over the RPC body plus Tako's management-auth context. Replayed nonces and stale timestamps are rejected. POST /release-artifactaccepts deploy artifacts as a streamed request body. The request is signed over an upload descriptor containing app id, release version, byte size, and SHA-256 digest; the server verifies the received size and digest before extracting the release.POST /logsreturns raw app log bytes with offset headers. The request is signed over app id, current/previous offsets, optional history cutoff, and byte limit.- Signed HTTP commands reuse the existing dispatcher; bulk deploy artifacts and logs use dedicated byte-body endpoints instead of the JSON RPC path.
- The Unix management socket remains the local server IPC path. SSH remains setup/recovery, not the normal remote management transport.
tako servers addexpects the host to be the server's Tailscale MagicDNS name or Tailscale IP. MagicDNS hostnames default the local server name to the first DNS label; IP addresses require--name. It verifies the host resolves to a Tailscale address, verifiestako@hostSSH recovery access, enrolls the SSH key that authenticated that connection, probes private HTTP management withhelloandserver_info, verifies signed HTTP access, and refuses to writeconfig.tomlif any check fails.- App-scoped runtime commands (
deploy,status,logs,scale,releases,backups,delete, andsecrets sync) use signed HTTP remote management. SSH is not used for normal app/runtime management.
tako servers reloadperforms a zero-downtime control-plane reload by default (systemctl reload tako-serveron systemd,rc-service tako-server reloadon OpenRC).--forceperforms a full service restart instead.tako servers upgradeperforms an in-place upgrade via service-manager reload (systemctl reload tako-serveron systemd,rc-service tako-server reloadon OpenRC) with root privileges (root login or sudo-capable user). Reload uses temporary process and listener overlap until the replacement process reports ready.- Management socket uses a symlink-based path: the active server creates a PID-specific socket (
tako-{pid}.sock) and atomically updates thetako.socksymlink on ready, so clients always connect to the current process. - Restart/stop still honor graceful shutdown semantics from the host service manager (systemd or OpenRC as described above).
/opt/tako/
├── config.json
├── identity.key
├── identity.pub
├── state.sqlite
├── runtimes/
│ └── {tool}/{version}/ # Downloaded runtime binaries
├── acme/
│ └── credentials.json
├── certs/
│ ├── {domain}/
│ │ ├── fullchain.pem
│ │ └── privkey.pem
└── apps/
└── {deployment-id}/
├── current -> releases/{version}
├── data/
│ ├── app/
│ └── tako/
├── logs/
│ └── current.log
└── releases/{version}/
└── build files...
App log files contain app stdout/stderr plus app-scoped Tako server diagnostics. Each app keeps
current.log and the previous rotated file.
tako-server socket:
- Symlink path:
/var/run/tako/tako.sock(always points to the active server socket) - PID-specific socket path:
/var/run/tako/tako-{pid}.sock(created by active server; symlink updated atomically on ready) - Used by local server IPC and app/workflow internal commands. Normal remote CLI management uses signed HTTP.
Remote management HTTP:
tako-serverlistens for management RPC over HTTP on port9844for Tailscale-reachable server status and deploy operations.- Only
helloandserver_infoare public probes. All other RPCs require SSH-key-signed headers, a fresh timestamp, and a non-replayed nonce against the server'smanagement-authorized-keysfile. - Management RPC request bodies are capped at 1 MiB.
- Deploy artifact uploads use
POST /release-artifact, signed over upload metadata rather than the full artifact body; the server verifies the streamed body size and SHA-256 digest before extracting it. - Log reads use
POST /logs, signed over request metadata and returned as raw bytes with cursor headers.
Public proxy listeners:
tako-server --http-port {port}controls the public HTTP listener.tako-server --https-port {port}controls the public HTTPS listener.- The installer owns these args in normal service installs;
tako servers add --install --http-port {port} --https-port {port}passes them through to the remote installer.
App instance upstream transport:
- Native TCP over loopback
tako-serversetsPORT=0andHOST=127.0.0.1; the SDK binds to an OS-assigned port- The SDK signals readiness by writing the bound port to fd 4
tako-serverdelivers the per-instance internal auth token on the fd 3 bootstrap envelope (see below); the SDK uses it for health-probe authentication- Used by: tako-server to proxy HTTP requests and probe health
- Container TCP over loopback
tako-serverstarts Podman with a server-assigned loopback host port published to container port3000- The container receives
HOST=0.0.0.0,PORT=3000, andTAKO_BOOTSTRAP_DATA - Readiness is the first successful SDK-authenticated response on the configured health path
Native HTTP instances and workflow workers receive the same app/runtime environment, except HTTP-only bind vars (PORT, HOST) and per-instance CLI args. Container HTTP instances receive the app/runtime environment plus TAKO_BOOTSTRAP_DATA. In production, spawned native app and worker processes start from a cleared service environment; Tako preserves only minimal process env (PATH, HOME when available) before applying the app/runtime variables below.
| Name | Used by | Meaning | Typical source |
|---|---|---|---|
ENV |
app + worker | Active environment name | Set by Tako in both dev and deploy (development, production, staging, etc.). |
PORT |
app | Listen port for HTTP server | Native: 0 in both dev and deploy; the SDK binds to an OS-assigned port and reports it to Tako via fd 4. Container: 3000. |
HOST |
app | Listen host for HTTP server | Native: 127.0.0.1 in both dev and deploy. Container: 0.0.0.0. |
TAKO_APP_NAME |
app + worker | App identity used by the SDK to tag internal-socket RPCs | Set by both spawners (tako-server and tako-dev-server). In deploy this is the deployment id ({app}/{env}); internal HTTP hosts use the base {app} segment. |
TAKO_INTERNAL_SOCKET |
app + worker | Path to the shared internal unix socket for workflow enqueue/signal and channel publish | Set by both spawners. Together with TAKO_APP_NAME this must always be set as a pair; the SDK asserts this at boot. |
TAKO_DATA_DIR |
app + worker | Persistent app-owned runtime data directory | Set by Tako in dev and native deploys; points to the app's data/app directory. Not set for container releases in v0. |
TAKO_APP_ROOT |
app + worker | JavaScript app root for channels/ and workflows/ discovery |
Set for JS apps from tako.toml app_root; defaults to src. |
NODE_ENV |
app + worker | Node.js convention env | Set by runtime adapter / server (development or production). |
BUN_ENV |
app + worker | Bun convention env | Set by runtime adapter (development or production). |
TAKO_BUILD |
app + worker | Deployed build/version identifier | Written into release app.json by tako deploy; tako-server reads it from the manifest and passes it as an env var at spawn. |
| user-defined | app + worker | User config vars | From app.json in the release dir. Native secrets + internal token pass via fd 3 bootstrap envelope. Container bootstrap data passes through TAKO_BOOTSTRAP_DATA. |
Instance identity (CLI args, not env vars): tako-server passes per-instance identity to the SDK entrypoint as command-line arguments:
--instance <id>— 8-character nanoid instance identifier
The SDK parses this from process.argv (JS) or os.Args (Go) at startup and exposes it through the internal status endpoint and health-check responses. Build/version identity comes from TAKO_BUILD.
Bootstrap envelope (fd 3): For native releases, tako-server opens a pipe on fd 3 of every spawned app and worker process. The pipe carries one JSON object:
{
"token": "<per-instance internal auth token>",
"secrets": { "KEY": "value", ... },
"storages": {
"uploads": {
"provider": "s3",
"bucket": "app-uploads",
"endpoint": "https://<account>.r2.cloudflarestorage.com",
"region": "auto",
"access_key_id": "<access key id>",
"secret_access_key": "<secret access key>",
"force_path_style": false,
"public_base_url": "https://cdn.example.com/uploads"
}
}
}The SDK checks fd 3 first at startup and closes it when present. For container processes, where fd 3 does not cross the container boundary in v0, the SDK falls back to the same envelope in TAKO_BOOTSTRAP_DATA and removes that variable from the process environment after startup. The token authenticates Host: <app>.tako requests (health probes, channel auth callbacks). Secrets populate tako.secrets; storage bindings populate tako.storages. Backup storage is deliberately not included here unless the same resource is separately bound under [envs.<env>].storages. Generated tako.d.ts augments both project-specific surfaces. The fd 3 pipe is always present for native dev/prod processes — in dev mode with no secrets or storages, the envelope is {"token": "...", "secrets": {}, "storages": {}}.
CLI → tako-server (management commands):
hello(capabilities / protocol negotiation; CLI sends this before other commands):
{ "command": "hello", "protocol_version": 0 }Response:
{
"status": "ok",
"data": {
"protocol_version": 0,
"server_version": "0.1.0",
"capabilities": [
"on_demand_cold_start",
"idle_scale_to_zero",
"scale",
"upgrade_mode_control",
"server_runtime_info",
"release_history",
"rollback",
"backups"
],
"server_identity": "SHA256:..."
}
}server_info(returns runtime config + upgrade mode):
{ "command": "server_info" }enter_upgrading/exit_upgrading(durable single-owner lock transitions):
{ "command": "enter_upgrading", "owner": "upgrade-prod-..." }{ "command": "exit_upgrading", "owner": "upgrade-prod-..." }prepare_release(download runtime and install production dependencies for a release; called beforedeployso that the deploy step only does app registration and instance startup):
{
"command": "prepare_release",
"app": "my-app/production",
"path": "/opt/tako/apps/my-app/production/releases/1.0.0"
}prepare_release_upload(returns the server release path and whether the artifact needs to be uploaded):
{
"command": "prepare_release_upload",
"app": "my-app/production",
"version": "1.0.0"
}prepare_deploy(validates and stages deploy metadata that must be checked from the server, including Cloudflare SSL credentials; called after artifact upload and before release preparation or release commands):
{
"command": "prepare_deploy",
"app": "my-app/production",
"version": "1.0.0",
"path": "/opt/tako/apps/my-app/production/releases/1.0.0",
"routes": ["api.example.com", "*.example.com/admin/*"],
"ssl": {
"provider": "letsencrypt",
"cloudflare_api_token": "..."
}
}cleanup_prepared_deploy(clears staged deploy metadata after a failed deploy attempt without deleting the release artifact):
{
"command": "cleanup_prepared_deploy",
"app": "my-app/production",
"version": "1.0.0"
}cleanup_release(removes a newly-created partial release after deploy failure):
{
"command": "cleanup_release",
"app": "my-app/production",
"version": "1.0.0"
}finalize_release(pointscurrentat the deployed release and prunes old or excess releases):
{
"command": "finalize_release",
"app": "my-app/production",
"version": "1.0.0"
}check_deploy_space(checks free bytes under the server data directory before artifact upload):
{ "command": "check_deploy_space", "min_free_bytes": 268435456 }deploy(includes route patterns, source-IP mode, optional secrets payload, optional storage bindings, and SSL provider selection; env vars are read fromapp.jsonin the release dir). Whensecretsorstoragesare omitted ornull, the server keeps existing values for the app. When Cloudflare credentials were staged byprepare_deploy,deployreferences that staged binding by sending the same provider withssl.cloudflare_api_tokenomitted. If no Cloudflare credential is required andssl.cloudflare_api_tokenis absent ornull, the server clears app SSL credentials:
{
"command": "deploy",
"app": "my-app/production",
"version": "1.0.0",
"path": "/opt/tako/apps/my-app/production/releases/1.0.0",
"routes": ["api.example.com", "*.example.com/admin/*"],
"source_ip": "direct",
"secrets": {
"DATABASE_URL": "...",
"API_KEY": "..."
},
"storages": {
"uploads": {
"provider": "s3",
"bucket": "my-app-prod",
"endpoint": "https://example.r2.cloudflarestorage.com",
"region": "auto",
"access_key_id": "...",
"secret_access_key": "...",
"force_path_style": false
}
},
"ssl": {
"provider": "cloudflare"
},
"backup": {
"retention_days": 30,
"backup_keys": [
{
"id": "backup-key-0123456789abcdef",
"key_base64": "..."
}
],
"storage": {
"provider": "s3",
"bucket": "my-app-prod",
"endpoint": "https://example.r2.cloudflarestorage.com",
"region": "auto",
"access_key_id": "...",
"secret_access_key": "...",
"force_path_style": false
}
}
}The backup field is optional. When present, the server stores the private backup target and backup encryption keys encrypted for that deployed app. When absent or null, the server clears backup configuration for the app.
backup_now,list_backups,backup_status,backup_download_url, andrestore_backup:
{
"command": "backup_now",
"app": "my-app/production",
"backup": null
}{ "command": "list_backups", "app": "my-app/production" }{ "command": "backup_status", "app": "my-app/production" }{
"command": "backup_download_url",
"app": "my-app/production",
"backup_id": "b1710000000-AbCdEf12"
}{
"command": "restore_backup",
"app": "my-app/production",
"backup_id": "b1710000000-AbCdEf12"
}backup_now stores the provided backup binding when present, then creates and uploads an encrypted private archive for the app's durable data on that server. list_backups reads the remote backup index. backup_status reports enabled/disabled state, retention, last backup, and next due time. backup_download_url returns a short-lived private archive URL. restore_backup stops the app on that server, verifies the encrypted archive SHA-256, decrypts it, replaces the app data tree, clears transient channel replay storage, reconciles workflows, and restarts according to the app's desired instance count.
scale(updates the desired instance count for an app on one server):
{ "command": "scale", "app": "my-app/production", "instances": 3 }get_secrets_hash(returns the SHA-256 hash of an app's current secrets; used by deploy to skip sending secrets when unchanged):
{ "command": "get_secrets_hash", "app": "my-app/production" }run_release(run a one-shot release command on the leader server before rolling update):
{
"command": "run_release",
"app": "my-app/production",
"version": "abc1234",
"path": "/opt/tako/apps/my-app/production/releases/abc1234",
"command_line": "bun run db:migrate",
"vars": {},
"secrets": {}
}The server validates the app name and release version, acquires the per-app deploy lock, derives base env from release app.json, overlays command vars, injects TAKO_BUILD, TAKO_DATA_DIR, and command secrets, then runs sh -c "<command_line>" in the release directory from a cleared service environment and the app's per-app Unix identity on root-managed production servers. Parent PATH is inserted only when the app/release env did not already provide it. Deploy sends the freshly decrypted secrets in the run_release payload so first deploys and secret rotations run against the same env the new HTTP instances will receive. Success returns exit metadata; non-zero exit or timeout returns an error response with a stderr tail.
Server-side validation on deploy and app-scoped commands:
-
appis the deployment id used on the server. CLI app-scoped commands send{app}/{env}. Each segment must be normalized ([a-z][a-z0-9-]{0,62}with no trailing-). -
versionmust be a simple release id (letters/digits/.-_, no path separators). -
pathmust resolve under<data-dir>/apps/<app>/releases/. -
routes(returns app → routes mapping used for conflict detection/debugging):
{ "command": "routes" }inject_challenge_token(test/support command for HTTP-01 challenge serving):
{ "command": "inject_challenge_token", "token": "abc", "key_authorization": "abc.123" }list_releases(returns release/build history for an app):
{ "command": "list_releases", "app": "my-app/production" }rollback(roll back an app to a previous release/build id):
{ "command": "rollback", "app": "my-app/production", "version": "abc1234" }stop(stop a running app):
{ "command": "stop", "app": "my-app/production" }status(get status of a specific app):
{ "command": "status", "app": "my-app/production" }list(list all deployed apps with their status):
{ "command": "list" }delete(remove app state/routes):
{ "command": "delete", "app": "my-app/production" }update_secrets(update secrets for a deployed app; refreshes workflow workers and triggers rolling restart):
{ "command": "update_secrets", "app": "my-app/production", "secrets": { "KEY": "value" } }SDK/app → tako-server (internal socket commands):
- From any Tako-managed app or worker process:
enqueue_run,signal, andchannel_publish. - From workflow worker processes:
register_schedules,claim_run,heartbeat_run,save_step,complete_run,cancel_run,fail_run,defer_run, andwait_for_event. - Every internal command carries an
appfield so the shared internal socket can route commands for every deployed app.
Instance communication model:
- App and workflow worker processes run as the per-app Unix user for their deployed app id and do not connect to the management socket. The shared
tako-appgroup is retained only for internal socket permissions. If a roottako-servercannot resolve or create the per-app identity, spawning fails closed instead of running app code as root. tako-servercontrols lifecycle directly (spawn/stop/rolling update). Startup readiness is signaled by the SDK via fd 4; ongoing health is verified via active HTTP probing.- Native app processes receive
PORT=0andHOST=127.0.0.1, bind to an OS-assigned loopback port, and write the actual port to fd 4. The server then routes traffic and health probes to that endpoint. - Container app processes run through Podman with a server-assigned loopback host port published to container port
3000. The container receivesHOST=0.0.0.0,PORT=3000, andTAKO_BOOTSTRAP_DATA; readiness is the first successful SDK-authenticated health probe. - Secrets are passed to native instances via fd 3 (file descriptor 3) at spawn time. The server creates a pipe, writes the JSON bootstrap envelope to the write end, and the child process reads fd 3 at startup before any user code runs. EBADF on fd 3 means the process is not running under Tako (dev mode). Container instances receive the same envelope through
TAKO_BOOTSTRAP_DATA. - Secret updates (
update_secretscommand) store new secrets in SQLite, drain/restart any workflow worker for the app, and trigger a rolling restart for HTTP instances; fresh native processes receive updated bootstrap data via fd 3 and fresh container instances receive it throughTAKO_BOOTSTRAP_DATA.
Active HTTP probing is the source of truth for instance health:
- Probe interval: 1 second steady-state, dropped to 100 ms while any instance is still in startup (Starting/Ready, not yet Healthy). The fast startup tier collapses cold-start probe slack from up to 1 s to ~100 ms without paying high-frequency probes at steady state.
- Probe endpoint: App's configured health check path (default:
/status) withHost: <app>.tako, where<app>is the base app name (for exampledashboard.tako) - Transport: Probes use the instance's private TCP endpoint.
- Process exit fast path: Before each probe,
try_wait()checks if the process has exited. If so, the instance is immediately marked dead without waiting for the probe timeout. - Failure threshold: 1 failed probe is tolerated, 2 consecutive failures mark the instance unhealthy, and 3 consecutive failures mark it dead and trigger replacement. A successful probe resets the failure count.
- Recovery: Single successful probe resets failure count and restores to healthy
Tako-server performs health checks against the deployed app process:
GET /status
Host: dashboard.tako
X-Tako-Internal-Token: <instance-token>
Expected response:
{
"status": "healthy",
"app": "dashboard",
"version": "abc1234",
"instance_id": "a1b2c3d4",
"pid": 12345,
"uptime_seconds": 3600
}The SDK wrappers implement this endpoint automatically. The edge proxy does not reserve or bypass Host: <app>.tako routes.
The expected response includes the same X-Tako-Internal-Token header value. The SDK wrappers enforce and echo this token automatically.
Container health checks use the same health path, host header, and internal token echo as native releases.
Tako-server exposes a Prometheus-compatible metrics endpoint for observability.
Endpoint: http://127.0.0.1:9898/ (localhost only, not publicly accessible)
CLI flag: --metrics-port <port> (default: 9898, set to 0 to disable request/upstream metrics collection and the endpoint)
Exposed metrics:
| Metric | Type | Labels | Description |
|---|---|---|---|
tako_http_requests_total |
Counter | server, app, status |
Total proxied requests, grouped by status class (2xx/3xx/4xx/5xx) |
tako_http_request_duration_seconds |
Histogram | server, app |
End-to-end proxy request latency distribution |
tako_upstream_request_duration_seconds |
Histogram | server, app |
Upstream-only latency (proxy → origin → response headers); subtract from end-to-end to get proxy overhead |
tako_http_active_connections |
Gauge | server, app |
Currently active connections |
tako_cold_starts_total |
Counter | server, app |
Total cold starts triggered (scale-to-zero apps) |
tako_cold_start_duration_seconds |
Histogram | server, app |
Cold start duration distribution (records on success and failure) |
tako_cold_start_failures_total |
Counter | server, app, reason |
Cold start failures by reason (spawn_failed, instance_dead) |
tako_tls_handshake_failures_total |
Counter | server, reason |
TLS handshake failures by reason (no_sni, cert_missing) |
tako_instance_health |
Gauge | server, app, instance |
Instance health status (1=healthy, 0=unhealthy) |
tako_instances_running |
Gauge | server, app |
Number of running instances |
All metrics carry a server label (machine hostname) so multi-server deployments are distinguishable without scraper-side relabeling. A single scrape returns data for all deployed apps on that server.
Only proxied requests (routed to a backend) are measured for the request/upstream histograms. ACME challenges, direct static asset responses, and unmatched-host 404s are excluded. tako_tls_handshake_failures_total only tracks Tako-visible reasons (missing SNI, cert lookup miss); raw TLS protocol failures inside Pingora's listener are not counted.
Usage with monitoring platforms:
- Self-hosted Prometheus/Grafana: Add
127.0.0.1:9898as a scrape target. - Hosted platforms (Grafana Cloud, Datadog, etc.): Install the platform's agent on the server, configure it to scrape
http://127.0.0.1:9898/metrics. - Tailscale/WireGuard: Expose port 9898 on the private network interface for remote scraping.
The endpoint uses Pingora's built-in Prometheus server with gzip compression.
Tako-server uses SNI (Server Name Indication) to select the appropriate certificate during TLS handshake:
- Client connects and sends SNI hostname
- Server looks up certificate for that hostname in CertManager
- If exact match found, use that certificate
- If no exact match, try wildcard fallback (e.g.,
api.example.com→*.example.com) - If still no match, serve fallback default certificate so HTTPS can complete and routing can return normal HTTP status codes (for example
404for unknown routes/hosts)
This requires OpenSSL (not rustls) for callback support.
- Let’s Encrypt ACME by default, or Cloudflare Origin CA when an environment sets
ssl = "cloudflare" - Automatic issuance for domains in app routes
- For private/local route hostnames (
localhost,*.localhost, single-label hosts, and reserved suffixes such as*.local,*.test,*.invalid,*.example,*.home.arpa), Tako skips ACME and generates a self-signed certificate during deploy. - If no certificate exists yet for an SNI hostname, Tako serves a fallback self-signed default certificate so TLS handshakes still complete.
- Automatic renewal 30 days before expiry
- HTTP-01 challenge uses the public HTTP port. Let's Encrypt reaches port 80, so custom HTTP ports require external port-80 forwarding, DNS-01, or manual certificates.
- Zero-downtime renewal
- DNS-01 challenges are supported for Let’s Encrypt certificates through Cloudflare DNS. Set up encrypted provider credentials with
tako credentials set ssl.cloudflare --env <env>. Use a Cloudflare user or account API token with Zone Read and DNS Write for the matching zone. Deploy rejects missing or expired credentials for wildcard routes, warns when they expire within 30 days, verifies Cloudflare DNS zone access from each target server during remote prepare, stages the SSL binding for the final deploy command, andtako-serverstores it encrypted per deployed app. Exact routes use DNS-01 when this credential is present; otherwise they use HTTP-01. - Cloudflare SSL uses Cloudflare Origin CA. Set
ssl = "cloudflare"intako.tomland set up encrypted provider credentials withtako credentials set ssl.cloudflare --env <env>. Use a Cloudflare user or account API token with Zone / SSL and Certificates / Edit for the matching zone. Deploy rejects missing or expired credentials, warns when they expire within 30 days, validates supported user-owned Cloudflare tokens from each target server during remote prepare, stages the SSL binding for the final deploy command, andtako-serverstores it encrypted per deployed app. Account-owned Cloudflare tokens are accepted and validated by Origin CA API calls during certificate issuance.
Routing supports wildcard hosts (e.g. *.example.com). For TLS:
- Let’s Encrypt wildcard certificates are issued automatically via Cloudflare DNS-01 challenges when the deployed app environment has unexpired
ssl.cloudflarecredentials stored bytako credentials; exact Let’s Encrypt certificates also use DNS-01 when the credential is present. Each target server verifies the token can read the matching Cloudflare zone during remote prepare, the token must allow DNS Write for issuance, and deploy warns when those credentials expire within 30 days - Cloudflare SSL wildcard certificates are issued through Cloudflare Origin CA and do not require DNS-01 credentials
- Wildcard certificates are used when present in cert storage
- If Let’s Encrypt wildcard routes are deployed without Cloudflare credentials, deploy fails with an error directing the user to run
tako credentials set ssl.cloudflare --env <env>
/opt/tako/certs/{domain}/
├── fullchain.pem # Certificate + intermediates
└── privkey.pem # Private key (0600 permissions)
Pass --acme-staging to tako-server to use Let's Encrypt staging:
- No rate limits
- Unlimited certificate issuance
- Certificates not trusted by browsers
- Perfect for development/testing
npm install tako.shApps export a Web Standard fetch handler:
export default function fetch(request: Request): Response | Promise<Response> {
return new Response("Hello!");
}Tako v0 does not install any global. App code imports runtime state from tako.sh; tako generate emits a project-local tako.d.ts file that augments tako.sh with project-specific environment names, secret keys, storage bindings, channel metadata, workflow metadata, and user-defined env var names for process.env / import.meta.env. Tako-owned runtime values are typed through tako.sh exports such as tako.env, tako.build, and tako.dataDir, not through generated env-var globals. The declaration file keeps an existing app/, src/, or root location, otherwise uses an existing legacy tako.gen.ts location, then app/, then src/, then the project root. App code does not import the generated file:
import { tako } from "tako.sh";
tako.logger.info("boot", { env: tako.env, build: tako.build });
const dbUrl = tako.secrets.DATABASE_URL;| Export | Description |
|---|---|
tako |
Frozen runtime object containing app state, logger, secrets, and storage bindings |
env |
ENV value ("development", "production", ...); also available as tako.env |
isDev |
true when env === "development"; also available as tako.isDev |
isProd |
true when env === "production"; also available as tako.isProd |
port |
Port assigned to this app instance; also available as tako.port |
host |
Host/address Tako bound this app instance to; also available as tako.host |
build |
Build identifier injected at deploy time ("dev" under tako dev); also available as tako.build |
dataDir |
Persistent app-owned data directory — writes survive restarts; also available as tako.dataDir |
appDir |
Directory the app is running from (equivalent to process.cwd()); also available as tako.appDir |
secrets |
Typed secret bag — redacts automatically on bulk serialize; also available as tako.secrets |
storages |
Typed storage binding bag; also available as tako.storages |
logger |
Structured JSON logger (logger.info(...)) bound to source: "app"; also available as tako.logger |
Env |
TypeScript union of environment names declared in tako.toml, narrows tako.env === "staging" checks at compile time |
TakoSecrets |
TypeScript interface of secret keys declared in .tako/secrets.json |
TakoStorages |
TypeScript interface of storage binding names declared in tako.toml |
TakoRuntime |
TypeScript type of the tako runtime object |
tako.secrets redacts automatically on JSON.stringify, console.log, and toString (returns "[REDACTED]"); individual key access (tako.secrets.MY_KEY) returns the value. The generated TakoSecrets augmentation is regenerated from .tako/secrets.json on every tako dev, tako deploy, tako generate, and tako secrets change. Generation prefers secret names from the development environment when present, then falls back to the union of all secret environments.
tako.storages exposes storage bindings delivered through the bootstrap envelope. Individual storage access (tako.storages.uploads) returns an object with createDownloadUrl, createUploadUrl, createImageUrl, and createImageSrcSet. The generated TakoStorages augmentation is regenerated from tako.toml on tako dev, tako deploy, and tako generate.
Channels and workflows are not on the runtime context — they are regular ES modules you import from their files:
import sendEmail from "../workflows/send-email";
import chat from "../channels/chat";
await sendEmail.enqueue({ to: "u@e.co" });
await chat({ roomId: "r1" }).publish({ type: "msg", data: { text: "hi" } });The tako.sh package exports tako, imageUrl, imageSrcSet, defineChannel, defineWorkflow, signal, TakoError, InferChannel, and InferWorkflowPayload. Storage URL option types are exported from tako.sh. The tako.sh/runtime subpath is reserved for browser-safe runtime internals. Server-adapter plumbing (handleTakoEndpoint, initServerRuntime, and the channel/workflow definition types) lives under tako.sh/internal and is intended for framework adapters. The Channel class is not exported from tako.sh: code uses the accessor returned by defineChannel(...).$messageTypes<M>() from the matching <app_root>/channels/ file. Browser code imports Channel and configureChannels from tako.sh/client; React channel hooks remain available from tako.sh/react. There is no Tako global.
go get tako.shGo apps use the tako package to serve an http.Handler:
package main
import (
"net/http"
"tako.sh"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello from Tako!"))
})
tako.ListenAndServe(mux)
}For frameworks that manage their own server (e.g. Fiber on fasthttp), use tako.Listener() to get a pre-bound net.Listener instead.
| Export | Purpose |
|---|---|
tako.ListenAndServe(handler) |
Wraps an http.Handler with Tako protocol support (fd 4 readiness, Host: <app>.tako handling). |
tako.Listener() |
Returns a bound net.Listener for frameworks that own their own server loop. |
tako.InstanceID() / tako.Version() / tako.Uptime() |
Runtime identity helpers (empty strings in dev mode). |
tako.GetSecret(name) |
Low-level secret accessor. Prefer the typed Secrets struct from tako generate. |
tako.AllowChannel(grant) / tako.RejectChannel() |
Channel auth helpers for ChannelDefinition callbacks. |
tako.Channel, tako.ChannelRegistry, tako.Channels, tako.ChannelTransport, and related types |
Channel authoring surface mirrored from tako.sh/internal. |
- Go compiles to a native binary — no runtime download needed on the server.
- The compiled binary runs directly (
launch_args: ["{main}"]), no SDK entrypoint wrapper. Thetakopackage wires up the protocol from inside the user's own binary. tako.ListenAndServe()handles the full protocol: CLI arg parsing (--instance), TCP serving,Host: <app>.takoendpoint interception, graceful shutdown onSIGTERM/SIGINTwith a 10s drain window.- Deploy auto-injects
GOOS=linuxandGOARCHfor cross-compilation to the target server. - Default build:
CGO_ENABLED=0 go build -o app .producing a static HTTP binary. Whencmd/worker/main.goexists, the default build also runsCGO_ENABLED=0 go build -o worker ./cmd/workerand the deploy manifest recordsworkflow_worker_main = "worker"sotako-servercan supervise the worker subprocess. - In
tako dev, a Go project withcmd/worker/main.goruns workflows throughgo run ./cmd/workeras a separate scale-to-zero worker subprocess. - Secrets:
tako.GetSecret("name")provides access to Tako-managed secrets. Runtako generateto generate a typedSecretsstruct intako_secrets.go.
[dependencies]
tako = "0.1"Rust apps use the tako crate to bind through Tako and install the internal status handler:
use axum::{routing::get, Router};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let app = Router::new().route("/", get(|| async { "Hello from Tako" }));
tako::axum::serve(app).await
}For frameworks that own their server loop, use tako::std_listener() or tako::listener().await with the tokio feature. Secrets are available through tako::secret("NAME") or tako::bootstrap()?.secret("NAME"). Container and native releases use the same SDK API; only the bootstrap transport changes.
import { tako } from "tako.sh/vite";tako.sh/viteprovides a plugin that prepares a deploy entry wrapper in Vite output.- It emits
<outDir>/tako-entry.mjs, which normalizes the compiled server module to a default-exported fetch handler. - During
vite dev, it adds.test,.tako.test, and configured dev route hostnames toserver.allowedHosts. - During
vite dev, whenPORTis set, it binds Vite to127.0.0.1:$PORTwithstrictPort: true. - During
tako dev, it reads the fd 3 bootstrap before server code runs so Vite SSR can accesstako.secretsandtako.storages. - During
tako dev, it routes Vite-processconsole.*, stdout, and stderr through structured Tako app log events so multi-line framework/runtime errors stay grouped in the CLI log stream. - Deploy does not read Vite metadata files.
- To use the generated wrapper as deploy entry, set
mainintako.tomlto the generated file (for exampledist/server/tako-entry.mjs) or define preset top-levelmain.
import { withTako } from "tako.sh/nextjs";tako.sh/nextjsprovideswithTako(), a helper that setsoutput = "standalone", pointsadapterPathat the installed Tako adapter, and appends"*.test"and"*.tako.test"toallowedDevOriginssonext devaccepts requests from Tako's dev hostnames.withTako()configuresnext/imageto use Tako's public optimizer globally by settingimages.loader = "custom"andimages.loaderFileto Tako's packaged loader. It also aligns Next's generated image widths with Tako's public optimizer defaults by settingimages.deviceSizes = [320, 640, 960, 1200, 1920]andimages.imageSizes = [].- On build, the adapter writes
.next/tako-entry.mjs. - If Next emits
.next/standalone/server.js, the adapter copiespublic/and.next/static/into.next/standalone/so that standalone server can serve them. - If standalone output is not emitted, the generated wrapper falls back to
next startagainst the built.next/directory and installednextpackage. - The generated
tako-entry.mjsexports a fetch handler that proxies requests to the standaloneserver.js.
- Internal fetch handler adapters for Bun/Node runtimes (used by entrypoint binaries)
- Go SDK with
tako.ListenAndServe()for native http.Handler support - Rust SDK with bootstrap helpers, listener binding, and
tako::axum::serve(router)for Axum apps - Deployed app serving over private TCP with
PORT/HOST;tako devalso uses TCP (PORT) - Internal status endpoint (
Host: <app>.tako+/status) - Internal channel auth endpoint (
Host: <app>.tako+POST /channels/authorize) - Internal channel registry endpoint (
Host: <app>.tako+GET /channels/registry) - Internal WebSocket dispatch endpoint (
Host: <app>.tako+POST /channels/dispatch) - Public durable channel read/connect route at
GET /_tako/channels/<name> - Graceful shutdown handling
GET /status with Host: <app>.tako
{
"status": "healthy",
"app": "dashboard",
"version": "abc1234",
"instance_id": "a1b2c3d4",
"pid": 12345,
"uptime_seconds": 3600
}Used for health checks during rolling updates and monitoring.
POST /channels/authorize with Host: <app>.tako
Used by tako-server to ask the app SDK whether a channel operation is allowed and which lifecycle settings apply. The SDK returns ok, optional subject, optional transport, and channel lifecycle settings such as replayWindowMs, inactivityTtlMs, keepaliveIntervalMs, and maxConnectionLifetimeMs.
GET /channels/registry with Host: <app>.tako
Used by tako-server to fetch the app's declared channel metadata for validation, auth, and transport selection.
POST /channels/dispatch with Host: <app>.tako
Used by tako-server to dispatch incoming WebSocket frames to the app's declared channel handler before fan-out.
Durable channels are Tako-owned pub-sub routes for app events:
GET /_tako/channels/<name>withAccept: text/event-streamserves SSE with replay + live tailGET /_tako/channels/<name>withUpgrade: websocketupgrades to WebSocket
Every channel publish is stored before delivery. In production, the replay log is scoped to the deployed app id ({name}/{env}), not to a release or instance. Single-server environments without postgres_url use local SQLite. Environments with postgres_url use Postgres schema tako_channels, so every server reads the same replay log and publishes from any server are visible to subscribers connected to any other server. In local dev, replay is per registered app and in-memory for the current daemon process. The log is bounded by replayWindowMs, which defaults to 10 minutes. Apps that need canonical history still store it in their own database and use channel replay as the reconnect bridge.
- SSE resumes from
Last-Event-ID - WebSocket resumes from
last_message_idin the query string - If no cursor is provided, Tako starts from the latest retained message
- If the requested cursor is older than the retained replay window, Tako returns
410 Gone
Browser clients keep reconnecting until explicitly closed. Network loss, laptop sleep, server restarts, and clean connection rotation are treated as transient disconnects: the SDK retries with bounded exponential backoff and jitter, wakes early when the browser reports it is back online, and resumes from the last received message id.
Channel WebSocket transport uses JSON text frames:
- server-to-client text frames are serialized
ChannelMessageobjects - client-to-server text frames are parsed as
ChannelPublishPayloadobjects, routed through the channel's declaredhandler, and the handler's return value is fanned out to subscribers - client-to-server frame payloads are capped at 128 MiB, matching the proxy request body limit
Channel routes are exact and flat: defineChannel("chat", ...) is served at /_tako/channels/chat. Dynamic values are query params validated against the channel's declared JSON Schema, for example /_tako/channels/chat?roomId=room-123.
JS/TypeScript — file-based discovery: drop a file into <app_root>/channels/*.ts with a default export of defineChannel("<name>", ...).$messageTypes<M>(). The first argument is the wire channel name and is the source of truth for the public route. Generated/scaffolded files use the file stem as the initial name, but users may choose a different explicit name; discovery rejects duplicate declared names. Generated TakoChannels metadata uses the declared channel name as its key and import("tako.sh").InferChannel<typeof import("./channels/<file>").default> to infer params, messages, and transport from the channel export. paramsSchema is a TypeBox schema that becomes both the TypeScript params type and the server-side JSON Schema used by tako-server before app auth. .$messageTypes<M>() is a type-level narrower that declares the message map; at runtime it returns the same export.
// <app_root>/channels/chat.ts
import { defineChannel } from "tako.sh";
type ChatMessages = {
msg: { text: string; userId: string };
typing: { userId: string };
};
export default defineChannel("chat", {
paramsSchema: (t) => t.Object({ roomId: t.String({ minLength: 1 }) }),
auth: {
headerName: "authorization",
async verify(input) {
const session = await readSession(input.header);
if (!session) return false;
const allowed = await db.isMember(input.params.roomId, session.userId);
return allowed ? { subject: session.userId } : false;
},
},
handler: {
msg: async (data, ctx) => {
await db.saveMessage(ctx.params.roomId, data);
return data; // what gets fanned out to subscribers
},
typing: async (data) => data,
},
}).$messageTypes<ChatMessages>();paramsSchema— optional TypeBox schema. Omit it for channels with no params. The serialized JSON Schema is sent totako-server, which rejects invalid query params before round-tripping to the app.auth— optional. Omit or setfalsefor public channels. Auth is declarative:{ headerName, cookieName, verify }.headerNamedefaults toauthorization; set it tofalsefor cookie-only auth.verify(input)receives{ header?, cookie?, params, channel, operation }and returnsfalse,true, or{ subject }.handler— optional map keyed by message type. Presence ofhandlermakes the channel a WebSocket channel (bidirectional); absence makes it SSE (broadcast-only). Each handler returns the data to broadcast, orvoid/undefinedto drop the message.replayWindowMs— retained replay window for reconnecting clients. Defaults to 10 minutes. Set0to keep messages until manual database maintenance.inactivityTtlMs— optional idle-channel cleanup window. Defaults to off.keepaliveIntervalMs— SSE/WS heartbeat cadence. Defaults to 25 seconds.maxConnectionLifetimeMs— maximum lifetime for a single connection. Defaults to 2 hours.
Transport inference:
handlerpresent → WS. Clients can send over the socket; each frame routes through the declared handler; the return value fans out to subscribers. Handler errors orvoidreturns drop the message. Types not in the handler map pass through without server processing.handlerabsent → SSE. Broadcast-only. Server publishes via the imported channel module (await missionLog({ base }).publish(...)); clients only receive.
WebSocket header auth is sent as the first text frame:
{ "type": "tako.auth", "token": "Bearer ...", "lastMessageId": "123" }Browser clients may pass authorization: token to Channel.subscribe, Channel.connect, or useChannel to send Authorization: Bearer <token> for header-auth channels. The option is optional; omit it for public channels and cookie-auth channels. headers.Authorization remains available for explicit authorization values.
If an auth-required WebSocket does not send a valid first frame within five seconds, tako-server closes it with an errkit-generated app close code.
Go — programmatic registration mirrors the same wire protocol:
tako.Channels.Register("chat", tako.ChannelDefinition{
ParamsSchema: []byte(`{"type":"object","properties":{"roomId":{"type":"string"}},"required":["roomId"]}`),
Auth: &tako.ChannelAuthScheme{HeaderName: "authorization"},
Verify: func(input tako.VerifyInput) tako.ChannelAuthDecision {
if input.Header == nil || input.Header.Scheme != "Bearer" {
return tako.RejectChannel()
}
return tako.AllowChannel(tako.ChannelGrant{Subject: "user-123"})
},
})Server-side code (HTTP handlers, workflow bodies) imports a channel module directly and calls it. Unparameterized channels expose publish / subscribe / connect on the export; parameterized ones are callable with their params, returning the same handle:
// <app_root>/channels/status.ts (unparameterized)
import status from "../channels/status";
await status.publish({ type: "ping", data: { at: Date.now() } });
// <app_root>/channels/mission-log.ts (parameterized by paramsSchema)
import missionLog from "../channels/mission-log";
await missionLog({ base }).publish({ type: "event", data: event });Params are URL-encoded automatically. publish payloads are type-checked against the message map declared via .$messageTypes<M>(). The Channel class is not re-exported from tako.sh — browser code imports it from tako.sh/client (or uses the useChannel hook from tako.sh/react).
Tako's workflow engine runs durable background work alongside an app's HTTP
instances — retries with exponential backoff, delayed/cron schedules, and
multi-step workflows whose progress survives process restarts via ctx.run
checkpoints. It positions Tako for the "backend of your backend" use case
(image processing, email, reindexing, LLM calls) without requiring a separate
queue service.
Vocabulary:
- workflow — a named handler (the file in
<app_root>/workflows/*.ts, or a registered handler in the Go worker binary). - run — one execution of a workflow (the row in the queue that gets claimed, retried, completed, or moved to dead).
- step — a memoized portion inside a run via
ctx.run(name, fn).
- Storage: local mode uses
{tako_data_dir}/apps/<app>/data/tako/workflows.sqlitewith WAL. Remote mode uses Postgres schematako_workflows. tako-server is the only process that reads/writes either backend; SDKs reach it exclusively via the shared internal unix socket. - Tables:
runs— one row per run (status, attempts, lease, payload).steps— one row per completed step(run_id, name, result). First-write-wins viaINSERT OR IGNOREso duplicate saves after a retried RPC don't overwrite.event_waiters— runs parked onctx.waitFor, indexed byevent_namefor fast lookup onsignal.schedules,leader_leases— cron infrastructure.
- tako-server (Rust) — owns the DB, exposes the internal unix socket, runs the cron ticker, dispatches runnable work, and supervises the worker subprocess.
EnqueueRunreturns after the run is committed to SQLite; worker startup is a separate dispatch step. The dispatcher coalesces enqueue/signal/cron/reclaim notifications and scans for due pending runs every second, waking a worker only whenstatus='pending' AND run_at <= now. The ticker also callsreclaim_expired()every second: any run stuck instatus='running'past itslease_untilis moved back topending, and the dispatcher wakes a fresh worker when it is runnable. This is how runs recover from a worker that died mid-execution (SIGKILL, OOM, host crash, server-level restart without graceful drain). - Worker process (JS or Go) — loads user code, claims runs, executes handlers. Separate from HTTP instances so heavy workflow deps don't bloat the request-serving process.
- SDK — each workflow module's default export provides
.enqueue(payload, opts?);signal(event, payload?)is a top-level export fromtako.shthat throwsTakoError("TAKO_UNAVAILABLE")when called outside an installed workflow runtime. Workers use the same RPC client for claim/heartbeat/save/complete/cancel/fail/defer/wait. No SQLite in any SDK.
[workflows] # base config inherited by every worker group
workers = 1
concurrency = 10
[workflows.email] # named worker-group override
workers = 2
[servers.lax.workflows] # base override on one server
workers = 2
[servers.lax.workflows.email] # named worker-group override on one server
workers = 4Fields:
workers— number of always-on worker processes.0= scale-to-zero: tako-server starts a worker when runnable workflow work appears from enqueue, signal, cron, delayed retry/sleep, or lease reclaim. The worker exits after it has been idle (no claimed runs) long enough for the supervisor's idle window. Default0.concurrency— max parallel runs per worker. Default10.
Precedence for unnamed workflows: built-in defaults (workers = 0, concurrency = 10) < [workflows] < [servers.<name>.workflows].
Precedence for worker: "email": built-in defaults < [workflows] < [workflows.email] < [servers.<name>.workflows] < [servers.<name>.workflows.email]. A top-level workers = 5 under [workflows] is inherited by each worker group unless that group overrides it.
If a JS app has a <app_root>/workflows/ directory (or a Go app declares a worker binary) but no workflow config anywhere, the app is implicitly scale-to-zero on every server in the env.
JS/TypeScript — file-based discovery: drop a file into <app_root>/workflows/<name>.ts with a default export from defineWorkflow<P>(name, opts). The opts.handler function's second argument is the workflow context (ctx) — call ctx.run/ctx.sleep/ctx.waitFor/ctx.bail/ctx.fail as needed, use ctx.logger for workflow-scoped logs, and read ctx.runId / ctx.workflowName / ctx.attempt (the 1-indexed run attempt, bumped on each run-level retry) for context:
// <app_root>/workflows/send-email.ts
import { defineWorkflow } from "tako.sh";
type SendEmailPayload = { userId: string };
export default defineWorkflow<SendEmailPayload>("send-email", {
retries: 4,
schedule: "0 9 * * *",
handler: async (payload, ctx) => {
ctx.logger.info("send-email started");
const user = await ctx.run("fetch-user", (step) => {
step.logger.info("fetching user");
return db.users.find(payload.userId);
});
await ctx.run("send", (step) => {
step.logger.info("sending email");
return mailer.send(user.email);
});
},
}); // 9am dailyThe name is required (it must be a string literal — codegen and the dedup/cron systems read it) and should match the filename for the file-based discovery scan.
On worker startup, the SDK registers the complete current cron schedule set for the discovered workflows. Missing schedules are deregistered, including the case where workflows still exist but none define schedule.
Set worker: "name" in the workflow opts to assign a workflow to a named worker group; workflows without worker belong to the default group. Worker processes launched with TAKO_WORKFLOW_WORKER=<name> load only workflows assigned to that group. Worker processes without TAKO_WORKFLOW_WORKER load all workflows for compatibility with the default single-worker deployment path.
Set local: true in the workflow opts to force that workflow onto per-server local SQLite storage. In multi-server environments, deploy uses shared Postgres workflow storage when postgres_url is set; otherwise every JS workflow source file must opt into local: true or deploy fails before build/deploy work starts. Local cron schedules run once per server and local enqueue uniqueness is scoped to each server. Channels do not have a local opt-out for multi-server deploys because channel publish/replay must reach subscribers connected to every server.
Go — explicit registration in a separate cmd/worker/main.go binary. Go's separate-binary design is intentional: a single-binary design would link workflow dependencies (image libraries, ML bindings, mail clients, queue integrations) into the HTTP server binary.
The HTTP app remains the default Go package and serves through tako.ListenAndServe():
main.go
cmd/worker/main.go
tako dev runs the HTTP process with go run . and starts the workflow process with go run ./cmd/worker when that file exists. tako deploy builds the HTTP binary to app and the worker binary to worker; app.json records workflow_worker_main = "worker" so tako-server supervises that worker as a separate scale-to-zero subprocess.
// anywhere:
import sendEmail from "../workflows/send-email";
await sendEmail.enqueue({ userId: "u1" });
await sendEmail.enqueue(payload, {
runAt: new Date(Date.now() + 60_000),
retries: 9,
uniqueKey: "daily-digest:2026-04-14",
});Each workflow module's default export is a typed handle: .enqueue(payload, opts?) is constrained to the payload type declared on defineWorkflow<P>(name, opts). No generated file is needed for workflow enqueue typing — it flows from the module's own types.
uniqueKey deduplicates: if an existing non-terminal run has the same key, enqueue is a no-op and returns the existing run's id. Cron ticks use this internally (key = cron:<name>:<bucket_ms>) so catching up doesn't double-enqueue.
ctx.run(name, fn, opts?) persists fn's return value as one row in the steps table keyed by (run_id, name). On retry, previously-completed steps return their stored value instead of re-executing. The step callback receives a step context (step) with step.logger scoped to the workflow and step name, plus step.runId, step.workflowName, step.stepName, and step.attempt.
Per-step options:
retries: N— in-step retry budget (default 0). After N+1 in-step attempts the error propagates → run-level retry kicks in.backoff: { base, max }— between in-step retries.retry: false— short-circuit: any throw fails the run immediately, skipping both in-step and run-level retries.
If the worker crashes between fn returning and the SaveStep RPC completing, fn runs again on the next claim. The window is one RPC (~1ms) but it's real. Make step bodies idempotent: Stripe idempotency keys, db.users.upsert not create, dedup keys on outbound webhooks. This contract matches every workflow engine in the industry — it's the cost of durability without two-phase commit.
ctx.sleep(name, ms) waits until the wake time. Short waits (< 30s) run inline; longer waits defer the run via DeferRun — the worker exits the handler, the run goes back to pending with run_at = wakeAt, and the dispatcher wakes a worker once the run is due. Crash-safe across days.
ctx.waitFor(name, { timeout }) parks the run waiting for a named event. The handler exits, the run goes to pending with no run_at, an event_waiters row is inserted, and the worker can release.
signal(name, payload?) from tako.sh (or the equivalent internal-socket call in Go) wakes every parked waiter with matching name. The payload is materialized as the waiter's step result, the run is set runnable, and the dispatcher wakes a worker if needed. signal is runtime-guarded: calling it from browser code (where the workflow runtime is not installed) throws a TakoError("TAKO_UNAVAILABLE") instead of silently no-oping.
// Worker handler — pause until approval arrives
const decision = await ctx.waitFor<{ approved: boolean }>(`approval:order-${payload.id}`, {
timeout: 7 * 24 * 3600 * 1000,
});
if (decision === null) ctx.bail("approval timed out");// Anywhere else (HTTP handler, webhook receiver, another workflow)
import { signal } from "tako.sh";
await signal(`approval:order-abc`, { approved: true });Routing is by event name only — embed any selectors in the name. No JSON predicates server-side.
ctx.bail(reason?)— end the run cleanly. Status:cancelled. No retries.ctx.fail(error)— end the run with failure. Status:dead. No retries (skips the run-level retry budget).
Both work via sentinel exceptions caught by the worker. Useful for "this work isn't needed anymore" (bail) and "this is permanently broken, don't bother retrying" (fail).
pending | running | succeeded | cancelled | dead. Terminal: succeeded, cancelled, dead.
- Failed handlers retry with exponential backoff (default base 1s, ±20% jitter, capped at 1h). Override via
defineWorkflow(name, { handler, retries, backoff }). Default is 2 retries (3 total attempts). attemptsbumps on every claim. When attempts reach the budget, the run moves todead.defer_run(sleep, waitFor) decrements attempts so parking doesn't consume retry budget.
- When
tako-serverstops an app through the internalstopcommand, scale-down, rolling replacement, service shutdown, or delete flow, it drains the worker first (SIGTERM, waits for in-flight, SIGKILL after 120s). - On
tako delete: drain first, then remove per-app data — in-flight runs get a chance to finish before the DB goes away.
- Single shared internal socket at
{tako_data_dir}/internal.sock(symlink →internal-{pid}.sock, atomically swapped during upgrades for zero-downtime handoff — same pattern as the mgmt socket). Workflow RPCs and server-side channel publishes both land here, hence the role-neutral name. - Every command carries an
appfield so one socket routes for every deployed app. - Auth: filesystem permissions plus app-scoped runtime tokens. The socket is
chmod 0660and group-accessible to the sharedtako-appgroup so per-app users can connect through their supplementary group. - SDKs read
TAKO_INTERNAL_SOCKETandTAKO_APP_NAMEenv vars. HTTP instance and workflow worker spawners (tako-server in production and tako-dev-server intako dev) share one env contract defined intako-core::instance_env::TakoRuntimeEnvso the dev and prod runtimes can't drift. The SDK asserts the pair is set together at import time — a half-set env (one var without the other) is a platform bug and crashes the process on boot rather than silently failing at the first workflow enqueue or channel publish. - From any process:
EnqueueRun,Signal,ChannelPublish(server-side publish goes straight to the channel store instead of round-tripping through the HTTPS proxy). - From worker processes:
ClaimRun,HeartbeatRun,SaveStep,CompleteRun,CancelRun,FailRun,DeferRun,WaitForEvent,RegisterSchedules. - The management socket rejects workflow/channel commands with an explicit "must be sent over the internal socket" error, and vice versa — the two sockets never cross wires even though they share the
Commandenum intako-core. - JSONL protocol, per-call connection (connect → send → read → close).
- Server → Worker for drain: SIGTERM + grace period (120s), then SIGKILL.
tako dev uses the same workflow architecture as production: tako-dev-server owns the runs DB, internal socket, dispatcher, and a WorkerSupervisor that spawns a worker subprocess on demand. The worker is scale-to-zero (workers: 0, idle_timeout_ms: 3_000) so it only runs while there's runnable work, and each post-idle dispatch starts a fresh worker — so code edits take effect on the next runnable enqueue/signal/cron tick without restarting tako dev. Worker stdout/stderr is tee'd into the same log stream as the app process, with scope: "worker" so the CLI can prefix it.
On every RegisterApp, the dev-server registers the app with its embedded WorkflowManager — same ensure() call as production — so the first workflow enqueue or channel publish from user code doesn't race the registration.
Fail-fast on broken workers. If the worker subprocess exits non-zero without claiming any run (typical for import errors, missing workflow module, crash on boot), the supervisor marks the app unhealthy for 5s. During that window, EnqueueRun returns worker unhealthy: <reason> instead of silently queuing work that will never execute. The SDK surfaces this to the caller as a normal error so broken dev code is loud instead of hanging. Clean idle-out (exit 0 after idle_timeout_ms) never marks unhealthy.
| Scenario | Behavior |
|---|---|
| Config/data directory deleted | Auto-recreate on next command |
config.toml corrupted |
Show parse error with line number, offer to recreate |
tako.toml deleted |
Commands that require project config fail with guidance to run tako init |
.tako/ deleted |
Auto-recreate on next deploy |
.tako/secrets.json deleted |
Warn user, prompt to restore secrets |
Low free space under /opt/tako |
Deploy fails before upload with required vs available disk sizes |
| Concurrent deploy already running | Later deploy fails immediately with a retry message |
tako-server restarts during deploy |
In-flight deploy fails; retry does not require lock cleanup |
| Deploy fails mid-transfer/setup | Auto-clean newly-created partial release directory |
| Health check fails | Automatic rollback to previous version |
| Network interruption during deploy | Partial failure handling, can retry |
| Process crash | Auto-restart, health checks detect and handle |
- Unit tests for all business logic (config parsing, validation, routing)
- Integration tests for critical paths (deploy, rolling updates, health checks)
- Edge case tests (deleted files, network failures, process crashes)
- Critical-path coverage target: >=80% line coverage across core modules (config parsing, runtime detection, routing, static file resolution, cold-start orchestration)
- TDD mandatory: write tests first, implement after tests pass
- Proxy throughput: Faster than Caddy, on par with Nginx
- Cold start: ~100-500ms for on-demand instances
- Health detection: immediate for exited processes; generally <10s for unresponsive health endpoints
- Deploy time: <1 minute for rolling update of 3 instances
- Memory: Minimal footprint with on-demand scaling