FrontierJS
Skip to content
Family A · The analogy stackBorrow a known name per realm. The connection is the new part.
A1The straight version — a row per realm, each anchored, each with its twist.Densest but most informative. Risk: eight rows is a lot for a first screen.

A fullstack framework · alpha

You already know every part. The wiring is the new part.

Nothing here asks you to learn a new paradigm. Each realm is shaped like something you have already used — and all of them read the same db/schema.lite, so a field you add shows up in the migration, the API's validation and the browser's form rules without being written three times.

Data
API
UI
Static
Jobs
Email
Style
Tooling
A2Same idea, playful register — a family tree with named parents.Most shareable, most quotable. Risk: cute can read as unserious.

Closest thing · several things at once

What if they'd all been designed together?

Every part of this stack has a well-known relative. The difference is that they share a parent instead of a package registry: one schema, one hook pipeline, one error boundary, one CLI. You are not integrating six good libraries. You are using one thing that already agrees with itself.

Prisma · schemaZenStack · policiesSQLite · one fileFeathers · services + hooksLaravel · batteriesSvelte · compilerSvelteKit · routingTanStack · data bindingAstro · islandsSidekiq · jobsMJML · emailArtisan · CLI

↓

FrontierJS

One schema seeds all of it. Three realms, one vocabulary, fourteen packages that were written to fit.

  • Write the schema — the API and its rules are derived from it
  • Write a six-line service when the derived one isn't enough
  • Bind the UI to the same schema, rules and permissions included
A3Round one's BaaS hero, corrected — the frontend half was missing.Widest audience. Risk: “BaaS” sets an expectation of a dashboard.

Closest thing · Supabase, Firebase, PocketBase

A backend that derives itself. And the frontend that knows it.

A hosted platform gives you the first half: declare your data, get endpoints, auth, realtime and types. Then it hands you a client SDK and the browser starts again from zero — you re-type the validation, re-decide who may see the edit button, and hope the two halves stay in agreement.

Here both halves come from the same file. And there is no platform: it is a package in your node_modules, MIT, over a SQLite file you own.

The half you'd get anywhere

  • Schema → REST API, no handler written
  • Validation and 400s from the field rules
  • Row permissions compiled into every query
  • Realtime on mutation, migrations, types

The half that usually stops at the SDK

  • resource.fields — the same rules, client-side
  • resource.can() — the model's own permissions, for hiding a button
  • make() — a form the schema describes
  • Components that also render to static HTML, email and an extension

Client-side permissions are an affordance, never a boundary — the server enforces regardless. That is the point of them living in one place.

Family B · The complete frameworkLaravel / Rails / Django — but it crosses the boundary they stop at.
B1The concept map, extended — every row familiar until the last two.Instantly legible to a Laravel dev. Risk: says nothing to anyone else.

Closest thing · Laravel, Rails, Django

That kind of complete. In TypeScript. Past the boundary.

A queue, a scheduler, mail, notifications, policies, migrations, generators, one CLI — the things that make a framework feel finished rather than started. The difference is where they stop. In all three the model layer ends at the network boundary, and the browser starts again from nothing.

Here the schema keeps going: the form knows the field rules and the button knows the permission, because both read the file the migration came from.

You knowHere
Artisan · rails→fli
Eloquent · ActiveRecord→litestone
Migrations→diffed from schema.lite
Policies · Pundit→@@gate · @@allow
Form requests · validation→derived from the fields
Resource controllers→createService({ model })
Middleware→hooks · around/before/after/error
Queues · Scheduler→app.jobs
Mail · Notifications→app.notify
Broadcasting→app.channel
Blade · ERB · Jinja→.mesa
Inertia · Hotwire→sierra
no equivalent — the validation rules, in the browser→resource.fields
no equivalent — the model's permissions, in the browser→resource.can()

Everything above the line you can already get. The two below it are the argument.

B2The same claim without naming anyone — the boundary itself is the visual.Works on a reader with no Laravel background. Risk: less immediate hook.

Where every other framework stops

Full-stack usually means two stacks and a fetch.

The server has a model, validation, policies, a hook pipeline. The browser has none of it, so it gets a second implementation of the same three facts — in a different language, kept in agreement by hand, and discovered to be wrong by a user.

FrontierJS puts the same four concepts on both sides of the wire, and the same schema under both.

API realm · server

  • createService({ model })
  • hooks — around before after error
  • validation from the field rules
  • @@gate · @@allow, compiled into SQL
  • the result envelope

UI realm · browser

  • createResource('leads')
  • hooks — around before after error
  • resource.fields · resource.validate()
  • resource.can() — affordance, not boundary
  • the same envelope, unwrapped

