FrontierJS

The stack

Twenty pieces. One vocabulary.

Each stands alone — Litestone with any server, Mesa with any bundler. Together they share one model: Data → API → UI, seeded by a single schema.

Click any feature to see what it does, and what it saves you wiring up.

LitestoneData · ModelJunctionAPI · ServiceAuthAPI · IdentityCaravanAPI · JobsConduitAPI · IntegrationsNotificationsAPI · SliceMCPAPI · AgentOrionAPI · AutomationsSierraUI · ResourceMesaUI · ComponentThe component kitUI · KitThe design systemUI · StyleEmail kitUI · EmailPrint kitUI · PrintJettyUI · ExtensionToolbeltSubstrateTestingTesting · SuitefliToolingcreate-frontierToolingThe tooling opinionToolingBasecampControl planeEditor supportToolingOutpostOps · Fleet

Litestone →

Data · Model @frontierjs/litestone

Authorization is part of the data definition, not a layer above it.

Shape

Models · enums · relations · reusable functions

One declarative file describes your data. Functions are reusable expressions you can call from defaults and policies, so logic lives beside the data it governs.

model Invoice {
  id      Int    
  total   Float  (0)
  ownerId Int
  owner   User   (ownerId)
}

Replaces

Prisma schemaDrizzle schemaTypeORM entities

Types

Int String Float Bool DateTime Json Bytes

Seven scalars, mapped to SQLite storage classes and to TypeScript. Optionality is a trailing ? and it means the same thing in the database, the API and the editor.

Replaces

Nothing — there is no common equivalent to swap out.

Access

Gates on a 0–9 scale · policies compiled into WHERE

A gate grades each operation against a trust level. A policy is a row predicate compiled into the query itself — so there is no code path that can forget to apply it.

(0.4.4.5)            // read.create.update.delete
(read, ownerId == auth().id)

-- every read becomes:
SELECT * FROM invoices WHERE ownerId = ?

Replaces

CASLOsoPostgres RLShand-rolled middleware

Fields

@guarded · @secret · @encrypted at rest

Field-level protection travels with the field. Guarded fields never leave the data layer, secrets are redacted from logs and responses, and encrypted fields are ciphertext on disk.

Replaces

manual redaction helpersfield-encryption libraries

Lifecycle

Defaults · transforms · @updatedAt · soft delete

What happens automatically over a row's life, declared once. Transforms like @trim and @lower run on the way in, so the database never holds the unnormalized version.

email     String     
updatedAt DateTime 
deletedAt DateTime?          // soft delete

Replaces

ORM lifecycle hooksparanoid / soft-delete plugins

Audit

@@trail(audit), per model

Mark a model and every write is recorded with the actor, the operation and the before/after. No decorator on every mutation, no forgetting the one that matters.

Replaces

audit-trail librariesdatabase triggerstemporal tables

Querying

Relations · cursor pagination · transactions · $raw

A full query API with nested reads, aggregates, windowing and distinct — plus a documented raw escape hatch for the cases a builder should not try to own.

await db.invoice.findMany({
  where:   { total: { gte: 500 }, owner: { is: { active: true } } },
  orderBy: { createdAt: 'desc' },
  cursor:  { id: 120 },
  take:    20,
})

Replaces

KnexKyselyraw SQL scattered in handlers

Multi-db

@@db(name) · database-per-tenant registries

Route models to separate databases from the schema, and get database-per-tenant isolation from one accessor rather than a connection-juggling layer of your own.

Replaces

manual connection routingper-tenant connection pools

External

@@external mirrors tables you don't own

Describe a table another system owns and query it with the same client. Migrations leave it alone, so you get typed reads without claiming responsibility for the schema.

Replaces

a second read-only ORM client

Migrations

Diffed from the schema as reviewable SQL

Change the schema, and the diff against the live database becomes a migration file you read before applying. Drift between file and database is a command, not a surprise.

$ fli db:migrate
  + ALTER TABLE invoices ADD COLUMN company TEXT
  ✓ migrations/0002_company.sql applied

$ fli db:status
  ✓ live database matches the schema

Replaces

Prisma MigrateKnex migrationsFlywayhand-written SQL

Generates

TypeScript declarations · JSON Schema

The same file emits types for your editor and JSON Schema for validation and the browser — so the three can never describe different shapes.

$ litestone types ./types.d.ts =client

interface Invoice       { id: number; total: number }
interface InvoiceCreate { total: number }

Replaces

openapi-typescriptzod-to-tsorvalhand-written types

Tooling

Litestone Studio · REPL · doctor · seed · backup

A local web UI over your data, an interactive shell against the live client, a health check for your setup, and seeding and backup as first-class commands.

Replaces

Prisma StudioTablePlusad-hoc scripts

Junction →

API · Service @frontierjs/junction

Name a model and its CRUD, validation and authorization come with it.

Services

Five methods plus named actions

