Expert instructions for creating, configuring, and bootstrapping ObjectStack
projects. Covers project scaffolding, defineStack() configuration, driver
selection, adapter integration, and the runtime boot sequence.
When to Use This Skill
- Creating a new ObjectStack project from scratch.
- Choosing the right project template (minimal-api, full-stack, plugin).
- Writing or modifying
objectstack.config.ts(defineStack()config). - Selecting a database driver (Memory, SQL, Turso).
- Integrating with a web framework (Hono, Express, Fastify, Next.js, etc.).
- Understanding the runtime boot sequence and plugin loading order.
- Setting up multi-app composition with
composeStacks(). - Answering "how do I get started?" questions.
Decision Tree: Choosing a Template
What are you building?
│
├── A simple REST API or backend service?
│ └── ✅ minimal-api
│ • 1 object, REST endpoints, in-memory driver
│ • Fastest path to a running API
│
├── A full business application with UI?
│ └── ✅ full-stack
│ • Multiple objects, views, apps, auth
│ • Studio UI included
│ • CRM-like starter with relationships
│
└── A reusable extension for other projects?
└── ✅ plugin
• Plugin scaffold with onInstall/onEnable/onDisable
• Exports objects that other apps can import
• Designed for the marketplace
Scaffolding Command
# Interactive — prompts for name, template, package manager
npx create-objectstack
# Direct — skip prompts
npx create-objectstack my-app --template full-stack
Templates: minimal-api | full-stack | plugin
Project Structure Conventions
Every ObjectStack project follows this directory structure:
my-app/
├── objectstack.config.ts # ← THE entry point — defineStack()
├── package.json
├── tsconfig.json
└── src/
├── objects/ # Business object definitions
│ ├── task.object.ts # → exports a single object
│ └── index.ts # → barrel: export * from './task.object'
├── views/ # Optional: UI view definitions
│ ├── task.view.ts
│ └── index.ts
├── apps/ # Optional: app definitions (nav, pages)
│ ├── main.app.ts
│ └── index.ts
├── flows/ # Optional: automation flows
│ ├── task.flow.ts
│ └── index.ts
├── actions/ # Optional: action definitions
│ ├── task.action.ts
│ └── index.ts
├── dashboards/ # Optional: dashboards
├── reports/ # Optional: reports
├── i18n/ # Optional: translation bundles
└── handlers/ # Optional: runtime hook handlers
Naming Conventions
| Concept | Convention | Example |
|---|---|---|
| File names | {name}.{type}.ts |
task.object.ts, main.app.ts |
| Machine names | snake_case |
project_task, first_name |
| Config keys | camelCase |
maxLength, defaultValue |
| Barrel exports | Object.values(imported) |
objects: Object.values(objects) |
defineStack() — The Core Configuration
objectstack.config.ts is the single entry point for every project.
It calls defineStack() to declare all metadata.
Minimal Example
import { defineStack, Data } from '@objectstack/spec';
const { Field } = Data;
export default defineStack({
manifest: {
id: 'com.example.todo',
namespace: 'todo',
version: '1.0.0',
type: 'app',
name: 'Todo Manager',
},
objects: [
{
name: 'task',
label: 'Task',
fields: {
title: Field.text({ required: true }),
status: Field.select({ options: [
{ label: 'Open', value: 'open' },
{ label: 'Done', value: 'done' },
], defaultValue: 'open' }),
due_date: Field.date(),
},
},
],
});
Full Configuration Reference
defineStack() accepts an ObjectStackDefinitionInput with these top-level keys:
| Key | Type | Description |
|---|---|---|
manifest |
Manifest |
Package identity (id, namespace, version, type, name) |
objects |
Object[] or Map |
Business object definitions |
objectExtensions |
ObjectExtension[] |
Fields to merge into objects from other packages |
apps |
App[] or Map |
Application definitions with navigation |
views |
View[] or Map |
List/form view definitions |
pages |
Page[] or Map |
Custom page definitions |
dashboards |
Dashboard[] or Map |
Dashboard definitions |
reports |
Report[] or Map |
Analytics reports |
actions |
Action[] or Map |
Global and object-scoped actions |
themes |
Theme[] |
UI themes |
workflows |
WorkflowRule[] |
Event-driven workflow rules |
approvals |
ApprovalProcess[] |
Approval process definitions |
flows |
Flow[] or Map |
Screen and autolaunched flows |
roles |
Role[] |
User role hierarchy |
permissions |
PermissionSet[] |
Permission sets / profiles |
sharingRules |
SharingRule[] |
Record sharing rules |
policies |
Policy[] |
Security / compliance policies |
apis |
ApiEndpoint[] |
Custom API endpoints |
webhooks |
Webhook[] |
Outbound webhooks |
agents |
Agent[] or Map |
AI agents and assistants |
ragPipelines |
RAGPipeline[] |
RAG pipeline configurations |
hooks |
Hook[] |
Object lifecycle hooks |
mappings |
Mapping[] |
Data import/export mappings |
analyticsCubes |
Cube[] |
Analytics semantic layer cubes |
connectors |
Connector[] |
External system connectors |
data |
Dataset[] |
Seed data / fixtures |
datasources |
Datasource[] |
External data connections |
translations |
TranslationBundle[] |
I18n translation bundles |
i18n |
TranslationConfig |
Internationalization settings |
plugins |
Plugin[] |
Runtime plugins to load |
devPlugins |
Plugin[] |
Dev-only plugins (equivalent to devDependencies) |
Map Format (Key → Name)
All named collections support map format where the key becomes the name field:
export default defineStack({
// Array format (traditional)
objects: [
{ name: 'task', fields: { title: Field.text() } },
],
// Map format (key becomes name) — preferred for readability
objects: {
task: { fields: { title: Field.text() } },
project: { fields: { name: Field.text() } },
},
});
Barrel Import Pattern
Use barrel exports to keep config clean:
// src/objects/index.ts
export { default as task } from './task.object';
export { default as project } from './project.object';
// objectstack.config.ts
import * as objects from './src/objects';
import * as apps from './src/apps';
import * as views from './src/views';
import * as flows from './src/flows';
export default defineStack({
manifest: { id: 'com.example.pm', namespace: 'pm', version: '1.0.0', type: 'app', name: 'PM' },
objects: Object.values(objects),
apps: Object.values(apps),
views: Object.values(views),
flows: Object.values(flows),
});
Strict Validation
defineStack() validates by default (strict: true):
- Zod schemas — field names, types, enums
- Cross-references — views/actions/workflows reference defined objects
- Seed data — dataset objects exist in the definition
To disable (advanced — e.g., objects provided by another plugin):
export default defineStack({ ... }, { strict: false });
Manifest Reference
Every stack needs a manifest to identify itself in the ecosystem:
manifest: {
id: 'com.example.crm', // Reverse domain unique ID
namespace: 'crm', // 2-20 chars, lowercase, prefixes objects
version: '1.0.0', // Semver
type: 'app', // app | plugin | driver | module | ...
name: 'Acme CRM', // Human-readable display name
description: 'CRM system', // Optional description
}
Namespace scoping: Objects become {namespace}__{name} (double underscore)
in multi-app environments. Platform-reserved namespaces (base, system)
skip prefixing.
Driver Selection Guide
Drivers are the storage layer. Pick based on your environment:
| Driver | Package | Best For | Notes |
|---|---|---|---|
| Memory | @objectstack/driver-memory |
Dev, testing, prototyping | Data lost on restart (unless persistence adapter used) |
| SQL | @objectstack/driver-sql |
Production (PostgreSQL, MySQL, SQLite) | Uses Knex.js under the hood |
| Turso | @objectstack/driver-turso |
Edge, serverless, multi-tenant | LibSQL/Turso cloud, per-tenant databases |
Usage Pattern
import { DriverPlugin } from '@objectstack/runtime';
// Development (in-memory, zero config)
import { InMemoryDriver } from '@objectstack/driver-memory';
new DriverPlugin(new InMemoryDriver())
// Production (SQLite)
import { SqlDriver } from '@objectstack/driver-sql';
new DriverPlugin(new SqlDriver({
client: 'better-sqlite3',
connection: { filename: './data/app.db' },
useNullAsDefault: true,
}))
// Production (PostgreSQL)
new DriverPlugin(new SqlDriver({
client: 'pg',
connection: process.env.DATABASE_URL,
}))
// Edge / Serverless (Turso)
import { TursoDriver } from '@objectstack/driver-turso';
new DriverPlugin(new TursoDriver({
url: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
}))
Adapter Selection Guide
Adapters bridge ObjectStack to web frameworks. All expose the same REST API.
| Adapter | Package | Use When |
|---|---|---|
| Hono | @objectstack/adapter-hono |
Default choice. Lightweight, edge-ready, web-standard. |
| Express | @objectstack/adapter-express |
Existing Express codebase. |
| Fastify | @objectstack/adapter-fastify |
Need Fastify's schema validation / plugin ecosystem. |
| Next.js | @objectstack/adapter-nextjs |
Full-stack React with App Router. |
| Nuxt | @objectstack/adapter-nuxt |
Vue.js / Nuxt projects. |
| NestJS | @objectstack/adapter-nestjs |
Enterprise Angular-style architecture. |
| SvelteKit | @objectstack/adapter-sveltekit |
Svelte projects. |
Usage Pattern (Hono)
import { createHonoApp } from '@objectstack/adapter-hono';
const app = createHonoApp({
kernel, // ObjectKernel instance
prefix: '/api', // API route prefix (default: '/api')
});
export default app; // Deploy to Cloudflare Workers, Deno, Bun, Node
Usage Pattern (Next.js App Router)
// app/api/[...objectstack]/route.ts
import { createRouteHandler } from '@objectstack/adapter-nextjs';
import { kernel } from '@/lib/objectstack';
const handler = createRouteHandler({ kernel });
export const GET = handler;
export const POST = handler;
export const PUT = handler;
export const DELETE = handler;
Pattern Across All Adapters
Every adapter follows the same architecture:
- Accept a
kernel(ObjectKernel) instance - Create an
HttpDispatcherinternally - Mount explicit routes for auth, GraphQL, storage, discovery
- Delegate everything else to
dispatcher.dispatch()
This means new routes added to HttpDispatcher work automatically in all adapters without code changes.
Runtime Boot Sequence
Understanding how ObjectStack starts helps debug and customize:
objectstack.config.ts
└── defineStack({ manifest, objects, views, ... })
│
▼
CLI: `os serve` / `os dev`
1. Load .env files (NODE_ENV-based)
2. Dynamic import of config file
3. Create Runtime + ObjectKernel
4. Auto-detect and register plugins:
├── ObjectQLPlugin (if objects defined)
├── DriverPlugin (memory in dev, SQL in prod)
├── AppPlugin (loads the defineStack bundle)
├── I18nServicePlugin (if translations/i18n defined)
├── AuthPlugin
├── HonoServerPlugin
├── SetupPlugin (first-run wizard)
├── RESTPlugin (auto-generated API)
├── DispatcherPlugin
├── AIServicePlugin (if available)
└── StudioPlugin (if --ui flag)
5. Runtime.start() → init + start all plugins
6. Server listens on configured port
Plugin Loading Order Matters
Plugins initialize in registration order. Key dependencies:
| Plugin | Depends On | Reason |
|---|---|---|
| ObjectQLPlugin | (none) | Core data engine, should load first |
| DriverPlugin | (none) | Registers driver service |
| AppPlugin | ObjectQLPlugin | Registers objects/metadata with engine |
| AuthPlugin | ObjectQLPlugin | Needs user/session objects |
| RESTPlugin | ObjectQLPlugin, AppPlugin | Generates routes from registered objects |
| AIServicePlugin | ObjectQLPlugin, AppPlugin | Needs metadata for tool generation |
Programmatic Bootstrap (Without CLI)
import { Runtime, DriverPlugin, AppPlugin } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import appConfig from './objectstack.config';
const runtime = new Runtime();
runtime.use(new ObjectQLPlugin());
runtime.use(new DriverPlugin(new InMemoryDriver()));
runtime.use(new AppPlugin(appConfig));
await runtime.start();
const kernel = runtime.getKernel();
// kernel is now ready — use it with an adapter
Multi-App Composition
Use composeStacks() to merge multiple apps into one runtime:
import { composeStacks, defineStack } from '@objectstack/spec';
import CrmApp from './apps/crm/objectstack.config';
import TodoApp from './apps/todo/objectstack.config';
const combined = composeStacks([CrmApp, TodoApp], {
objectConflict: 'error', // Throw on duplicate object names
manifest: 'last', // Use last stack's manifest
});
export default combined;
Conflict Strategies
| Strategy | Behavior |
|---|---|
'error' (default) |
Throw if two stacks define the same object name |
'override' |
Last stack wins — later definition replaces earlier |
'merge' |
Shallow-merge objects with same name (later fields win) |
Host Pattern (Plugins as AppPlugin)
For a hosting environment where each app runs isolated:
import { Runtime, DriverPlugin, AppPlugin } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
import { AuthPlugin } from '@objectstack/plugin-auth';
export default defineStack({
manifest: { id: 'platform-host', type: 'app', version: '1.0.0', name: 'Platform' },
plugins: [
new ObjectQLPlugin(),
new DriverPlugin(new SqlDriver({ ... })),
new AuthPlugin({ secret: process.env.AUTH_SECRET }),
new AppPlugin(CrmApp), // → crm__account, crm__lead, ...
new AppPlugin(TodoApp), // → todo__task, ...
],
});
Each app's objects are prefixed by its namespace to prevent collisions.
Seed Data
Declarative data loading for bootstrapping, demos, and testing:
export default defineStack({
// ... objects, apps, etc.
data: [
{
object: 'task',
mode: 'upsert', // 'upsert' | 'insert' | 'ignore' | 'replace'
externalId: 'subject', // Idempotency key for upsert matching
records: [
{ subject: 'Learn ObjectStack', status: 'open', priority: 'high' },
{ subject: 'Build first app', status: 'open', priority: 'medium' },
],
},
],
});
| Mode | Behavior |
|---|---|
upsert (default) |
Insert or update based on externalId match |
insert |
Always insert (fails on duplicate) |
ignore |
Insert if not exists, skip otherwise |
replace |
Drop and re-insert all records |
CLI Commands
| Command | Alias | Description |
|---|---|---|
os init |
Initialize a new project in current directory | |
os dev |
Start dev server with hot reload | |
os serve |
Start production server | |
os studio |
Open Studio UI in browser | |
os compile |
Compile metadata for production | |
os validate |
Validate all metadata schemas and cross-references | |
os info |
Display project metadata summary | |
os generate |
os g |
Scaffold new objects, views, flows, etc. |
os test |
Run project tests (vitest) | |
os doctor |
Diagnose common project issues | |
os plugin list |
List installed plugins | |
os plugin add |
Install a plugin from registry | |
os plugin remove |
Uninstall a plugin |
Typical Development Workflow
# 1. Create project
npx create-objectstack my-app --template full-stack
cd my-app
# 2. Install dependencies
pnpm install
# 3. Start development with Studio UI
os dev --ui
# 4. Validate metadata
os validate
# 5. Compile for production
os compile
# 6. Serve in production
os serve --port 3000
Complete Working Example
A minimal but complete project from scratch:
package.json:
{
"name": "my-todo-app",
"type": "module",
"dependencies": {
"@objectstack/spec": "^4.0.0",
"@objectstack/runtime": "^4.0.0",
"@objectstack/objectql": "^4.0.0",
"@objectstack/driver-memory": "^4.0.0",
"@objectstack/adapter-hono": "^4.0.0",
"@objectstack/cli": "^4.0.0"
}
}
src/objects/task.object.ts:
import { Data } from '@objectstack/spec';
const { Field } = Data;
export default {
name: 'task',
label: 'Task',
fields: {
title: Field.text({ label: 'Title', required: true }),
description: Field.textarea({ label: 'Description' }),
status: Field.select({
label: 'Status',
options: [
{ label: 'Open', value: 'open' },
{ label: 'In Progress', value: 'in_progress' },
{ label: 'Done', value: 'done' },
],
defaultValue: 'open',
}),
priority: Field.select({
label: 'Priority',
options: [
{ label: 'Low', value: 'low' },
{ label: 'Medium', value: 'medium' },
{ label: 'High', value: 'high' },
],
defaultValue: 'medium',
}),
due_date: Field.date({ label: 'Due Date' }),
},
indexes: [
{ fields: ['status'] },
{ fields: ['due_date'] },
],
};
src/objects/index.ts:
export { default as task } from './task.object';
objectstack.config.ts:
import { defineStack } from '@objectstack/spec';
import * as objects from './src/objects';
export default defineStack({
manifest: {
id: 'com.example.todo',
namespace: 'todo',
version: '1.0.0',
type: 'app',
name: 'Todo Manager',
},
objects: Object.values(objects),
});
# Run it
os dev --ui
# → Server at http://localhost:5174
# → REST API at http://localhost:5174/api
# → Studio UI at http://localhost:5174/studio
Zod Schema References
When you need precise type definitions, read these bundled reference files:
| File | What It Contains |
|---|---|
references/kernel/manifest.zod.ts |
Manifest schema (id, namespace, version, type) |
references/data/datasource.zod.ts |
Datasource connection config |
references/data/dataset.zod.ts |
Seed data schema (mode, externalId, records) |
For the full defineStack() / composeStacks() implementation, read
packages/spec/src/stack.zod.ts directly.
Read references/_index.md for the complete list with descriptions.