↑ one schema under both ↑

B3Convention over configuration, extended to the whole app layout.Strong for the Rails crowd. Risk: layout is table stakes, not a differentiator.

Closest thing · Rails' conventions

Convention over configuration. Including the browser.

One layout for the whole application: a directory per realm, the schema above all of them, and the same six folders inside each. You do not decide where config goes, where tests go, or how the frontend finds the API — and because the convention is shared, a generator can write into all three at once.

  • fli make:resource lead writes the service and the route
  • model Lead → service leads → db.lead, checked in three places
  • fli project:map prints what exists and what derives from what
my-app/
├── db/              the seed — one schema, above both consumers
│   └── schema.lite
├── api/             API realm
│   ├── config/  src/  public/
│   └── test/    dist/ deploy/
└── web/             UI realm
    ├── config/  src/  public/
    └── test/    dist/ deploy/

# the same six folders in every sub-project,
# so nothing has to be looked up twice
Family C · The assembled stackYour own package.json — the pain is supplied by the reader.
C1The dependency diff — fourteen good libraries that never met.Most concrete. Risk: “fewer deps” is a weak claim; the copy must fix that.

Closest thing · the stack you already assembled

The same stack. Designed together.

Nothing on the left is a bad choice. They are all good — they just never met. Each brings a config file, an error shape, a notion of a user and its own idea of what your data looks like, and holding them in agreement is the job that never appears on a ticket.

  • One schema — the ORM, the API validation and the form rules read one file
  • One hook pipeline — the same four phases in both realms
  • One error boundary — every package's errors become a status the same way
  • One CLI — scaffold, migrate, run, build, deploy

Today

// package.json
"prisma",          // data
"zod",             // validation, again
"casl",            // authorization, again
"next-auth",       // identity
"express",         // transport
"socket.io",       // realtime
"bullmq",          // jobs
"node-cron",       // schedule
"nodemailer",      // mail
"react-email",     // mail, again
"swagger-jsdoc",   // the API, described again
"react",           // ui
"react-hook-form", // the fields, again
"tailwindcss"      // style

// + the glue between all fourteen

Instead

// package.json
"@frontierjs/litestone", // Data
"@frontierjs/junction",  // API
"@frontierjs/sierra",    // UI
"@frontierjs/css"        // style

// batteries, when you want them
"@frontierjs/auth"
"@frontierjs/caravan"    // jobs + cron
"@frontierjs/notifications"

// the glue is the framework

Fewer dependencies is not the point. One vocabulary is the point — the count is just what that looks like from outside.

C2The tax restated as vocabulary — the same fact spelled four ways.The truest description of the pain. Risk: needs a reader who's felt it.

Closest thing · every stack you've integrated

Four libraries. Four words for the same thing.

Integration cost is not the install. It is that each library names the same fact differently, so you spend your afternoons translating — and every translation is a place the two can disagree without anything failing loudly.

One vocabulary is not a style preference. It is the difference between one fact and four copies of it.

Who is asking?

req.userctx.authsession.userauth()

What does this record look like?

schema.prismaz.object()openapi.yamluseForm rules

Who may read it?

RLS policymiddlewareability ruleshidden button

What went wrong?

PrismaErrorZodErrorHttpException{ error: true }

↓

All four, once

auth()model Lead@@allowtoFrameworkError()
C3One business rule, written three times — then once. The sharpest of the three.Strongest single argument on the page. Risk: it is a detail, not a category.

Closest thing · how you do it today

One rule. Three places. Until one of them forgets.

“Only the owner can read a lead” is one sentence, and most stacks make you say it in three languages: once in the database, once in the handler, once in the component that hides the row. Three chances to be right, and no error when they stop matching.

Declare it on the Model and it compiles into every query, under every caller — including the ones written after you left.

Today · database

CREATE POLICY lead_owner ON leads
  FOR SELECT USING (owner_id = current_setting('app.user')::int);

Today · API

router.get('/leads', requireAuth, async (req, res) => {
  const rows = await db.lead.findMany({
    where: { ownerId: req.user.id },   // don't forget this
  })
})

Today · UI

{lead.ownerId === session.user.id && (
  <EditButton />   // a third opinion, in a third language
)}

Here · once, on the Model

model Lead {
  (read, ownerId == auth().id)
}

// the query is filtered, the API returns 403,
// and resource.can('read', lead) knows in the browser
Nine candidates across three anchor families. Not a page to ship as-is.Styled entirely by @frontierjs/css. Round one → · Current home page →