A service is the unit, not a route. It exposes find, get, create, patch and remove, plus any custom action — callable over HTTP, over WebSocket, or in-process.

createService({ name: 'invoices', model: 'invoice',
  async markPaid(ctx) { … },
})

// same service, three ways in:
GET /api/invoices
ws  { service: 'invoices', method: 'find' }
app.service('invoices').call('markPaid', id)

Replaces

Express routersFastify pluginsNestJS controllers

Hooks

around · before · after · error

Four phases, per method or for all of them, at app or service level. around wraps everything including failures, which is where timing and tracing belong.

hooks: {
  around: { all:    [timing] },
  before: { create: [setOwner] },
  after:  { create: [broadcast] },
  error:  { all:    [report] },
}

Replaces

Express middlewareFastify hooksNestJS interceptors

Context

One shape: auth · client · route · locals · query

Every hook and every method receives the same object, with the caller, the parsed query, the result and the error in known places — not a request object you decorate by convention.

Replaces

req/res + res.locals conventions

Envelope

Lists keep { data, total }; singles unwrap

One module decides the response shape for the entire framework, so pagination metadata is always in the same place and a single record is never wrapped for no reason.

GET /api/invoices     → { object: "list", data: [...], total: 17 }
GET /api/invoices/8   → { id: 8, total: 500 }

Replaces

hand-rolled response wrappers

Transport

HTTP and WebSocket over one bridge

Both protocols enter through the same boundary and produce the same Context, so a service does not know or care which one a caller used.

Replaces

Express + Socket.IO wired separately

Directives

$limit $offset $orderBy $select, parsed at the edge

Result shaping is transport syntax, split from filters once at the boundary. Past that line nothing sees a $ — services receive clean filters and a directives object.

GET /api/invoices?status=open&$limit=20&$orderBy=-createdAt

ctx.query      → { status: "open" }
ctx.directives → { limit: 20, orderBy: "-createdAt" }

Replaces

query-string parsersmanual pagination plumbing

Security

CORS · CSRF · headers · rate limiting

The usual middleware stack, configured rather than assembled, and ordered correctly by default — preflights short-circuit before CSRF, which is easy to get wrong by hand.

Replaces

helmetcorscsurfexpress-rate-limit

Channels

One announcement, two fan-outs, gated on the way out

Declare channel: on a service and every mutation announces once, reaching both the event bus and WebSocket subscribers. Gates still apply, so nobody is pushed a row they could not fetch.

createService({ name: 'invoices', channel: 'invoices' })

// subscribers receive:
{ type: 'invoices created', data: { id: 9 } }

Replaces

Socket.IO rooms + manual emit calls

Errors

Named classes; data denials normalize into them

Throw BadRequest or NotFound anywhere and the transport serializes it correctly. A data-layer denial becomes a 403 with the same shape, so clients match one contract.

Replaces

http-errorsBoomad-hoc error middleware

Client

Typed resources and live subscriptions in the browser

The same service API from the other side of the wire, including subscriptions and session handling — not a fetch wrapper you maintain alongside the server.

Replaces

axios wrappersgenerated API clients

Testing

Real apps, real pipelines, no port binding

Boot an app in-process, send real requests through the full hook pipeline, and stub only auth. Tests exercise the code that runs in production.

const app = await createTestApp({ db, auth: createStubAuth() })
await request(app).post('/api/invoices').send({ total: 5 })

Replaces

supertest + mock layers

Auth →

API · Identity @frontierjs/auth

A route establishes a session. Everything after it is an ordinary service.

Sessions

Register · login · logout · password reset · email verify

Login cannot be gated by login, so these five are raw routes and always will be. Everything past that line is a service with the normal pipeline over it.

Replaces

Auth.jsLuciaPassportClerk

Cookies

cookieAuth: true authenticates end to end

One session works for a fetch, for a browser navigation and for an OAuth callback — which is a redirect, and cannot carry a bearer header.

Replaces

session middleware beside a second token scheme

OAuth

The flow engine is split from the transport

Building the URL, trading the code and normalizing the identity are one module with no HTTP, no database and no session in it. The plugin owns the routes; the provider list is data.

Replaces

Passport strategies

Two-factor

TOTP, base32 and recovery codes — clockless

RFC 6238 with the instant as a parameter rather than a read of the clock, so a code is testable and a drifting device is a value you pass in.

Replaces

otplib wired by handspeakeasy

Services

account · sessions · api-keys

What a signed-in person does next is not a route. Three ordinary services, gated like everything else, so there is no second authorization story for account management.

Replaces

bespoke /me endpoints

Schema

Two .lite files, split by who owns them

User is appended into your own schema so you can edit it; the machinery models are imported, so an upgrade reaches an installed app. extend model adjusts them at your end rather than by pasting.

extend model User {
  company String?
}

Replaces

copy-pasted auth tablesa migration you own forever

Hooks

Four awaited callbacks; a throw refuses the call

Each one runs BEFORE the thing it can refuse, so declining a registration is returning early rather than undoing a write.

