An app, step by step
The smallest complete app: a schema, a server, a page with a list of orders, and a sign in.
This page builds an app with clients and their orders. Each step is a file of the app repo. A new app starts from a template with the same files, so this is also a tour of what an agent finds on its first day.
1. The schema#
schema.ts is the single source of truth for data. It declares the collections, their fields and their rules. The types of every record come from it, with no code generation.
import { auth, base, defineSchema, number, relation, select, text } from 'locosdk/schema'
const signedIn = '@request.auth.id != ""'
const rules = { listRule: signedIn, viewRule: signedIn, createRule: signedIn, updateRule: signedIn, deleteRule: signedIn }
export const schema = defineSchema({
users: auth({
fields: { name: text(), role: select(['admin', 'agent']) },
listRule: 'id = @request.auth.id',
viewRule: 'id = @request.auth.id',
updateRule: 'id = @request.auth.id',
}),
clients: base({ fields: { name: text({ required: true, presentable: true }), city: text() }, ...rules }),
orders: base({
fields: {
client_ref: relation('clients', { required: true }),
label: text({ required: true }),
status: select(['open', 'done']),
amount: number(),
},
...rules,
}),
})
A rule left out is null: only a superuser passes it. More: Schema as code, Rules and filters.
2. The server#
server.ts makes the app and serves it. createApp opens data/app.db, brings its tables in line with the schema, and mounts the PocketBase API, the pages and GET /api/health.
import { createApp } from 'locosdk/server'
import config from './loco.config.ts'
import { schema } from './schema.ts'
import index from './src/index.html'
const app = createApp({ schema, app: config.app, pages: { '/*': index } })
app.serve()
serve() listens on PORT, else 8090. Hooks, routes and crons go between the two lines: Server: hooks, routes, crons.
3. The identity#
loco.config.ts names the app. It is plain data: the portal's settings rewrite the app block in place.
import type { LocoConfig } from 'locosdk/src/config/config.ts'
export default {
app: { name: 'Orders', color: '#0b7285', logo: null, lang: 'en' },
} satisfies LocoConfig
The theme derives every shade from color. lang is en or fr: the language a visitor starts in.
4. The data layer#
src/data.ts holds the app's one client, its observable database and its session.
import { createClient, createDb } from 'locosdk/client'
import { notify, Session } from 'locosdk/ui'
import { schema } from '../schema.ts'
export const client = createClient(location.origin, { schema })
export const db = createDb(client, { notify })
export const session = new Session(client)
db.orders is typed from the schema. A name outside the schema fails the typecheck. More: Client and data layer.
5. A collection on screen#
defineCollectionUI describes a collection once. The grid, the record card, the form and the export all read it.
import { defineCollectionUI } from 'locosdk/ui'
import { db } from '../../data.ts'
export const clients = defineCollectionUI(db.clients, {
title: 'Clients',
label: (c) => c.name,
sort: 'name',
form: { fields: ['name', 'city'] },
columns: [
{ id: 'name', label: 'Name', value: (c) => c.name },
{ id: 'city', label: 'City', value: (c) => c.city },
],
})
6. The page#
src/main.tsx mounts the shell. AppRouter gives the sign in page, the guard, the navbar and a not found page.
import 'locosdk/ui/setup'
import { createRoot } from 'react-dom/client'
import { AppRoot, AppRouter, LiveGrid } from 'locosdk/ui'
import { clients } from './collections/clients/index.tsx'
import { client, session } from './data.ts'
import config from '../loco.config.ts'
createRoot(document.body.appendChild(document.createElement('div'))).render(
<AppRoot app={config.app} client={client}>
<AppRouter
session={session}
languages={['en', 'fr']}
pages={[{ path: 'clients', label: 'Clients', icon: 'group', element: <LiveGrid ui={clients} deletable exportMenu={{ filename: 'clients', formats: ['csv'] }} /> }]}
/>
</AppRoot>,
)
locosdk/ui/setup is always the first import: it installs the globals X (Mantine), H (Mantine hooks) and I (icons).
7. Run it#
bun install
bun --hot server.ts
# in a second terminal, once the app booted
bunx golo superuser upsert admin@example.com
The first boot creates data/app.db. superuser upsert reads the password on stdin. golo is the bin of locosdk: bunx golo runs it from the app's own install. Open the app on port 8090, sign in, and the Clients grid pages, sorts, searches and exports through the server.
8. Check it#
golo gate
golo gate runs the typecheck, the tests and the lint in parallel and exits 1 on red. It also fails when the code changed and CHANGELOG.md has no line for it: Changelog and releases.