FrontierJS

The story

A system you can hold in your head.

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.

  1. 00

    Why it stops fitting

    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

  2. 01

    Start with what it knows

    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.

    Usually
    an ORM schema, a set of types, a validator and a folder of migrations
    Here
    one file

    Data is the foundation, not ORM boilerplate.

  3. 02

    Three nouns

    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.

    Usually
    controllers, routes, DTOs, stores and components, each with its own idea of a Lead
    Here
    Model → Service → Resource

    Learn three words and you can guess where anything lives.

    Predict it: why is this field a select?

    See: Flow

  4. 03

    Rules in one place

    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.

    Usually
    a permission check in every route, and the same check again in the UI
    Here
    the rules, on the model

    Access is declared in the schema, not written into hooks.

    Predict it: who is allowed to delete a Lead?

  5. 04

    The outside world

    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.

    Usually
    SDKs, keys and timeout settings scattered across the codebase
    Here
    one list of outside services, and one way to call them

    Every outside service has one address in your code.

    See: Seams

  6. 05

    Work nobody clicked

    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.

    Usually
    a second set of permission rules for workers, and a third for agents
    Here
    the same gates, everywhere

    The same rules apply to work nobody clicked.

  7. 06

    Where it runs

    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.

    Usually
    a list of port numbers, and a guess about which .env is current
    Here
    a formula, and one place config is read

    Where it runs is part of the design.

  8. 07

    Keep watch

    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.

    Usually
    a dashboard for each tool, and a hunch about which one to check
    Here
    one fleet view, with health declared next to the code

    Find out it's broken before a customer tells you.

  9. 08

    Know what changed

    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.

    Usually
    which tests cover what, and whether that last change touched permissions
    Here
    a diff that shows it, and a command that tells you what to run

    You can see what a change does before it ships.

  10. 09

    Ship and undo

    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.

    Usually
    the deploy steps, and what's actually live right now
    Here
    a Release, and a journal that remembers

    Shipping is repeatable, and so is undoing it.

  11. 10

    Tools that remember for you

    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.

    Usually
    the conventions, in your head, until you forget one
    Here
    a check that remembers

    Your tools follow the same rules as your app.

  12. 11

    Count what you carry

    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:

    Datathe query refuses rows the caller can't see
    APIa denied call gets its status from the one place that maps errors
    UIx-gate hides the button, and the server enforces the rule either way
    Agentthe tool isn't listed for a caller who can't use it
    Prooffli test:access puts the change in the diff

    Predictability is the product.

  13. 12

    Your head isn't the archive

    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.

    Usually
    why things are the way they are, until you forget
    Here
    a record fli can read

    The next person reading this might be you, six months from now.

    See: Map · Landscape · Journey