Skip to content

Repository files navigation

FrontierJS

A full-stack JavaScript framework built on Bun. One schema. Three realms. Everything derived.


The Problem

Web development doesn't have a writing problem. It has a coherence problem.

The same intent gets redeclared across schemas, validators, APIs, and interfaces until no single source of truth remains. A field is declared in the database, redeclared as a type, redeclared again as a validator, and once more as a form shape. Each declaration is a gap — a place where one system doesn't know what another already said.

FrontierJS closes the gaps so there is nothing left to cross.


How It Works

Every FrontierJS application is built across three realms, each with one primary noun:

Realm Noun Concern
Data Model What exists and what rules govern it
API Service What operations are exposed and how
UI Resource How the interface binds to and consumes a service

The schema is the seed. Declare a model once — its fields, types, constraints, relations, and access rules — and the rest of the application grows from it. The API layer reads the schema to configure the service and generate validation. The UI layer reads it to produce blank instance factories. Add a field and it appears everywhere. Add a gate and it is enforced at the database boundary before any application code runs.

schema.lite
    │
    ├── Litestone  →  tables, migrations, gate enforcement
    ├── Junction   →  service shape, validation, OpenAPI spec
    └── Sierra     →  make() factories, form field references, incoming transforms

The Stack

Package Realm What it does
@frontierjs/litestone Data Schema-first SQLite ORM with a gate system, plugin pipeline, and tenant registry
@frontierjs/junction API Bun-native service framework with HTTP + WebSocket transport, hook pipeline, and real-time channels
@frontierjs/sierra UI Vite meta-framework with file-system routing, resource factory, and fine-grained reactive runtime
@frontierjs/mesa UI Reactive component language and compiler — the runtime Sierra is built on
@frontierjs/cli Tooling fli — the single interface to all of the above

The FJS World

The packages are the application layer. The FJS World is the operational environment every application runs within — eight domains, each owning a named concern:

Domain Concern Tool
01 · CLI Developer interface fli
02 · Database Schema, migrations, ORM Litestone
03 · Config & Secrets Environment config, secrets frontier.config.js
04 · Integrations Outbound connections, messaging Conduit
05 · Automation Background jobs, queues, workflows Caravan · Orion
06 · Auth Identity, sessions, access FJS Auth
07 · Command Central Deploy, monitor, operate Basecamp
08 · Application The application itself — realms, testing, deployment

Getting Started

npm install -g @frontierjs/cli

fli new my-app
cd my-app
bun run dev

API runs on :8100, web on :8000 — the FJS port scheme, packages/cli/core/ports.js.

Alpha. The published @frontierjs/cli is 0.0.0-beta.0 and lags this repo. Only Litestone and the CLI are on npm at all — see Publishing status. To work against current code, clone this repo and bun install.

Quickstart is the whole path — a new app, a model of your own, and the deploy pipeline that puts it on a server. Every command in it was run against a clean scaffold.

For the API realm on its own, the Junction example ladder and its UI counterpart in Sierra's example app.


A Minimal Application

The schema is the starting point. Everything else is derived.

// db/schema.lite

enum LeadStatus { new active closed }

model Lead {              // PascalCase, singular — always. Accessor: db.lead
  id        Int        @id
  name      String     @length(1, 200) @trim
  email     String     @email
  status    LeadStatus @default(new)
  value     Float      @gte(0)
  createdAt DateTime   @default(now())
  updatedAt DateTime   @default(now()) @updatedAt

  @@gate("0.4.4.6")   // Read=STRANGER  Create/Update=USER  Delete=OWNER
}
// api/server.ts — Data → API connection

const db         = await createClient({          // one options object, never positional
  schema:  './db/schema.lite',
  plugins: [gatePlugin],
})
const jsonSchema = generateJsonSchema(db.$schema)

app.services.register(createService({
  name:    'leads',      // the URL: /leads
  model:   'lead',       // the accessor: db.lead
  schema:  jsonSchema,   // 400s derived from the schema's own rules
  channel: 'leads',      // declare the broadcast target — no publish hook needed
  hooks: {
    before: { all: [authenticate] },
  },
}))
// web/src/resources/Lead.mesa — API → UI connection