Replaces

after-the-fact event listeners

Caravan →

API · Jobs @frontierjs/caravan

Background work in the database you already have. No second service to run.

Queue

SQLite job queue and cron on app.jobs

Queues, retries and exponential backoff in the same database as everything else — so local development, CI and a single-box deployment all run the real thing.

Replaces

BullMQ + RedisAgendanode-cron

Declaration

The file names the job, and the file is the whole of it

send-email.job.ts is send-email. A defineJob naming anything else is refused at load rather than registered, because a handler answering to send-emial while every dispatch says send-email never runs and never says so.

export default defineJob({
  handler: async ({ data }) => { await mail.send(data) },
  queue: 'mail',
  maxAttempts: 5,
})

Replaces

a registry file listing every job

Recurring

cron is an option on the handler, not a second system

A job file declares its own schedule, so moving the file moves the schedule. A name is one schedule — registering it twice replaces, where two entries used to fire the job twice a minute.

Replaces

node-cron tablesa crontab on the host

Idempotency

dispatch({ id }) is a no-op for all time

A stated id is the jobs table’s primary key, so the same dispatch can never run twice — where a unique key frees itself the moment the job goes terminal.

Replaces

dedupe keys with a TTL

Identity

A job runs as whoever asked for it

The principal travels with the dispatch, so a queued write is gated exactly as the request that queued it would have been. Background work is not a hole in the access model.

Replaces

a service account every job shares

Recovery

Heartbeats, crash resume, two instances on one queue

A running row carries the id of the instance holding it and that instance heartbeats, so a worker killed mid-job is replaced rather than leaving the job claimed forever.

Replaces

a stuck-job cleanup script

Operator

caravan queue … from a shell, JSON out

The verbs an operator needs are a binary, not an admin page — which is what a paused deploy runs inside the serving container.

Replaces

Bull Boarda psql session

Conduit →

API · Integrations @frontierjs/conduit

Every third party an app talks to, declared in one place.

Targets

A counterparty is named once — credential, policy and all

Because targets are declared, an application’s entire outbound surface is a list rather than an audit. Useful in review, and to anyone self-hosting who wants to know what the process can reach.

Replaces

scattered fetch callsa per-vendor HTTP client each

Receivers

The route a Provider calls you on, declared beside its Target

A connector exports how the vendor signs, the app says which key verifies and what to do, conduit runs the order and keeps the receipt — so a redelivery is a duplicate only once the first was applied, and a throw hands the sender a clean retry.

Replaces

a raw webhook route per vendoran inbound-events table per app

Policy

Seven numbers per target, falling back field by field

One conduit carries a card processor, a mail sink and a health probe. One set of timeouts and retries fits none of them, so each target states its own and inherits the rest.

Replaces

one global axios instance

Encoding

json or form, decided in the transport

The raw body is the same string the auth headers are built over, so an encoder living in a caller or a connector signs bytes that were never sent.

Replaces

hand-built form bodies beside a signer

Signing

Method, path, timestamp, nonce and a hash of the body

A captured signature cannot be replayed, moved to another endpoint, or kept while the body is swapped underneath it. The same module verifies on the far side.

Replaces

a bespoke HMAC helper per integration

Strictness

An unknown field on a descriptor is refused by name

Dropped silently is how a mistyped timeout becomes a production default that nobody can find.

Replaces

config objects with loose shapes

Observers

Callbacks that receive and cannot act

A throw in one is swallowed, so the word on the option says the tier: these observe. The management pipeline keeps the word hook, because that one can refuse the call.

Replaces

event emitters whose failures vanish

Not a wall

Conduit is where an outbound call belongs — not a boundary around the process

Nothing here blocks a raw fetch, and two batteries dial out directly on purpose. A contract with a working default is what keeps this package optional.

Replaces

Nothing — there is no common equivalent to swap out.

Notifications →

API · Slice @frontierjs/notifications

One call fans out to a record, a live event and an email.

Definition

A notification is a file, and the file names it

OrderPaid.notification.ts is OrderPaid — the same rule job files already follow, so there is no registry to keep in step with the filesystem.

export default defineNotification({
  via:    (p) => p.total > 100 ? ['inApp', 'email'] : ['inApp'],
  format: async (p) => ({ title: `Order ${p.id} paid` }),
})

Replaces

NovuCouriera hand-rolled fan-out

Fan-out

app.notify → in-app record, WebSocket event, email

One send reaches every transport the definition asked for, and the in-app record is a row like any other — queryable, gated, and readable back.

Replaces

three separate send calls

The type is persisted

type: holds the name still

The type is a column the browser reads to pick a renderer, so renaming the file orphans every existing row. State it and the file is free to move; a divergence is warned about.

Replaces

Nothing — there is no common equivalent to swap out.

Recipient

The unit of address is a Recipient, not a User

A notification can reach an address that was never an account — an invitee, a webhook, a shared inbox — without inventing a user row to hold it.

