Moss · guide

Boot an app

A charter, three screens and a database go in. Moss boots, and says which screens each person’s app is made of.

Install it
pnpm add @niscorp/moss @niscorp/charter @niscorp/nova @niscorp/prism @niscorp/strata @niscorp/tide @niscorp/vex zod @electric-sql/pglite
Step 1 of 6

An environment

Moss is handed two things: the app, and an environment to run it on. The environment is a database and a way to tell who a token belongs to.

PGlite is Postgres compiled to run wherever Node runs, so this file needs no database server. dev-open trusts any token, which is for development only. A real app passes sessions, or a verifier of its own.

An environment
import { PGlite } from '@electric-sql/pglite';
import { createServer, defineApp, mintDevToken } from '@niscorp/moss';
import type { ActionDefinition } from '@niscorp/nova';
import { createPglitePool } from '@niscorp/vex/pglite';

const db = new PGlite();
const runtime = { db, pool: createPglitePool(db), session: 'dev-open' as const };
Step 2 of 6

A charter

The charter says which screens each role gets. public is the role of anybody who is not signed in. assignments says who has which role.

A charter
const charter = {
  public: ['programme'],
  member: ['programme', 'seats'],
  manager: { extends: ['member'], allow: ['takings'] },
};
const assignments = { grace: ['member'], ruth: ['manager'] };
Step 3 of 6

Three screens

Each screen is a Nova action: an id, what it holds, what a press does, and what it draws. All of it is data.

Three screens
const text = (says: string) => ({ component: 'Text', children: says });
const actions: Record<string, ActionDefinition> = {
  programme: { id: 'programme', layout: text('This week: The Tempest') },
  seats: {
    id: 'seats',
    data: { held: 0 },
    triggers: [{ event: 'ui:click', ref: 'hold', do: [{ increment: 'held' }] }],
    layout: {
      component: 'Stack',
      children: [
        text('Seats held: {{$.held}}'),
        { component: 'Button', ref: 'hold', props: { label: 'Hold a seat' } },
      ],
    },
  },
  takings: { id: 'takings', layout: text('Saturday took 4,180') },
};
Step 4 of 6

The app

defineApp takes the artifacts. The shell manifest says there is one canvas, and that it opens on the first screen of the list that the person was given.

The app
const app = defineApp({
  charter,
  assignments,
  actions,
  shell: { canvases: [{ id: 'main', initial: ['takings', 'seats', 'programme'] }] },
});
Step 5 of 6

Boot

createServer reads the whole app before it serves anything. It verifies the charter, runs the migrations for its own tables, and builds what it needs for each role.

What comes back is the server. Here it is asked directly, with no port opened. serve from @niscorp/moss/node is the same thing listening.

Boot
const server = await createServer(app, runtime);
Step 6 of 6

Ask who gets what

/catalog answers with the screens the caller’s app is made of. The caller is whoever the token says, and nobody when there is none.

Grace is a member and gets two screens. Ruth manages and gets three. Somebody who is not signed in gets one.

Ask who gets what
for (const who of [null, 'grace', 'ruth']) {
  const headers = who === null ? undefined : { authorization: 'Bearer ' + mintDevToken(who) };
  const answer = await server.request('/catalog', { headers });
  const catalog = (await answer.json()) as { actions: string[] };
  console.log(who ?? 'nobody', catalog.actions);
}
server.close();
Then

Run it.

The screens each of the three is given. In your terminal Moss and Vex also say, once each, that this setup is for development.

Run it
npx tsx boot.mts
It prints
nobody [ 'programme' ]
grace [ 'programme', 'seats' ]
ruth [ 'programme', 'seats', 'takings' ]