<script module>
  import { resource } from '@/core/frontier'

  const _res = resource.createResource({ model: 'Lead', service: 'leads' })

  export const { store, service, load } = _res
  export const make = spec => _res.make(spec)
</script>

One schema. One service declaration. One resource binding. Any component that imports Lead.mesa gets a live, reactive window into the data — HTTP for writes, WebSocket push for real-time sync.


Design Principles

The schema is the source of truth. Declarative constraints are always preferred over imperative logic. If something can be declared in the schema, it should be.

Boundaries are checkpoints, not walls. Realms are separated intentionally. Communication flows through defined boundaries, carried by context, governed by hooks. One realm does not reach into another's internals.

Access is declared, not programmed. The gate system defines minimum trust levels per model, per operation, enforced at the database boundary. It cannot be bypassed from a route someone forgot to protect.

Real-time is core. Every service emits events after writes. Every resource subscribes to them. A FrontierJS UI is live by default — open two tabs, make a change in one, the other updates without a refresh.

Solve for the 80, leave an escape for the 20. FrontierJS makes the common decisions for you. Every feature has a documented path for cases it does not cover.


Requirements

  • Bun >= 1.0 — required, not optional
  • Node.js is not supported

Documentation

Document Description
Quickstart A new app to a deployed server, in the order you type it
Philosophy Why FrontierJS exists — the axioms and decision tests above the architecture
Architecture & Vocabulary The mental model (§1), the mandatory vocabulary (§2), the eight domains (§4)
Decisions Dated rulings — read before relitigating any semantics
Verifying How to know something here is true: run it, probe failure paths, don't trust docs
Realm Bridge Reference The named cross-package handoffs, and the file each one lives in
Issues The open register — every defect, gap and unruled question, one id each
Handoff Current state, newest session first. Narrative; the ledger lives in Issues

Runnable examples

Every example below is verified end-to-end, not sketched. A broken one is a bug.

Example What it shows
Junction ladder minimal/elegant.tsfullstack/single-file.ts — the API realm
Sierra example app The UI half — a real Junction API over SQLite, driven in headless Chrome
Litestone guides Getting started, schema, querying, migrations, multi-tenancy

Package Documentation

Package README
Litestone — Data realm ORM packages/litestone
Junction — API realm framework packages/junction
Sierra — UI meta-framework packages/sierra
Mesa — Reactive component language packages/mesa
CLI — fli packages/cli
Auth — identity, sessions, gate enforcement packages/auth
Caravan — SQLite job queue + cron packages/caravan
Conduit — outbound boundary (app.conduit.send()) packages/conduit
Notifications — in-app + email fan-out packages/notifications
CSS — semantics-first design system packages/css
Jetty — browser-extension app container packages/jetty
VS Code — Litestone + Mesa language support packages/frontierjs-vscode

Project Structure

This is the layout. It is not a suggestionfli create scaffolds it, every package README assumes it, and Sierra's schema auto-detection (../db/schema.lite) only finds the schema because the UI sits one level down in web/.

Three directories at the app root, one per realm, all orbiting the shared schema — and a fourth, widgets/, wherever an app ships embeddable scripts:

