Most of a developer's life with a stack is not writing the app. It is scaffolding, running, migrating, deploying, watching, and eventually extending it — usually across six tools that have never heard of each other. FrontierJS treats that whole loop as one surface, with one vocabulary, and the same nouns at every stop.
fli project:new my-app ✓ Junction API + Sierra/Mesa UI ✓ db/schema.lite seeded ✓ auth wired (optional) fli make:model Lead ✓ appended to db/schema.lite ✓ api/src/services/leads.ts ✓ src/routes/leads/ # make:model, :service, :resource, :route,# :component, :schema, :command, :scaffold
Start
shipped
One command puts a Junction API, a Sierra/Mesa UI and a seeded schema on disk — then generators keep appending to the same seed rather than to a pile of boilerplate.
make:modelappends to the schema and optionally scaffolds the service and resource with it
One seedgenerators write into db/schema.lite, so the file stays the single source
clilitestonejunctionsierra
fli ports:claim claimed port session for my-app api → http://localhost:3200 web → http://localhost:5173 fli db:studio browse and edit rows in the browser litestone types ./types.d.ts Lead, LeadCreate, LeadUpdate written
Build
shipped
A port broker claims a session per project, so two apps on one machine stop fighting over 3000. Litestone Studio gives you the data; typegen keeps the editor honest.
Port brokera claimed session per project — no more "address already in use" across repos
Litestone Studioa local web UI over the live database
DevtoolsSierra ships an in-page overlay; job and request telemetry feeds it
clilitestonesierramesa
fli db:migrate diffing db/schema.lite against app.db + ALTER TABLE leads ADD COLUMN company TEXT ✓ migrations/0002_company.sql applied fli db:status ✓ live database matches the schema bun run test createTestApp() + request(app).post(…) fli deploy:doctor is the target host actually ready?
Verify
shipped
Migrations are diffed, not written — and the same command can tell you whether the live database has drifted from the file. The test kit boots a real app rather than a mock.
Drift is checkabledb:status compares the live database to the schema, so drift is a command rather than a surprise
Real apps in testscreateTestApp, request(), createStubAuth — the hook pipeline runs for real
clilitestonejunction
fli env:set RESEND_API_KEY=… --env production fli deploy branch main → environment production build · upload · migrate · cut over ✓ release 2026-08-02T14:22 live fli deploy:status containers, web release, disk, last deploy fli deploy:rollback ✓ web and API back to the previous release
Ship
shipped
Deploy over SSH, with the environment inferred from the git branch you are on. Releases are versioned, so rolling back is a command and not an incident.
Branch → environmentthe deploy target is inferred rather than remembered
Rollbackweb and API return to the previous release together
TargetsSSH hosts today, with CapRover and Cloudflare command groups alongside
cli
curl https://my-app.com/health { "status": "ok", "uptime": 84213 } curl https://my-app.com/metrics { "requests": …, "jobs": { … }, ← caravan registered this "conduit": { … } } ← and conduit this curl https://my-app.com/manifest migrations applied, pending, drift fli deploy:logs --followfli db:backup
Operate
shipped
Every app answers the same three questions the same way — because the endpoints are contributed by plugins rather than written per app. This is the surface Basecamp is built on.
Extensible metricsa plugin registers a provider and its stats appear — no per-app wiring
Manifestmigration state is served, so drift is observable in production
NextBasecamp reads exactly these endpoints — see below
clijunctioncaravanconduit
fli make:command reports:weekly ✓ commands/reports/weekly.md # ---# title: reports:weekly# description: Email the weekly rollup# flags: { since: { type: string } }# ---# <script> … </script> fli reports:weekly --since 7d # then back to step 01: add a model,# and the whole loop runs again.
Extend
shipped
A CLI command is a markdown file — frontmatter for flags, body for the script. So extending the tooling uses the same skill as documenting it, and the two cannot drift apart.
Commands are documentsthe help text and the implementation are the same file
App extensionplugins attach to Junction; providers swap implementations behind a contract
Then it loopsa new model restarts the cycle at Start
clijunction