Your first project — from zero to running
Scaffold a standalone ObjectStack project with npm, extend the data model, call the REST API and build a deployable artifact — no AI agent required.
This is the hands-on path for developers building on ObjectStack from the published npm packages — you never clone the framework repository. In about ten minutes you will scaffold a project, run it, add a field by hand, call the generated REST API, and compile the artifact you would deploy.
The by-hand path from Start here: you write every file yourself, so you understand each one the agent would touch — useful for evaluating the platform, wiring CI, or working without an agent. Want an agent to write the app instead → Build with Claude Code. The two paths produce identical projects.
Prerequisites
| Tool | Minimum | Check |
|---|---|---|
| Node.js | 22+ | node --version |
| A package manager | npm 9+ / pnpm 10.15+ / yarn / bun | npm --version |
That's all. A standalone project does not need pnpm, TypeScript pre-installed, or a database server — the default template runs on an in-memory driver, and TypeScript comes in as a dev dependency.
1. Scaffold
npm create objectstack@latest my-app
cd my-appThe scaffolder (create-objectstack) does four things:
- Copies a template into
my-app/and rewrites its identity — the npm package name, the manifestname/displayName, and a namespace derived from your project name (my-app→my_app). Every object in the template is renamed to carry that prefix (my_app_note), which is whatos validatelater enforces. - Installs dependencies (pnpm if available, otherwise npm).
- Installs the AI skills bundle for one coding agent from the
@objectstack/skillspackage, at your installed@objectstack/spec's version — copied once into.claude/skills/, to commit with the project — so a coding agent is productive in the project from the first prompt. The closing summary prints the command for any other runtime you use (Installing skills has the steps, the versions the package is published at, and the one scaffolder version that is an exception). - Writes
AGENTS.md(and.github/copilot-instructions.md) with the project conventions — including the rule to runnpm run validateafter every metadata change.
Steps 3–4 cost nothing if you never use an agent; skip step 3 (the skills
bundle) with --skip-skills if you prefer — AGENTS.md is always written.
Templates
| Template | Source | Description |
|---|---|---|
blank (default) | bundled, works offline | One object, REST API — a clean slate |
npm create objectstack@latest my-app -- --template blankblank is bundled inside the npm package, so scaffolding always works offline.
The remote content templates (todo, compliance, content, contracts,
procurement) have been retired: they were delisted from the ObjectStack
template marketplace and are no longer maintained. Asking for one by name says
so explicitly.
os init (from @objectstack/cli) is an alternative scaffolder with app /
plugin / empty templates. Its plugin template writes a metadata package —
declarative objects another stack loads — and not the kernel code plugin
os create plugin writes; Which scaffolder? routes
between all four entry points by the artifact you want rather than by the word.
npm create objectstack is the recommended entry point for new standalone projects.
2. What was generated
my-app/
├── objectstack.config.ts # defineStack() — the single entry point
├── objectstack.manifest.json # name, namespace, specVersion
├── package.json
├── tsconfig.json
├── note.test.ts # the starter test `npm test` runs
├── vitest.config.ts # its test-runner config
├── AGENTS.md # conventions for coding agents
└── src/
├── objects/
│ ├── index.ts # barrel — re-exports every object
│ └── note.object.ts # a sample object
├── views/index.ts # an empty barrel for each directory
├── actions/index.ts # `os generate` writes into, already
├── flows/index.ts # wired into objectstack.config.ts
├── dashboards/index.ts
├── apps/index.ts
└── skills/index.tsTwo files matter most:
objectstack.config.ts wires everything together. There is no
filename-suffix magic — metadata exists in the app only if it is imported here.
Every directory os generate writes into is already imported, so a generated
view, action or flow adds a file and one export line to its directory's
index.ts and is part of the stack with no edit to this file:
import { defineStack } from '@objectstack/spec';
import { ConnectorRestPlugin } from '@objectstack/connector-rest';
import { ConnectorOpenApiPlugin } from '@objectstack/connector-openapi';
import { ConnectorMcpPlugin } from '@objectstack/connector-mcp';
import * as objects from './src/objects';
import * as views from './src/views';
import * as actions from './src/actions';
import * as flows from './src/flows';
import * as dashboards from './src/dashboards';
import * as apps from './src/apps';
import * as skills from './src/skills';
// Object.values, typed by what the barrel exports, so an empty barrel still
// type-checks.
const exportsOf = <M extends object>(barrel: M): M[keyof M][] => Object.values(barrel);
export default defineStack({
manifest: {
id: 'com.example.my-app',
namespace: 'my_app',
version: '0.1.0',
type: 'app',
name: 'My App',
// Protocol major this app is authored against (ADR-0087 load-time check).
engines: { protocol: '^18' },
},
// `automation` runs flows and, per ADR-0097, materializes declarative
// `connectors:` entries at boot. The three generic executors below register
// their `rest` / `openapi` / `mcp` provider factories with it. `triggers`
// fires a flow that starts on a record change, the kind `os generate flow`
// writes.
requires: ['automation', 'triggers'],
plugins: [
new ConnectorRestPlugin(),
new ConnectorOpenApiPlugin(),
new ConnectorMcpPlugin(),
],
objects: exportsOf(objects),
views: exportsOf(views),
actions: exportsOf(actions),
flows: exportsOf(flows),
dashboards: exportsOf(dashboards),
apps: exportsOf(apps),
skills: exportsOf(skills),
});The config file is a module, and the whole module is the stack. What
defineStack() returns is the base; every named export of
objectstack.config.ts is then merged onto it as a top-level stack key,
under the export's own name. That is how the onEnable and functions hooks
are authored — and it is why a helper exported beside the stack is not read as a
helper:
export default defineStack({ /* … */ });
// ⛔ Read as a top-level stack key called `collectPackageDirs`.
// `os build` refuses it: Unrecognized key(s) on this stack definition.
export const collectPackageDirs = () => [ /* … */ ];A named export is legal only when its name is a key the stack schema
declares. Keep helpers in a sibling module — objectstack.composition.ts,
say — and import them here. Two further shapes worth recognising, neither of
them a pattern to rely on:
- a named export whose name is a declared stack key that the default export
does not carry is merged in and accepted — that is the
onEnable/functionspath; - a named export whose name the default export already carries loses to the
default, and the build still succeeds — but the loader now says so on
stderr, naming the key. A second
export const objects = [...]besidedefineStack({ objects })is still a value nothing reads; it is no longer a value nothing reads and nothing mentions. Write every stack key insidedefineStack().
The REST API needs no configuration — it is on by default for every object
with apiEnabled. The plugins: array ships the three generic connector
executors, so you can integrate an external REST / OpenAPI / MCP service as
pure metadata: add a connectors: entry naming a provider and the
automation capability materializes it into a live connector at boot — see
Automation → Flows.
src/objects/note.object.ts is a complete data model — schema, validation,
API surface, and searchability in one typed definition:
import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Note = ObjectSchema.create({
name: 'my_app_note', // namespace-prefixed, snake_case
label: 'Note',
pluralLabel: 'Notes',
icon: 'sticky-note',
fields: {
title: Field.text({ label: 'Title', required: true, searchable: true, maxLength: 200 }),
body: Field.textarea({ label: 'Body' }),
},
sharingModel: 'private', // org-wide default — an explicit, authored decision
enable: { apiEnabled: true, searchable: true },
});The packages you depend on
A standalone project uses a handful of published packages — this is the entire framework surface you install:
| Package | What it does |
|---|---|
@objectstack/spec | The protocol: defineStack, ObjectSchema, Field.* builders, all Zod schemas. Your metadata imports only this. |
@objectstack/runtime | The kernel that interprets metadata at runtime. Brings the database drivers with it — you do not install one separately. |
@objectstack/plugin-hono-server | The HTTP server that mounts the generated REST API. |
@objectstack/connector-rest · -openapi · -mcp | The three generic connector executors — register the rest / openapi / mcp provider factories so declarative connectors: entries materialize (ADR-0097). |
@objectstack/cli (dev) | The os / objectstack CLI: dev, validate, build, start. |
@objectstack/verify (dev) | The test harness: bootStack boots this app's own stack in-process, on an in-memory database and with no server port, and returns the handle a test asserts through. npm test runs the starter test note.test.ts with it. |
No driver package is listed because none is installed directly: @objectstack/runtime
depends on driver-sql, driver-sqlite-wasm and driver-memory, and the CLI carries
them too. objectstack dev resolves SQLite by default; point OS_DATABASE_URL at
Postgres / MySQL / MongoDB for production — no code changes.
3. Run it
npm run dev # objectstack dev — rebuild + server restart on changeOr with the visual Console (data browser, metadata explorer, API docs):
npx os dev --ui # then open http://localhost:3000/_console/4. Call your API
The REST API is derived from the object metadata — you wrote no routes. Data
endpoints run under the same permission model as the UI, so first sign in as
the dev admin the server seeds on an empty database
(admin@objectos.ai / admin123):
# Sign in once — stores the session cookie
curl -c cookies.txt -X POST http://localhost:3000/api/v1/auth/sign-in/email \
-H "Content-Type: application/json" \
-d '{"email": "admin@objectos.ai", "password": "admin123"}'
# Create a record
curl -b cookies.txt -X POST http://localhost:3000/api/v1/data/my_app_note \
-H "Content-Type: application/json" \
-d '{"title": "Hello ObjectStack", "body": "Created via the generated API"}'
# Query records (filtering, sorting, pagination built in)
curl -b cookies.txt "http://localhost:3000/api/v1/data/my_app_note?select=title&sort=-created_at&top=10"See the Data API for the full CRUD, batch, and analytics
surface, and the Client SDK for the typed TypeScript
client (@objectstack/client).
5. Extend the model
Add a field and a second object by hand — the same edit an agent would make.
fields: {
title: Field.text({ label: 'Title', required: true, searchable: true, maxLength: 200 }),
body: Field.textarea({ label: 'Body' }),
status: Field.select({
label: 'Status',
options: [
{ label: 'Draft', value: 'draft', default: true },
{ label: 'Published', value: 'published' },
],
}),
},New objects follow the same pattern: create src/objects/<name>.object.ts with
a name of <namespace>_<short_name>, then re-export it from
src/objects/index.ts (the barrel is what the config imports).
After every metadata change, run the gate:
npm run validateos validate checks schema compliance, CEL predicate scoping, and widget
bindings — the classes of mistakes that otherwise fail silently at runtime.
See Validating Metadata.
Beyond pass/fail, npx os lint --score grades the data model 0–100 against the
platform's design conventions (relationship patterns, missing select options,
roll-ups, name fields) — a quick health check as the model grows.
6. Build the deployable artifact
npm run build # objectstack build → dist/objectstack.jsonThe artifact is a portable, self-describing deployment unit: one JSON file
containing your entire app. Any server with @objectstack/cli can boot it —
no TypeScript, no build step on the host:
npx os start --artifact ./dist/objectstack.json --port 8080Database URL, auth secret, and environment identity stay outside the
artifact and are injected at boot via flags or OS_* environment variables —
which is exactly what makes it container-friendly.
7. Hand the project to your agent
You just did by hand what an agent does in seconds — and the scaffolder already
prepared the project for that handoff: the skills bundle is installed (step
1.3) and AGENTS.md routes each task to the right skill and enforces the
validate gate. Open the project in Claude Code (or Cursor / Copilot) and
describe the next feature instead of typing it:
Add a
statusfilter to the note list view, a dashboard with a metric of published notes, and a daily flow that archives notes older than 90 days. Runnpm run validatewhen done.
Each of those lands in a different metadata domain (UI, dashboards,
automation) — the agent loads the matching skill per task. The full loop, and a
per-domain prompt catalog, is on
Build with Claude Code.
The catalog's version is your installed @objectstack/spec's (step 1.3): when
you upgrade, bump @objectstack/skills with the other @objectstack/*
packages, then re-run
npx skills@1.7.2 experimental_sync --agent claude-code --copy -y and commit
what it changed, to keep the agent in sync with the schemas you run. Name your
own runtime after --agent if it is not Claude Code.
Next steps
Build with Claude Code
The AI-first loop — describe the app, let an agent author the metadata you just wrote by hand.
Self-Hosted Deployment
Ship the artifact you just built — Docker, Postgres, reverse proxy, health checks.
Data Modeling
The full field catalog, relationships, validation rules, and formulas.
CLI Reference
Every os command — dev, validate, build, start, generate, migrate.
Build with Claude Code
Build an ObjectStack app with Claude Code: the AI writes the metadata, you verify it in the Console, guardrails catch mistakes, and the app speaks MCP.
Anatomy of an ObjectStack App
A tour of the metadata an AI agent writes for you — objects, actions, views, apps — and how to read it so you can verify the result in the Console.