Flight is a Node.js application server for teams who want something fast, boring in the good way, and ready for serious traffic. You bring your own hosting—there is no lock-in to a proprietary edge or a single vendor’s deployment story. It fits twelve-factor style workflows: configuration via environment variables, horizontal scaling, and state kept where it belongs (for Flight, that includes Redis for sessions and cache-friendly layers).
Think platform-agnostic: not framework-as-a-platform, but a clear runtime you can run wherever Node runs—VMs, Kubernetes, bare metal, your cloud of choice. Flight is aimed at hyperscale-friendly designs (cluster workers out of the box), ephemeral processes, and component-shaped backends so routes stay colocated with the features they serve. Vue and Vite are first-class today; React support is on the roadmap.
Flight is open source from ThoughtPivot.
- Performance-focused: Cluster mode, compression, Redis-backed caching hooks, rate limiting in production
- Developer velocity: Vite-powered dev server with HMR for Vue (React roadmap)
- Composable backends: Discover
**/*.backend.tsunder your app root and mount Koa routes per component - Production SPA: Built-in
dist+index.htmlfallback (option B) when runningproductionwithdisable_vite, with an explicit opt-out for API-only processes - Configurable discovery:
--exclude_paths/FLIGHT_EXCLUDE_PATHSto skip directories when scanning backends - TypeScript-native: Written for TS projects; sensible defaults, minimal ceremony
- Interop-friendly: Correct handling of
yargswhen launched via tsx or similar loaders (no patch-package needed from v1.0.8 onward)
npm install @thoughtpivot/flight
# or
yarn add @thoughtpivot/flightLegacy npm scope: the package was previously published as @spytech/flight—use @thoughtpivot/flight going forward.
From v1.0.8 onward, Flight normalizes require('yargs/yargs') under loaders that expose { default: factory }. If you added patch-package only for that issue, upgrade Flight and drop that patch.
- Create a new project directory and initialize:
mkdir my-flight-app
cd my-flight-app
npm init -y- Install Flight and its dependencies:
npm install @thoughtpivot/flight ioredis-
Ensure Redis is running locally or set environment variables (see table below).
-
Create a component with a backend route:
mkdir -p components/helloCreate components/hello/hello.backend.ts:
import Router from '@koa/router'
const router = new Router()
router.get('/hello', async (ctx) => {
ctx.body = { message: 'Hello from Flight!' }
})
export default router.routes()- Add scripts to
package.json(theflightbinary comes fromnode_modules/.binafter install):
{
"scripts": {
"dev": "flight --mode development",
"start": "flight --mode production"
}
}Use FLIGHT_DISABLE_VITE, FLIGHT_DIST_PATH, and related env vars in .env for production built-assets mode (see the configuration table below).
- Start the server:
Development (Vite on 3001, API on 3000):
npx flight --mode development
# or: npm run devProduction:
npx flight --mode production
# or: npm run start--app_home defaults to . (the current working directory). Pass --app_home path/to/app only when your app root is not the directory you run the command from.
Try without adding a dependency (downloads the package for this invocation). Use --package=@scope/pkg (equals form) and a literal -- before the binary name so npx does not treat flight as a separate package or lose the install’s node_modules/.bin on your PATH (otherwise you can see sh: flight: command not found):
npx --yes --package=@thoughtpivot/flight -- flight --mode developmentEquivalent:
npm exec --yes --package=@thoughtpivot/flight -- flight --mode developmentGlobal install (optional): npm install -g @thoughtpivot/flight, then run flight from your PATH the same way as npx flight.
Flight picks mode as: FLIGHT_MODE (if set and non-empty), else --mode from argv, else production. Everything below assumes Redis is reachable unless you only use routes that avoid session/ratelimit/cache.
| Topic | Development | Production |
|---|---|---|
| Vite | Child process: npx vite --port 3001 --host 0.0.0.0 (HMR). |
If disable_vite is false: exec('npx vite build') is triggered once at worker startup. If disable_vite is true: no Vite build. |
| Listen port | API + sessions on --port / FLIGHT_PORT (default 3000). UI dev server on 3001. |
Same --port / FLIGHT_PORT for the worker. |
| Static / SPA | The Vite dev server serves UI; Flight does not mount the production static/SPA stack. | See Production SPA pipeline below: when production + disable_vite, Flight uses Option A by default unless opted out. |
| Typical use | Local full-stack with HMR. | CI-built dist served by Flight (or behind a load balancer). |
Edge cases
production+disable_vitefalse: Vite build still runs; legacy middleware order applies (compress→ratelimit→koa-cash→ static), because built-assets-only mode is off.production+disable_vitetrue +FLIGHT_DISABLE_SPA_PIPELINE: Legacy stack only—no SPA HTML fallback for unknown GETs; tailkoa-staticonly after compress/ratelimit/cache.development: unchanged—Redis session store and production-only static/SPA branches do not apply the SPA pipeline section (the dev Vite server owns UI URLs).
Flight loads a .env file from the current working directory at startup (via dotenv). Use FLIGHT_* in real deployments.
Precedence
mode:FLIGHT_MODEwins over--modewhen the variable is set (non-empty).disable_vite: explicit CLI wins; if omitted,FLIGHT_DISABLE_VITE(true/1/yes→ on); otherwisefalse.- Other settings: argv value if your launcher passes it, else the
FLIGHT_*column, then default.
Registered CLI options: Flight registers --exclude_paths, --mode, --port, --app_home, --app_key, --app_secret, --payload_limit, and --disable_vite (with kebab-case aliases where listed in --help). yargs is not in strict mode, so additional flags may still appear on argv if you extend the entrypoint.
| Setting (CLI / argv) | Environment variable | Default | Use case |
|---|---|---|---|
--exclude_paths, --exclude-paths |
FLIGHT_EXCLUDE_PATHS |
(empty) | Skip subtrees when globbing **/*.backend.ts (monorepo vendor trees, fixtures). |
--app_home |
FLIGHT_APP_HOME |
. |
Repository root; process.chdir target before discovery and static paths. |
--app_key |
FLIGHT_APP_KEY |
flightApp |
Session cookie name. |
--app_secret |
FLIGHT_APP_SECRET |
(see code) | Cookie signing secret(s); comma-separated for key rotation. |
| (argv) | FLIGHT_SESSION_DURATION_MS |
86400000 |
Session maxAge in ms. |
--port |
FLIGHT_PORT |
3000 |
HTTP listen port (all modes). |
--payload_limit |
FLIGHT_PAYLOAD_LIMIT |
1mb |
koa-bodyparser JSON limit. |
--disable_vite |
FLIGHT_DISABLE_VITE |
false |
true / 1 / yes: skip vite build and treat the process as built-assets mode (triggers SPA pipeline rules in production). |
--mode |
FLIGHT_MODE |
production |
development spawns Vite; production enables production middleware. |
| (n/a) | FLIGHT_DIST_PATH |
../dist |
Filesystem root for koa-static (relative to cwd after chdir to app_home). |
| (n/a) | FLIGHT_DISABLE_SPA_PIPELINE |
(unset) | 1 / true / yes: force legacy production stack (no in-router SPA fallback) while keeping production + disable_vite. Use for API-only workers or until duplicate app middleware is removed. |
| (n/a) | FLIGHT_STATIC_PREFIXES |
/assets,/fonts |
Baseline URL prefixes for rate-limit skipping on GET/HEAD (static hashed chunks). |
| (n/a) | FLIGHT_RATE_LIMIT_EXCLUDE_PREFIXES |
(empty) | Extra prefixes merged with FLIGHT_STATIC_PREFIXES for the same skip list. |
| (n/a) | FLIGHT_SPA_INDEX |
index.html |
SPA shell file inside the dist root for HTML fallback. |
| (n/a) | FLIGHT_SPA_DENY_PREFIXES |
(empty) | Extra prefixes that never get SPA fallback (merged with /api, /health). |
| (n/a) | FLIGHT_TRUST_PROXY |
false |
1 / true / yes: set app.proxy = true so ctx.ip / rate-limit id use X-Forwarded-For behind an L7 LB. |
| (n/a) | FLIGHT_HTTP_CACHE |
false |
1 / true / yes: enable koa-cash in the SPA pipeline branch only (off by default there). Legacy branch always attaches koa-cash. |
| (n/a) | FLIGHT_REDIS_HOST |
localhost |
Redis for sessions, rate limit, and cache adapter. |
| (n/a) | FLIGHT_REDIS_PORT |
6379 |
Redis port. |
| (n/a) | FLIGHT_MAX_WORKERS |
CPU count | Cap cluster worker count on small nodes. |
Example .env fragment:
FLIGHT_MODE=development
FLIGHT_APP_HOME=.
FLIGHT_REDIS_HOST=127.0.0.1
FLIGHT_REDIS_PORT=6379
FLIGHT_PORT=3000
FLIGHT_MAX_WORKERS=4Production behind a proxy:
FLIGHT_MODE=production
FLIGHT_DISABLE_VITE=true
FLIGHT_TRUST_PROXY=1my-app/
├── components/
│ └── Hello/
│ ├── Hello.vue # Vue UI (example)
│ └── Hello.backend.ts # Koa routes for this component
├── assets/
├── dist/ # Production build output
└── package.json
Example Vue + backend snippets:
components/hello/Index.vue:
<template>
<div>
<h1>{{ message }}</h1>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const message = ref('Hello from Flight!')
</script>components/hello/Index.backend.ts:
import Router from '@koa/router'
const router = new Router()
router.get('/hello', async (ctx) => {
ctx.body = { message: 'Hello from Flight!' }
})
export default router.routes()- Vue + Vite with HMR on port 3001
- Backend worker on
--port(default 3000) - Request logging via
koa-logger
- Optional
npx vite buildbefore serving static assets whendisable_viteis false - Cluster workers capped by
FLIGHT_MAX_WORKERS - Compression + Redis-backed rate limiting in all production configurations
- Static dist + SPA behavior depends on built-assets mode—see next section
When it is on (option B): mode === 'production' and disable_vite === true and FLIGHT_DISABLE_SPA_PIPELINE is not truthy.
What it does: After your router (all **/*.backend.ts routes), Flight serves FLIGHT_DIST_PATH with koa-static, then applies an index.html fallback for typical browser navigations (see tests in src/spa-pipeline.test.ts for the behavioral contract). Only then runs koa-compress, Redis koa-ratelimit (with GET/HEAD skipped under FLIGHT_STATIC_PREFIXES ∪ FLIGHT_RATE_LIMIT_EXCLUDE_PREFIXES), and optionally koa-cash when FLIGHT_HTTP_CACHE is enabled.
Path ownership
- API / BFF: Implement on your
Routerin*.backend.tsunder prefixes you own (conventionally/api/...). Those paths are excluded from the SPA fallback, along with/healthand anyFLIGHT_SPA_DENY_PREFIXES. - Static: Any file present under the dist root is served by
koa-staticfirst. - SPA: Remaining
GET/HEADrequests that look like document navigations and whose last path segment has no dot (sofile.extURLs are not rewritten) may receiveFLIGHT_SPA_INDEX.
Load balancers and X-Forwarded-For
- Set
FLIGHT_TRUST_PROXY=1when Flight sits behind a trusted reverse proxy soctx.ipand the rate limiter’sidreflect the client. Configure your proxy to append one well-formedX-Forwarded-Forchain. - Sessions use cookies (
sameSite: truein code today); sticky sessions or shared Redis are operational choices outside Flight’s defaults.
SPA pipeline (default when production + disable_vite):
flowchart TD
A[cors + bodyparser + session] --> B[router + *.backend.ts]
B --> C[koa-static dist]
C --> D[SPA index.html fallback]
D --> E[koa-compress]
E --> F[koa-ratelimit with prefix skips]
F --> G{koa-cash}
G -->|FLIGHT_HTTP_CACHE on| H[Redis-backed cache]
G -->|default| I[no cache middleware]
Legacy production stack (disable_vite false, or FLIGHT_DISABLE_SPA_PIPELINE set): router → compress → ratelimit → koa-cash → koa-static.
- If you rely on the old order (everything after
routersaw compress/ratelimit/cache before static), setFLIGHT_DISABLE_SPA_PIPELINE=1until you validate the new behavior. - If your app already shipped custom SPA / history fallback middleware in
*.backend.ts, remove the duplicate once you confirm Flight’s fallback matches your routes (especially non-/apiprefixes—useFLIGHT_SPA_DENY_PREFIXES). - Expect
koa-cashoff in the SPA pipeline unless you opt in withFLIGHT_HTTP_CACHE.
See CHANGELOG.md for the full 2.0.0 notes.
- Node.js 16.x or higher
- Redis (sessions / rate limit / cache integrations)
- TypeScript in your app if you author
.backend.tsmodules as TS
MIT
Issues and pull requests are welcome. Flight improves fastest with real workloads—if you hit an edge case, open an issue with a minimal repro.
Flight succeeds Avian—the component-oriented Node server that helped prove this programming model. Dan Stevenson created Avian and carried it into FlyPaper Technologies, where Nick Fredericks, Dan, and the FlyPaper team sharpened Avian’s component boundaries and pushed its operational story. Dan continued to maintain Avian while Flight took shape to embrace newer tooling and a cleaner baseline for the next decade.
Today Flight is maintained by the ThoughtPivot engineering team and contributors like you—the same spirit of openness and iteration, with a hard focus on speed, clarity, and deployment flexibility.