Replaces

a nullable user on every notification

Transports

inApp and email, plus drivers of your own

A transport is a delivery medium, kept separate from the broadcast set a WebSocket subscriber joins — two things one word usually covers.

Replaces

per-medium send helpers

Introspection

app.notifications answers what this app can send

Formatters may be async and take the payload rather than closing over it, so the catalog can be listed with nothing actually sent.

Replaces

reading the source to find out

MCP →

API · Agent @frontierjs/mcp

An MCP endpoint over the app you are already running, with the gate as the permission model.

Mounting

One plugin, inside the running API

Stdio would have to boot the app a second time — a second job worker, a second migration check, and an event bus no browser tab is on. Mounted, a tool call is one more caller of the process that is already serving.

import { mcp } from '@frontierjs/mcp'

app.configure(mcp())   // POST/GET/DELETE at {apiPrefix}/mcp

Replaces

a separate MCP server process

Scoping

Tools are computed per standing, not described in a prompt

What an agent may do is the gate it is already graded by. A stranger sees a stranger’s tools; a prompt can be talked out of a rule, and the database layer cannot.

Replaces

a system prompt listing what not to touch

One path

A tool call goes through app.service(name)

The same hooks, transaction and announcement an HTTP or WebSocket call gets, so nothing an agent does is invisible to a browser watching the same rows.

Replaces

tool handlers that call the database directly

Arguments

The schema the browser already gets

Each tool’s input is the client-audience JSON Schema, and no call through this package can ask for the system audience — so a protected column cannot reach a tool description.

Replaces

hand-written tool schemas

Stateless

Nothing is kept between two requests

The projection is rebuilt at the caller’s standing on every request, so nothing an agent was shown outlives a sign-out.

Replaces

Nothing — there is no common equivalent to swap out.

Orion →

API · Automations @frontierjs/orion

Automations that run inside your app, against your schema, refused by your gates.

Installing

A schema import and one plugin

Flows, versions and runs are declared models with real gates, so they are data in your database rather than rows in somebody else’s.

// db/schema.lite
import "@frontierjs/orion/orion.lite"

// api
import { orion } from '@frontierjs/orion'
app.configure(orion())

Replaces

Zapiern8na hosted workflow service

Flows

Triggers, conditions and actions

A trigger is a model write, a cron schedule, a webhook or a manual run. An action creates or patches a model, calls a service, makes an HTTP request or sends a notification. All of them are nouns the app already has.

Replaces

webhook glue code

Access

A flow runs as its owner

Every step goes through the same gates a person does. A flow cannot read or write anything its owner could not, and run history redacts protected fields the way the audit trail does.

Replaces

a service account with every permission

Durability

Runs on the job queue and survives a crash

The run is checkpointed once per stage, so a worker killed mid-run is replaced by another that finishes without repeating the finished stages. A wait can last days and resumes once.

Replaces

TemporalInngesta cron job and a status column

Screens

Flows, runs and credentials, mounted by one file

The builder and the run inspector are components from the same kit as the rest of the app. Each node’s settings form is built from its schema, so a node you add gets a form too.

Replaces

Nothing — there is no common equivalent to swap out.

Sierra →

UI · Resource @frontierjs/sierra

Your file tree is the route table. A Resource is a service, made reactive.

Routing

File tree · dynamic · catch-all · layouts · groups

Routes come from the filesystem, so the URL structure and the folder structure cannot drift. Layouts nest by directory and groups organize without affecting the path.

src/routes/invoices/index.mesa   → /invoices
src/routes/invoices/[id].mesa    → /invoices/42
src/routes/[...404].mesa         → anything else
src/routes/_module.mesa          → the layout

Replaces

React Router configTanStack RouterNext app directory

Frontmatter

Title · guards · render mode, per route

Route metadata lives at the top of the route file rather than in a separate config, so moving a page moves its configuration with it.

Replaces

route config objectsroute manifest files

Navigation

Guards · hooks · prefetch · scroll restore · slots

A navigation API with before and after hooks, guards that can redirect, link prefetching, and scroll restoration handled rather than reimplemented per app.

Replaces

React Router loaders and guards

Resources

The API's four hook phases, client-side

A Resource binds a service to reactive state, with the identical around/before/after/error phases the server uses. Learn the pipeline once and it holds on both sides.

const invoices = createResource('invoices', {
  hooks: { around: { all: [withSpinner] } },
})
await invoices.find({ status: 'open' })

Replaces

TanStack Query + a storeSWR + ZustandRTK Query

make()

A blank record shaped by the schema's defaults

Start a create form from the schema itself, with declared defaults already applied and server-owned fields left out. No hand-written initial state to fall out of sync.

invoices.make()
// { total: 0, status: 'draft' }  — id and createdAt omitted

Replaces

hand-written initial form state

Schema

JSON Schema shipped into the bundle

The build puts your schema in the browser, so the client knows the same field rules the server enforces — without a second copy of them written in a client-side validator.

