Thank you for your interest in contributing to ObjectOS! This guide will help you get started.
ObjectOS is the runtime engine that executes metadata defined in the ObjectQL format. It's part of a two-repository ecosystem:
- objectql/objectql: The protocol definition and core drivers
- objectstack-ai/objectos (this repo): The runtime implementation
This is a Monorepo managed by PNPM workspaces and organized as follows:
objectos/
├── packages/
│ ├── agent/ # @objectos/agent - AI Agent Framework
│ ├── analytics/ # @objectos/analytics - Analytics Engine
│ ├── audit/ # @objectos/audit - Audit Logging
│ ├── auth/ # @objectos/auth - Authentication (BetterAuth)
│ ├── automation/ # @objectos/automation - Triggers & Rules
│ ├── browser/ # @objectos/browser - Offline Runtime (SQLite WASM)
│ ├── cache/ # @objectos/cache - LRU + Redis Cache
│ ├── federation/ # @objectos/federation - Module Federation
│ ├── graphql/ # @objectos/graphql - GraphQL API
│ ├── i18n/ # @objectos/i18n - Localization
│ ├── jobs/ # @objectos/jobs - Background Jobs & Cron
│ ├── marketplace/ # @objectos/marketplace - Plugin Marketplace
│ ├── metrics/ # @objectos/metrics - Prometheus Metrics
│ ├── notification/ # @objectos/notification - Email/SMS/Push/Webhook
│ ├── permissions/ # @objectos/permissions - RBAC Engine
│ ├── realtime/ # @objectos/realtime - WebSocket Server
│ ├── storage/ # @objectos/storage - KV Storage Backends
│ ├── telemetry/ # @objectos/telemetry - OpenTelemetry Tracing
│ ├── ui/ # @objectos/ui - Admin Console shared components
│ └── workflow/ # @objectos/workflow - State Machine Engine
├── apps/
│ ├── web/ # @objectos/web - Admin Console (Vite + React 19)
│ └── site/ # @objectos/site - Documentation (Next.js + Fumadocs)
├── api/ # Hono API routes & middleware
├── examples/ # Example applications (CRM, Todo)
├── docs/ # VitePress documentation (guides, spec)
├── e2e/ # Playwright E2E tests
└── scripts/ # Build & maintenance scripts
| Package | Role | Dependencies |
|---|---|---|
@objectos/auth |
Identity — BetterAuth, SSO, 2FA, Sessions | @objectstack/spec, @objectstack/runtime |
@objectos/permissions |
Authorization — RBAC, Permission Sets | @objectstack/spec, @objectstack/runtime |
@objectos/audit |
Compliance — CRUD events, field history | @objectstack/spec, @objectstack/runtime |
@objectos/workflow |
Flow — FSM engine, approval processes | @objectstack/spec, @objectstack/runtime |
@objectos/automation |
Triggers — WorkflowRule, action types | @objectstack/spec, @objectstack/runtime |
@objectos/jobs |
Background — queues, cron, retry | @objectstack/spec, @objectstack/runtime |
@objectos/realtime |
Sync — WebSocket, presence | @objectstack/spec, @objectstack/runtime |
@objectos/graphql |
GraphQL API — schema generation, subscriptions | @objectstack/spec, graphql |
@objectos/agent |
AI — LLM agents, tools, orchestration | @objectstack/spec, @objectstack/runtime |
@objectos/analytics |
Analytics — aggregation, reports, dashboards | @objectstack/spec, @objectstack/runtime |
"Kernel handles logic, Drivers handle data, Server handles HTTP."
This must be maintained at all times:
- Kernel never touches HTTP or database connections directly
- Server never touches database queries directly
- Drivers are injected via dependency injection
TypeScript
- Use strict mode (
strict: truein tsconfig) - No
any- useunknownwith type guards if needed - Prefer interfaces over type aliases for public APIs
- Use async/await for all I/O operations
Naming Conventions
- Files:
kebab-case.ts - Classes:
PascalCase - Functions/variables:
camelCase - Interfaces:
PascalCase(noIprefix) - Constants:
UPPER_SNAKE_CASE
Comments
- Use JSDoc for all public APIs
- Explain why, not just what
- Include examples for complex functions
Example:
/**
* Loads an object definition into the registry.
* Triggers a schema sync if the driver supports it.
*
* @param config The object metadata from YAML
* @throws {ValidationError} If the config is invalid
*
* @example
* await kernel.load({
* name: 'contacts',
* fields: { email: { type: 'email' } }
* });
*/
async load(config: ObjectConfig): Promise<void> {
// ...
}// ❌ BAD
interface ObjectConfig {
name: string;
fields: any;
}
// ✅ GOOD
import { ObjectConfig } from '@objectstack/spec/data';// ❌ BAD
async find(name: string, opts: any): Promise<any> {
// ...
}
// ✅ GOOD
import { FindOptions } from '@objectstack/spec/data';
async find(
name: string,
options: FindOptions
): Promise<Record<string, any>[]> {
// ...
}All new features must include tests.
- Unit Tests: For kernel logic (target: 90%+ coverage)
- Integration Tests: For server endpoints (target: 80%+ coverage)
- E2E Tests: For critical user flows
Example test:
describe('ObjectOS.insert', () => {
let kernel: ObjectOS;
let mockDriver: jest.Mocked<ObjectQLDriver>;
beforeEach(() => {
kernel = new ObjectOS();
mockDriver = createMockDriver();
kernel.useDriver(mockDriver);
});
it('should validate required fields', async () => {
await kernel.load({
name: 'contacts',
fields: {
email: { type: 'email', required: true },
},
});
await expect(
kernel.insert('contacts', {}), // Missing email
).rejects.toThrow('email is required');
});
});- Update relevant docs in
/docsfor any user-facing changes - Add JSDoc comments for all public APIs
- Include migration notes for breaking changes
- Update CHANGELOG.md
- Node.js 20+ (LTS recommended — see
.node-version) - PNPM 10+ (exact version managed via
packageManagerfield inpackage.json) - PostgreSQL or MongoDB (optional — SQLite by default)
# Clone the repository
git clone https://github.com/objectstack-ai/objectos.git
cd objectos
# Install dependencies
pnpm install
# Copy and configure environment variables
cp .env.example .env
# Edit .env to set AUTH_SECRET (required)
# Build all packages
pnpm build
# Run tests
pnpm test# Start API server (:5320) + Admin Console (:5321)
pnpm dev
# Start everything including docs site
pnpm dev:all
# Start only the API server
pnpm objectstack:serve
# Start only the Admin Console
pnpm web:dev
# Start only the docs site
pnpm site:dev
# Build for production
pnpm build
# Run the production build
pnpm start
# Run all tests (via Turborepo)
pnpm test
# Lint all packages
pnpm lint
# Type-check all packages
pnpm type-check
# Validate ObjectStack configuration
pnpm objectstack:validate
# Check development environment health
pnpm objectstack:doctor- Check GitHub Issues
- Look for labels:
good first issue,help wanted - Comment on the issue to claim it
# Create a feature branch
git checkout -b feature/your-feature-name
# Or a bugfix branch
git checkout -b fix/issue-123Follow the coding standards above and ensure:
- Code compiles without errors
- Tests pass
- Documentation is updated
- No regressions
# Run tests for specific package
cd packages/kernel
pnpm run test
# Run all tests
cd ../..
pnpm run testUse conventional commits:
# Format: <type>(<scope>): <subject>
git commit -m "feat(workflow): add hook priority support"
git commit -m "fix(graphql): handle null values in query"
git commit -m "docs(guide): add architecture examples"Types:
feat: New featurefix: Bug fixdocs: Documentation onlystyle: Code style changes (formatting)refactor: Code refactoringtest: Adding or updating testschore: Build process or tooling changes
# Push your branch
git push origin feature/your-feature-name
# Create a Pull Request on GitHub
# - Provide a clear description
# - Reference related issues
# - Add screenshots for UI changesBefore submitting, ensure:
- Code follows style guidelines
- All tests pass
- New tests added for new features
- Documentation updated
- No merge conflicts
- Conventional commit messages used
# All tests (via Turborepo, concurrency=3)
pnpm test
# Specific package
pnpm --filter @objectos/permissions test
# E2E tests (Playwright)
pnpm e2eNote: Most packages use Jest (with
ts-jestESM preset). Some packages (permissions,automation,workflow) use Vitest. Both frameworks use the samedescribe/it/expectAPI — check each package'spackage.jsonfor the exact test runner.
// packages/cache/test/plugin.test.ts
import { CachePlugin } from '../src/plugin';
describe('CachePlugin', () => {
let plugin: CachePlugin;
beforeEach(() => {
plugin = new CachePlugin({ maxSize: 100, ttl: 60000 });
});
it('should store and retrieve values', async () => {
await plugin.set('key', 'value');
const result = await plugin.get('key');
expect(result).toBe('value');
});
it('should return undefined for expired keys', async () => {
await plugin.set('key', 'value', { ttl: 1 });
await new Promise((r) => setTimeout(r, 10));
const result = await plugin.get('key');
expect(result).toBeUndefined();
});
});Each package has a test:coverage script that runs tests with coverage reporting:
# Run coverage for a specific package
pnpm --filter @objectos/cache test:coverage
# Run coverage for all packages (aggregated via Turborepo)
pnpm test:coverageCoverage thresholds are enforced per package:
| Metric | Server Packages | Frontend (apps/web) |
|---|---|---|
| Branches | 70% | 60% |
| Functions | 70% | 60% |
| Lines | 80% | 70% |
| Statements | 80% | 70% |
CI automatically collects coverage from all packages and uploads to Codecov.
Snapshot tests capture the output of a component or function and compare it against a stored reference. Use them judiciously:
- Serialized output: JSON responses, generated schema, config objects
- Error messages: Validate that error messages remain consistent
- Complex transformations: Metadata transforms, template rendering output
- Random/dynamic values: Timestamps, UUIDs, incrementing IDs — these change every run
- Large objects: Snapshots over ~50 lines become difficult to review in PRs
- UI components: Prefer behavioral tests (
getByRole, assertions on visible text) over HTML snapshots
// ✅ GOOD: Small, focused snapshot
it('should generate correct schema for object', () => {
const schema = generateSchema(objectMeta);
expect(schema).toMatchSnapshot();
});
// ✅ GOOD: Inline snapshot for small expected values
it('should format error message', () => {
const error = formatError({ code: 'NOT_FOUND', object: 'contacts' });
expect(error).toMatchInlineSnapshot(`"Object 'contacts' not found"`);
});
// ❌ BAD: Snapshot of a large, frequently-changing object
it('should render the entire page', () => {
const html = render(<DashboardPage />);
expect(html).toMatchSnapshot(); // Too large, hard to review
});When a snapshot legitimately changes (e.g., you added a field to a schema):
# Jest
pnpm --filter @objectos/graphql test -- -u
# Vitest
pnpm --filter @objectos/permissions test -- --updateAlways review snapshot diffs in your PR before committing. Never blindly update snapshots.
# Start docs site in development mode
pnpm site:dev
# Build docs site for production
pnpm site:build
# Preview the built docs site
pnpm site:previewdocs/
├── index.md # Homepage
├── guide/
│ ├── index.md # Getting Started
│ ├── architecture.md # Architecture guide
│ ├── data-modeling.md # Data modeling
│ ├── logic-hooks.md # Writing hooks
│ └── ...
└── spec/
├── index.md # Spec overview
├── metadata-format.md
└── ...
- Use clear, concise language
- Include code examples
- Add diagrams for complex concepts
- Link to related documentation
- Test all code examples
Releases are managed by maintainers using Changesets.
# Create a changeset
pnpm changeset
# Version packages
pnpm version
# Publish to NPM
pnpm release- Discord: Coming soon
- GitHub Issues: For bugs and feature requests
- GitHub Discussions: For questions and discussions
- Be respectful and inclusive
- Provide constructive feedback
- Focus on the code, not the person
- Help newcomers get started
By contributing, you agree that your contributions will be licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
Thank you for contributing to ObjectOS! 🎉