The story
Data, screens, access, email, background jobs, servers, monitoring, tests, deploys, backups. Each one usually brings its own tool, its own config file and a few words of its own. FrontierJS gives each one a home, and keeps the vocabulary small enough to carry.
Information systems design is the art of compressing reality into ones and zeros: balancing fidelity to truth against the power of abstraction. Part science, part judgment.
Most apps say the same thing over and over. The schema says a Lead has an email. So do the validator, the TypeScript type, the access middleware and the API docs. That's five copies, and sooner or later two of them disagree.
Worse, you have to keep all five in your head and guess which one is right. Add a job queue, a mail provider, a deploy script and a monitoring tool, each with its own config and its own words for things, and the picture stops fitting.
Every duplicated truth eventually becomes a duplicated mental model.
See: Before & after
Before your app can do anything, it has to know something. What's a Lead? What fields does it have, what does it belong to, and what can't be left blank? You write that once, in db/schema.lite. Declare model Lead and db.lead already works. Litestone takes the tables, the types and the migrations from that one file.
Data is the foundation, not ORM boilerplate.
The app side of FrontierJS has three words. A Model is what the app knows. A Service is how it's reached: Junction serves lead as an API straight from the schema. A Resource is how it's shown: Lead.mesa builds its form from the same definition, and its list updates live over WebSocket.
Need a marketing site, a widget, a browser extension, a desktop app or a CLI? Each one is a folder next to api/ and web/, built from the same three nouns.
Learn three words and you can guess where anything lives.
Predict it: why is this field a select?
See: Flow
Who's allowed to see a Lead? Edit one? Change its status? You answer that on the model itself: @@gate for who can read and write, @encrypted and @guarded for sensitive fields, and @@transitions for which status changes are allowed.
Those rules are enforced at the data, so no route, job or agent can get around them. The UI can hide a button with x-gate, but the server checks either way. The audit trail logs protected fields as [redacted] without being asked, and @frontierjs/auth handles sign-in, sessions and API keys.
Access is declared in the schema, not written into hooks.
Predict it: who is allowed to delete a Lead?
Mail goes out, payments come in, and other systems want to hear when something changes. Every outside service you talk to is declared in one place, with its own timeouts and retries, and every call to it goes through app.conduit.send(). When something changes in your app, webhooks go out. They're delivered at least once and retried if the other end is down.
Every outside service has one address in your code.
See: Seams
Some of the most important work happens when no one's clicking anything. Save a Lead and queue a follow-up in the same transaction, so the job only runs if the save really happened. Caravan runs the queue and the cron schedule, right on SQLite. A notification lands in the app, live on screen and in an inbox at once.
An AI agent working through MCP gets exactly the permissions your gates already set, and nothing more. Orion, for triggers and automations, is being ported.
The same rules apply to work nobody clicked.
Your app has to live somewhere other than your laptop. Config differs per environment, secrets stay out of the repo, and every service needs a port. Config is read through $.config, or app.configFor(tenant) for a single tenant, and fli env:* moves environment values between machines. Ports follow a formula of environment, category, project and service, so dev, test and prod never collide and you can work out any port without looking it up.
.env is currentWhere it runs is part of the design.
Every request carries a trace id. Health checks and metrics are registered right where the code lives, and the devtools panel shows calls as they happen. Outpost runs on each server, and Basecamp, itself a FrontierJS app, watches the whole fleet. fli deploy:logs is there when you need the raw output.
Find out it's broken before a customer tells you.
Changing code shouldn't feel like defusing something. createTestEnv gives your tests a real database and a real API, built from the schema. Migrations come from diffing the schema. fli test:access writes your whole access policy out as a snapshot, so a change to who can do what shows up in the diff.
fli proves reads your change and names the checks it needs, and fli done tells you whether you're finished.
You can see what a change does before it ships.
fli release:mint works out exactly what would deploy, without changing anything. fli deploy ships it, the deploy journal records every attempt, and fli deploy:rollback puts the previous release back.
fli db:backup takes a hot backup of every database the schema declares, and fli db:import pulls a copy of production down to your machine. One gap: restoring a backup into production is still a manual step.
Shipping is repeatable, and so is undoing it.
fli check turns your project's conventions into checks that fail, so you don't have to remember them. fli commands are written in markdown, so your project's own commands sit next to the framework's and you can read what they do. The editor extension, not yet published, understands .lite and .mesa. And an AI agent helping with your code reads the same map you do.
Your tools follow the same rules as your app.
Add it up. For the app: Model, Service and Resource. For shipping: a Release. For testing: a Suite. Under all of it sit three rules: one origin, one name, one owner. Every fact starts in one place, every thing has one name, and every job has one owner.
Here's what that buys you. Take the @@gate on Lead. You wrote it once, and it shows up in five places:
| Data | the query refuses rows the caller can't see |
|---|---|
| API | a denied call gets its status from the one place that maps errors |
| UI | x-gate hides the button, and the server enforces the rule either way |
| Agent | the tool isn't listed for a caller who can't use it |
| Proof | fli test:access puts the change in the diff |
Predictability is the product.
Code tells you what. It doesn't tell you why. The reasons go in DECISIONS.md, what's broken goes in ISSUES.md, and what isn't built yet goes in IDEAS/. fli reads all three together: fli next suggests what to work on, fli file logs a new issue, and fli decide records a decision. Basecamp, the biggest app built on FrontierJS, is the worked example.
fli can readThe next person reading this might be you, six months from now.