Replaces

duplicated Zod schemas on the client

Targets

spa · static · widget · extension

One route tree, several outputs, chosen at build time. A prerendered marketing page, an embedded widget and a browser extension come from the same components.

Replaces

separate build configs per output

Loading

Route loaders · parallel fetch · connection state

Data loading declared per route and resolved in parallel, with live connection state you can render against instead of tracking your own online flag.

Replaces

suspense boilerplatemanual loading flags

Devtools

Routes, resources, requests, events, jobs

An in-page overlay fed by the server's own telemetry, so you see the request, the hooks that ran and the job it queued in one timeline.

Replaces

React Query DevtoolsRouter Devtoolsnetwork tab archaeology

Build

Vite · state-preserving HMR · auto-imports

Vite underneath with Mesa compilation, hot replacement that keeps signal state across edits, and auto-imports so common components need no import line.

Replaces

hand-tuned Vite configplugin wiring

Mesa →

UI · Component @frontierjs/mesa

One file renders to a page, an email, a fragment or a module. Zero dependencies.

Reactivity

Signals · watchProxy() makes plain objects reactive

Fine-grained signals with automatic dependency tracking. State stays a plain object — no wrapper to unwrap, no dependency array to keep correct.

let total = 0
$: withTax = total * 1.2      // recomputes when total changes

Replaces

React hooks + dependency arraysMobXImmer

Template

