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.
pnpm add @niscorp/moss @niscorp/charter @niscorp/nova @niscorp/prism @niscorp/strata @niscorp/tide @niscorp/vex zod @electric-sql/pgliteAn 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.
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 };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.
const charter = {
public: ['programme'],
member: ['programme', 'seats'],
manager: { extends: ['member'], allow: ['takings'] },
};
const assignments = { grace: ['member'], ruth: ['manager'] };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.
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') },
};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.
const app = defineApp({
charter,
assignments,
actions,
shell: { canvases: [{ id: 'main', initial: ['takings', 'seats', 'programme'] }] },
});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.
const server = await createServer(app, runtime);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.
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();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.
npx tsx boot.mtsnobody [ 'programme' ] grace [ 'programme', 'seats' ] ruth [ 'programme', 'seats', 'takings' ]