@ossy/app
v3.10.0
Published
Server-side rendering runtime and build tooling for Ossy apps.
Readme
@ossy/app
Server-side rendering runtime and build tooling for Ossy apps.
For custom setups (Next.js, Vite, etc.), use @ossy/connected-components directly.
Setup
Add @ossy/app, react, react-dom, and @ossy/connected-components (plus its peer deps) to your package.json, then run app build.
{
"scripts": {
"build": "app build",
"start": "node build/server.js"
}
}Pages
Create *.page.jsx files in src/. Each file becomes a route.
// src/home.page.jsx
export const metadata = {
id: 'home',
title: 'Home',
path: '/',
}
export default function Home({ url }) {
return (
<body>
<h1>Welcome</h1>
<p>Current URL: {url}</p>
</body>
)
}File → route mapping: home.page.jsx → /, about.page.jsx → /about. The metadata export controls the route id, path, and page title. For multi-language paths:
export const metadata = {
id: 'about',
path: { en: '/about', sv: '/om' },
}What the framework provides: The build wraps every page automatically with <html>, <head> (including the page title, optional metadata.description / Open Graph tags, and injected styles), and <App> (providers for theme, router, SDK). Your page component only needs to return the <body> element and its content.
export const metadata = {
id: 'about',
title: 'About',
description: 'About Ossy — composable capabilities for running your business.',
ogImage: '/og-about.png',
path: '/about',
}SSR emits <meta name="description">, og:title, og:description, og:type=website, and when config.siteUrl or config.domain is set, og:url. Root-relative ogImage values (e.g. /og.png) are absolutized against that origin. Prefer {pageId}.metaDescription in translation catalogs when the copy should be localized.
Props: The full app config is passed as props to the page component — url, theme, isAuthenticated, pages, workspaceId, apiUrl, etc. You can also access config via useApp() / useRouter() hooks from @ossy/connected-components and @ossy/router-react.
Build output: Each page produces hashed ESM bundles under build/public/static/ (React included) for SSR and hydration. When config.domain or config.siteUrl is set, the build also writes build/public/sitemap.xml from static page paths (locale maps expand to every language URL; dynamic :param routes and metadata.sitemap: false are skipped).
Config
Add src/config.js to set workspace, theme, and API options:
import { CloudLight } from '@ossy/themes'
export default {
workspaceId: 'your-workspace-id',
theme: CloudLight,
apiUrl: 'https://api.ossy.se/api/v0',
}Optional flags:
| Key | Default | Purpose |
|-----|---------|---------|
| enablePushInvalidation | false | App-wide SSE cache invalidation. Prefer <PushInvalidationSubscriber /> on specific pages instead — see @ossy/sdk-react README. |
Config is loaded at build time and merged with request-time settings (e.g. user theme preference from cookies).
API routes
Create *.api.js files in src/. Each file exports a metadata object and a default handler function.
// src/health.api.js
export const metadata = {
id: 'health',
path: '/api/health',
}
export default function handle(req, res) {
res.json({ status: 'ok' })
}The handler receives the raw Express req and res. API routes are matched before page rendering. The router supports dynamic segments (e.g. path: '/api/users/:id').
Build output: build/.ossy/api.generated.json — a registry of [{ id, path, module }] entries. Handlers are lazy-imported on first request.
Background tasks (*.task.js)
Task modules export metadata (id, optional triggers / schedule) and a run function. They are registered by TaskService at server startup.
Sync invoke: POST /actions with { action, payload } awaits the primary task at {feature}/tasks/{intent} derived from the action id.
Async: changestream events and cron schedules dispatch matching tasks without blocking the caller.
Build output includes task entries in the manifest for server registration. Async execution does not use a separate polling worker — see ADR 0003.
Task ids use the canonical form @ossy/{feature}/tasks/{intent}. Action ids (@ossy/{feature}/actions/{intent}) map to their primary task via ADR 0003.
Schemas (*.schema.js)
Resource document schemas are POJOs default-exported from *.schema.js in src/ or installed feature packages. The build discovers them, validates canonical ids (@{provider}/{feature}/schema/{concept}), and merges them into manifest.schemas.
// packages/booking/src/service.schema.js
export default {
id: '@ossy/booking/schema/service',
name: 'Service',
fields: [{ name: 'name', type: 'text', required: true }],
}Forms reference a schema via metadata.schemaId (not templateId).
Forms (*.form.js)
Form modules export intent metadata only — field definitions live on the schema:
export const metadata = {
id: '@ossy/booking/form/service',
schemaId: '@ossy/booking/schema/service',
}Build manifest
app build writes build/manifest.json plus derived catalogs:
| Output | Purpose |
|--------|---------|
| manifest.json | Pages, actions, tasks, schemas, components, layouts, taskCatalog, taskGraphEdges, … |
| build/capabilities.json | Automation UI catalog — actions, tasks, schemas, edges |
| build/actions.schema.json | JSON Schema for agent/MCP tool inputs per action |
Action and task metadata are validated at build time via @ossy/schema. Task triggers reference schema ids (type: '@ossy/platform/schema/file') and lifecycle events (Created, Patched, …).
Components (*.component.jsx)
Register injectable UI via *.component.jsx in src/ or feature packages. Components are bundled separately and resolved into slots at runtime.
App chrome — the app assigns placement in *.layout.jsx:
export const slots = {
'app:header': 'app-header',
'app:sidebar': 'app-sidebar',
}Resource / input UI — feature packages use metadata.id as the slot key:
export const metadata = { id: '@ossy/booking/form/service' }
export default function ServiceForm() { … }See docs/component-primitive.md and design-system/docs/SLOTS.md.
Compact / mobile shell
Default and workspace layouts use @ossy/app/shell helpers for viewports ≤900px:
useCompactShellLayout/COMPACT_SHELL_MEDIA_QUERY— match Cloud theme compact typographyShellHeaderRow+MobileShellNav— menu control + overlay drawer hostingapp:sidebarwithpresentation="drawer"OpenMobileShellNav/CloseMobileShellNavaction POJOs on the menu/close controls (data-action) for flow coverage with{ viewport }/{ press: 'Escape' }/{ press: 'Tab' }/{ press: 'Shift+Tab' }- Header row stays when
app:headeris unset so sidebar-only pages keep primary nav - Drawer traps Tab focus via a
Viewpanel ref (flow asserts wrap withShift+Tab/Tab), restores the menu trigger on close, and keeps menu/close hit areas ≥44px - While open,
[data-ossy-app-shell]isinert+aria-hidden; body getsdata-ossy-mobile-shell-scroll-lock; the drawer panel respectsenv(safe-area-inset-*) - Compact shells pad with the same safe-area insets (not zero) so the header menu clears notches; document head uses
viewport-fit=cover - Compact header chrome (language / theme / auth) uses the same ≥44px hit areas via
compactShellControlStyle - Tab-trap helpers live in
mobileShellFocus.js(listFocusable,resolveTabTrapTarget,compactShellControlStyle,compactAppShellStyle,setAppShellBackgroundInert,setMobileShellScrollLock,mobileShellDrawerSafeAreaStyle) with unit coverage in__tests__/mobileShellFocus.test.js
See SHELL-SPEC §4.5.
Port configuration
The server listens on port 3000 by default.
PORT=4000 node build/server.js
node build/server.js --port 4000
node build/server.js -p 4000