{#if} {#each} {#await} {#key} · snippets · bindings

Block directives rather than expressions, keyed lists, await blocks for promises, reusable snippets, and two-way binding that does not require an onChange handler.

{#each rows as row (row.id)}
  <Row {row} />
{:else}
  <EmptyState />
{/each}

Replaces

JSX + .map() + ternaries

Styles

Scoped per component

A style block applies to its own component only, with no naming convention to maintain and no runtime style injection.

Replaces

CSS Modulesstyled-componentsEmotion

Source

.mesa or Markdown with frontmatter

Content-shaped pages stay content-shaped: a Markdown file compiles to the same component, with tables and syntax highlighting already handled.

Replaces

MDX

Targets

DOM · SSR · static · email · fragment · modules

The same component renders live in the browser, to an HTML string on the server, to a prerendered file, to an email, or to an isolated fragment for embedding.

Replaces

separate SSR and email renderers

Attachments

{@attach} — an element handed to your code

The escape hatch for a chart library or a focus trap: you get the node and a teardown. It does not run on the server, which is what makes a server-rendered page safe to write this way.

Replaces

useEffect + a refdirectiveslifecycle hooks

Leaf

No framework dependency of its own

Mesa sits at the bottom, so it can be adopted with any bundler and any server. The rest of the framework is what happens when you keep going.

Replaces

Nothing — there is no common equivalent to swap out.

The component kit →

UI · Kit @frontierjs/ui

Sixty-odd components over the design system. A form that already knows what it is editing.

Forms

<Form> takes a resource; every control resolves itself

A control reads its own label, its constraints and its server error from the form context, so a field is one tag with a name on it and the three things that usually drift cannot.

<Form resource={invoices} record={invoice}>
  <Input name="total" />
  <Select name="status" />
</Form>

Replaces

React Hook Form + a resolverFormik + Yup

Attributes

Every component forwards its caller’s attributes

A form control puts them on the CONTROL rather than the wrapper, because that is what a label’s for and an aria-describedby have to reach. A kit that swallows them cannot be made accessible from outside.

Replaces

wrapper components you fork to add an id

Autosave

<Form autosave> saves once the typing stops

The debounce, the in-flight state and the last-write-wins question are the form’s, not yours — one attribute rather than an effect in every screen.

Replaces

a useEffect with a timer per form

Style

A tone and a treatment, never a color

danger and outlined are what a button is; blue is what it happens to look like this theme. A kit component may not style a class the design system owns, so the two cannot fight.

Replaces

colour propsclassName overrides

Destructive

A destructive action confirms itself

The confirmation is part of the component rather than a modal you remember to wire, so the dangerous path is the one with a guard on it by default.

Replaces

a confirm modal copied per screen

Contributing

A control of your own is two registrations

A custom control joins the form contract the same way the built-in ones did, so it gets labels, errors and constraints without special-casing.

Replaces

a control that only works outside the form

Forms

FormButtonFieldFieldsetLabelInputTextareaSelectCheckboxSwitchRadioGroupNumberInputSliderComboboxMultiSelectDatePickerDateTimeInputJsonInputFileUpload

Display

BadgePillTagDotKbdMonoDividerBreadcrumbsPaginationStepsSectionHeaderCalloutEmptyStateCopyButtonCodeAvatarAvatarGroupStatStatCardTableBarSparklineAccountStatusJson

Layout

CardAccordionAccordionItemTabsTabListTabTabPanel

Overlay

ModalDrawerPopoverTooltipDropdownMenuDropdownItemDropdownGroupDropdownSeparatorCommandPaletteConfirmationPopoverConfirmPanelConfirmProvider

Feedback & stores

AlertAlertProviderToastToasterProgressSpinnerSkeletontoastStorealertStorecommandPaletteStore

The design system →

UI · Style @frontierjs/css

Thirty-five semantic terms. Plain CSS, no build step. This site is styled by it.

Layers

tokens → themes → tones → base → components → patterns

Cascade layers give an explicit precedence contract, which replaces the usual "do not reorder these imports" comment with something the browser enforces.

Replaces

import-order conventionsBEM discipline

Override

Your CSS is unlayered, so it always wins

Because every rule in the package sits in a layer, any unlayered rule you write beats it — no specificity war, and no !important.

/* yours, unlayered — wins by definition */
.btn { border-radius: 0 }

Replaces

!importantarbitrary-variant escape hatches

Contrast

Fill and text derived together — AA by construction

Text color is computed from the fill's own luminance, so any tone in any theme — including a brand color added later — meets AA without being audited.

Replaces

manual contrast auditsaxe passes after the fact

State

From [aria-current] and :user-invalid, never a class

Styling keys off the accessibility state that has to be correct anyway. A tab cannot look selected while telling a screen reader otherwise.

Replaces

is-active class bookkeeping

Themes

Six, swapped with one class on the root

A theme overrides token values only, so every component follows automatically — no provider, no runtime, no re-render.

Replaces

CSS-in-JS theme providers

Build

None

Plain CSS files with an import entry point. No compile step, no config, no purge pass, and no build tooling to keep current.

Replaces

Tailwind + PostCSS pipelineSass build

Frame

appshelltopbarsidebarscreenpaneview

Inline

btnpillbadgelinkchiptooltipavatarkbdicon

Surfaces

cardtilealerttoastdialogpopoverdrawer

Forms & data

fieldfield-hintswitchtabletilesfacts

Navigation & patterns

tabsbreadcrumbpaginationnavliststepsbarfeeddisclosure

Email kit →

UI · Email @frontierjs/email-kit

An email is a component. The same language, compiled for a worse renderer.

Components

Table-based blocks, compiled by the same compiler

An email body is written the way a page is, so a designer moving between the two learns nothing new and the components are testable in a browser.

Replaces

MJMLreact-emaila templating language

Compilation

an email build target inlines styles to attributes

Mail clients strip style blocks, so the compiler puts the styles where they survive rather than asking you to write it that way by hand.

Replaces

juiceinline styles written by hand

The rest of the message

Plain-text alternative and the subject, from the component

The three things a message needs come out of one file, so a subject cannot drift from the body it belongs to.

Replaces

a subject string in the calling code

Compatibility

The limits are written down

Outlook’s conditional-comment handling is the fragile part and the package’s docs say so. A compatibility claim nobody has opened in a real client is worth what it cost to write.

Replaces

Nothing — there is no common equivalent to swap out.

Jetty →

UI · Extension @frontierjs/jetty

A browser extension as a surface of the app, not a second app.

A surface

extension/ sits beside api/ and web/

It earns its own directory because its config, its tests and its release are all different answers — and it reaches the same services with the same client.

$ fli make:extension
$ fli dev:extension     # MV3, hot-reloaded
$ fli extension:build

Replaces

PlasmoWXTa hand-rolled MV3 setup

The relay

A service worker carries calls to the API

The adapter is the real client rather than a lookalike, so sign-in, live updates and gating behave exactly as they do in the app.

Replaces

fetch calls duplicated in the worker

Islands

A content script builds in lib mode

An MV3 content script is a CLASSIC script, so the preload helper a bundler normally writes makes the engine reject the whole file at parse time — before one line of it runs, with nothing at build time saying so.

Replaces

Nothing — there is no common equivalent to swap out.

The store

It remembers the query its rows answer

Nothing else can grade a record that arrives later: without the query, an update belonging to a different filter lands in the list anyway.

Replaces

a cache keyed by id alone

Permissions

extension:audit grades the manifest

A permission is easy to add while debugging and invisible afterwards, right up until a store review.

Replaces

reading the manifest before you submit

Toolbelt →

Substrate @frontierjs/toolbelt

The facts that have many possible answers and must have exactly one.

Zero dependencies

Substrate below the graph, not a member of it

That is what buys the import licence: because it depends on nothing, every package may import it without inverting the dependency direction the framework is built on.

Replaces

a shared utils package nobody may depend on

One kit per import

No root entry — you import the kit you need

A grab-bag with one entry point is a package every consumer pulls whole. A subpath per kit means a browser bundle carries the two you used.

import { plural } from '@frontierjs/toolbelt/inflect'
import { canAtLevel } from '@frontierjs/toolbelt/gate'

Replaces

lodasha utils folder copied between projects

Spelling

/inflect — singular, plural, table, model, human

Three separate resolvers have to agree on what a model is called. One kit is how they do, rather than three regexes that agree until an irregular noun arrives.

Replaces

pluralizea naming helper per package

Meaning

/query and /directives — what a query string says

A string is a number only if it round-trips as one; true, false and null are themselves; structure is bracket notation. Three readers agree because there is one table.

Replaces

qsa parser per boundary

Time and money

/datetime · /units

An instant read in a zone and written back, with the runtime’s own zone rules; rounding, allocation and currency data that ship rather than being read off the host.

Replaces

date-fns + a timezone librarydinero.jsIntl called directly

Rules

/gate · /predicate · /match · /signature · /redact · /cron

The access ladder and its grader, the schema expression language, whether a record still belongs in a query, a signed machine-to-machine request, whether a key is a credential, and what five fields mean.

Replaces

a copy of each, per package

Kits

inflectgatedirectivesqueryhistorymatchsignatureredactcronpredicatehooksjsonschemajsonunitsglowsearchdatetime

Testing →

Testing · Suite @frontierjs/testing

A real app, a real database, a real port when you want one. Stub nothing but the clock.

The environment

A template-cloned database, factories and a standing

Every check the Data layer ships works unchanged here, because they ask the data boundary directly rather than through a mock of it.

const env = await createTestEnv({ schema })
const orders = env.as(alice).service('orders')
await orders.create({ total: 5 })

Replaces

supertest + a pile of mockstestcontainers

Who is calling

env.as(user) binds the principal into every call

With no principal you get STRANGER, which is what an unauthenticated caller actually is — so the unhappy path is the default rather than something you remember to test.

Replaces

a fake JWT per test

Announcements

env.announced() — what the act broadcast

A mutation that should have announced and did not is invisible to an assertion on the response, and it is exactly what breaks a live client.

Replaces

spying on the socket

Transport parity

The same call down HTTP and WS, compared

Two real transports grading each other. A service that behaves differently depending on how it was reached is a bug no single-transport suite can see.

Replaces

Nothing — there is no common equivalent to swap out.

A real port

Asked for as zero and read back

For the tests that need an origin — a browser, a webhook, a redirect — without hard-coding a number that collides with the app you left running.

Replaces

a port constant per suite

Where it sits

Above the API, and imported by nothing

Which is the whole reason it is a separate package: the Data layer’s own test environment cannot mount an app without importing the API layer, and that would invert the dependency direction.

Replaces

Nothing — there is no common equivalent to swap out.

fli →

Tooling @frontierjs/cli

Every command is a markdown file. Help text and implementation cannot disagree.

Scaffold

project:new · make:model :service :resource :route

Generators append to the schema you already have rather than dropping a pile of boilerplate, so the seed stays the single source after generation.

$ fli make:model Invoice
  ✓ appended to db/schema.lite
  ✓ api/src/services/invoices.ts
  ✓ src/routes/invoices/

Replaces

YeomanPlopHygen

Data

db:migrate :status :studio :seed :backup

The whole data lifecycle in one namespace — diff and apply migrations, check for drift, browse rows, seed fixtures and take backups.

Replaces

prisma CLIknex CLIad-hoc scripts

Run

ports:claim claims a port session per project

A broker assigns and remembers ports per project, so two apps on one machine never collide and the URLs are stable between restarts.

$ fli ports:claim
  api → http://localhost:3200
  web → http://localhost:5173

Replaces

concurrentlymanual PORT juggling

Ship

deploy infers the environment from your git branch

Build, upload, migrate and cut over in one command, with the target derived from the branch you are on rather than a flag you have to remember.

Replaces

bespoke shell scriptshand-written CI deploy jobs

Recover

deploy:rollback · :status · :logs · :doctor

Releases are versioned, so going back is a command. Status reports what is actually running, and doctor checks the target before you find out the hard way.

Replaces

manual SSH and symlink swaps

Secrets

env:set :get :list :pull, per environment

Environment values are managed per environment and never committed, with pull to bring them down and copy to promote between environments.

Replaces

dotenv-vaultcopying .env files by hand

Monorepo

workspace: link · graph · version · publish

Workspace commands for linking packages, viewing the dependency graph, versioning together and publishing — without adding another build orchestrator.

Replaces

LernaChangesetsNx

Extend

make:command writes a new markdown command

Frontmatter declares flags, arguments and examples; the body is the script. Your team's operational knowledge becomes readable files instead of tribal npm scripts.

--- commands/reports/weekly.md ---
title: reports:weekly
flags:
  since: { type: string, defaultValue: 7d }
---

Replaces

npm scripts + a README that drifts

create-frontier →

Tooling create-frontier

The front door, and nothing else — it resolves the CLI and runs it.

One implementation

It resolves @frontierjs/cli and runs fli new

What a new app looks like is written once, in the CLI. A create package with a scaffolder of its own is a second answer that drifts from the first release onward.

Replaces

create-next-appcreate-vite

Where it lands

The target directory is pinned, not searched for

fli walks UP to find a project root, which is right when you are inside an app and wrong when npm create runs you inside an existing repo — without the pin the scaffold lands in that repo’s root.

Replaces

Nothing — there is no common equivalent to swap out.

Pass-through

Arguments are forwarded untouched

Every flag the scaffolder has works here on the day it is added, because this package does not know what they are.

Replaces

a wrapper that lists flags and forgets one

The tooling opinion →

Tooling @frontierjs/config

tsconfig, biome and editorconfig as a dependency — never as a copy.

Extend, don’t copy

One line in your tsconfig.json

A copied config is frozen at the moment it was written. A dependency moves when the framework does, which is the entire difference between a template and a package.

{ "extends": "@frontierjs/config/tsconfig" }

Replaces

a config pasted from a starter

No formatter

Linter only, and the refusal is measured

This house aligns columns and no formatter can express that. The code the scaffolder writes is aligned, so the first format run would rewrite the app the scaffold had just produced.

Replaces

Prettierdprint

The boundary

A linter owns generic JavaScript

It reads neither .mesa nor .lite, on purpose: everything derived from the seed is graded by the framework’s own checks, which can see the schema the linter cannot.

Replaces

ESLint plugins that parse your DSL

Basecamp →

Control plane @frontierjs/basecamp

The app for managing your apps — and itself a FrontierJS app.

Releases

What is deployed where, and one button to undo it

Every environment, its current release, what changed since the last one, and a rollback that does not require finding the right host first.

Replaces

deploy dashboardsSSH sessions

Migrations

Applied, pending and drifted, per environment

Read from each app's own manifest endpoint, so drift between the schema in your repo and the database in production is visible before it bites.

Replaces

running migrate status by hand on each host

Jobs

Queues across every app — in flight, failed, retry

One view over every Caravan queue you run, with failures and their reasons, and retries triggered from here.

Replaces

Bull BoardAgendash

Audit

Every trail, searchable by model, actor and window

The @@trail(audit) trails from all your apps in one place, so answering "who changed this record" does not start with a database console.

Replaces

log aggregation queries

Egress

Each app's declared targets — everything it can reach

Because Conduit targets are declared, the complete outbound surface of an application is a list rather than an audit. Useful for review, and for self-hosters who want to know.

Replaces

reading the source to find fetch calls

Tenants

Registries, sizes, migration state, and a way in

Database-per-tenant means many databases. This is the list, their sizes, whether each is migrated, and a route into any one of them.

Replaces

bespoke admin scripts

Health

Liveness and metrics, including plugin-contributed

Every app exposes /health and /metrics, and plugins add their own providers — so a new battery appears here with no configuration.

Replaces

Grafana dashboardsuptime checkers

Live

Over the same channels your apps use — not polling

Basecamp subscribes the way any client does. It is a demonstration that the realtime layer works, because it is the thing depending on it.

Replaces

polling dashboards

Editor support →

Tooling frontierjs-vscode

The seed is as pleasant to write as the code it produces.

Diagnostics

Errors as you type, from the parser the runtime uses

The extension runs the same parser the framework runs, so the editor cannot tell you something is fine that the build will reject.

Replaces

finding schema errors at runtime

Completions

Types, attributes, functions, and every model in scope

Field types, @attributes, @@model-attributes and your own schema functions — including models pulled in through schema imports.

Replaces

memorizing attribute names

Hover

Attribute and type docs, model and function signatures

What an attribute does and what it takes, in the file, without a documentation tab.

Replaces

tab-switching to docs

Go to def

Model references and function calls, across imported files

Jump from a relation to the model it points at, or from a function call to its block, following schema imports across files.

Replaces

project-wide text search

Format

Aligned field columns, on command or on save

Canonical formatting for schema files, so aligned columns are automatic rather than something reviewers ask for.

Replaces

manual alignmentformatting nits in review

Mesa

Markup, script, style, directives and interpolation

Component files highlight correctly across all three regions, including block directives and interpolation inside markup.

Replaces

plain-text component files

Outpost →

Ops · Fleet @frontierjs/outpost

The process on the machine. A control plane’s hands, with a signature on every one of them.

Signed by default

Every route but unsigned liveness requires a signature

One of these routes runs a shell command as the process’s user, so refuse is the default and a route opts out rather than in.

Replaces

an agent behind a firewall rule

The verbs

pull · deploy · stop · health-check · exec · logs · prune

The route table is read off the control plane’s own engines rather than invented here, so a verb it can ask for is a verb this answers.

Replaces

SSH plus a pile of shell scripts

Injectable runner

The thing that touches Docker is a parameter

Which is what makes the package testable with no daemon and no network, and lets the control plane’s own drive run the REAL server over a fake machine.

Replaces

tests that need a docker socket

Digests

It builds on the target and answers what it built

The digest comes back from the machine that produced it, so what is running is identified by content rather than by a tag someone may have moved.

Replaces

latest tagstrusting the registry