my-app/
  frontier.config.js         ← environment config

  db/                        ← Data realm — Litestone
    schema.lite              ← single source of truth
    migrations/
    backups/

  api/                       ← API realm — Junction
    index.ts                 ← bun --watch entry
    config/
      junction.config.js
    src/
      app.ts
      core/                  ← env, db client, auth, hooks
      services/              ← *.service.ts, autoloaded at boot
    test/

  web/                       ← UI realm — Sierra + Mesa (the Vite root)
    index.html
    config/
      vite.config.js         ← configuration lives in config/, not at the root
      sierra.config.js
      routes.js              ← generated, do not edit
    public/                  ← static assets, copied verbatim
    src/
      main.js
      App.mesa
      routes/                ← file-system routes (.mesa / .md)
      resources/             ← createResource() bindings to Junction services
      components/
    test/
    dist/                    ← build output

  widgets/                   ← UI realm — embeddable scripts (optional)
    index.html               ← the dev harness
    config/
      vite.config.js         ← the Vite root is widgets/, port 8200
      sierra.config.js       ← target: 'widget'
    src/
      Embeds/                ← one component per embeddable script
      styles/
    test/                    ← a host page per widget, hostile CSS on purpose
    deploy/                  ← serve.js + Dockerfile — the widget origin
    dist/embeds/             ← the built scripts, one <script src> each

  extension/                 ← UI realm — a browser extension, MV3 (optional)
    config/
      jetty.config.js        ← name, permissions, islands, both browsers' blocks
    src/
      harbor/index.js        ← the service worker — required, and the only thing
                               here holding a Junction connection
      dock/App.mesa          ← the popup
      options/  piers/       ← the options page · full-page surfaces
      islands/*.js           ← content scripts, FLAT — a subfolder throws
    public/icons/            ← a 128px PNG; a store upload needs one
    test/                    ← what to load unpacked, and what to check by hand
    deploy/                  ← packaging for the two web stores
    dist/chrome/ dist/firefox/

  deploy/                    ← everything about shipping — Dockerfile, deploy steps
  tests/                     ← cross-project integration tests
  wiki/                      ← project documentation

The database lives at the root — shared by all sub-projects, owned by none of them. api/, web/, widgets/ and extension/ are peers; none contains another, and none contains db/.

Which surfaces an app has is the app's business. fli new --template api-only leaves out web/; --template widgets-only and --template extension-only leave out both api/ and web/, because a project whose whole product is embeddable widgets — or a browser extension — is a normal FrontierJS project. fli check's app-layout rule asks only that the schema is at the root and that no surface is hiding inside another: folded into web/, a surface inherits the SPA's build, its port and its release, and the first symptom is it shipping when the app does.

A surface is its own sub-project when its config, its tests and its release are a different set of answers from the SPA's. Both optional ones are:

widgets/ extension/
Config target: 'widget' — N self-contained IIFEs, not one app jetty.config.js — emits a manifest; one source, two browsers
Tests a host page it does not own, with hostile CSS loaded unpacked into a browser profile; no URL to point at
Release static files on an origin a stranger's page links to signed upload to two web stores, review in days
Ports 8200 dev · 8300 served 8400 dev (the reload channel; nothing is served)
Create it fli make:widget <Name> fli make:extension

Both generators create the surface the first time and top it up after, so the app a scaffold wrote is the app the next command extends.

Every sub-project has the same six folders, widgets/ and extension/ included, so knowing one means knowing all of them:

Folder Holds
config/ configure your settings — vite.config.js, sierra.config.js, junction.config.js
src/ develop your project
public/ static assets, served or copied as-is
test/ test your code
dist/ build output, ready for distribution
deploy/ everything related to shipping the app

All Sierra code lives under web/config/ and src/ belong to the UI realm, not to the app root. web/ is the Vite root: index.html and the dev server's working directory are there, and the build runs as cd web && vite -c config/vite.config.js. Sierra locates sierra.config.js by looking beside vite.config.js first, so the config/ pair needs no extra wiring.


Publishing status

FrontierJS is alpha. Only two packages are on npm today; everything else is workspace-only and is consumed through workspace:*, not the registry.

Package On npm Notes
@frontierjs/litestone npm latest is 1.1.0. Pin ^1.1.0 — never latest or *
@frontierjs/cli npm Published as 0.0.0-beta.0; the in-repo version is ahead of it
@frontierjs/junction not yet published workspace only
@frontierjs/sierra not yet published workspace only
@frontierjs/mesa not yet published workspace only
@frontierjs/auth not yet published relative ../junction/* imports block publishing
@frontierjs/caravan not yet published workspace only
@frontierjs/conduit not yet published workspace only
@frontierjs/notifications not yet published workspace only
@frontierjs/css not yet published workspace only
@frontierjs/jetty not yet published workspace only

License

MIT

About

FrontierJS Ecosystem

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages