From 03327962600882f51fed76b0117dd03c616d9eca Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 06:23:47 +0000 Subject: [PATCH 1/5] docs: add third-party developer onboarding and self-hosted deployment guides Evaluated the docs from an external (npm-consumer) developer's perspective: the getting-started section had no hands-on scaffold-to-API tutorial that doesn't assume an AI agent or the monorepo, and the deployment section had no self-hosting guide (only Vercel as a concrete target). - getting-started/your-first-project: scaffold via create-objectstack, generated layout and package surface, dev server, REST calls, manual model edits, validate gate, artifact build - deployment/self-hosting: systemd, Docker (multi-stage), Docker Compose with Postgres, required secrets (OS_AUTH_SECRET/OS_SECRET_KEY), health and readiness probes, reverse-proxy wiring, multi-node notes - create-objectstack README: replace stale template list (minimal-api/full-stack/plugin) with the real registry (blank + remote todo/compliance/content/contracts/procurement) and document --skip-skills and the identity-rewrite behavior - nav meta.json + cross-links from section indexes Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Em3ky9uu6zw2cYzVhqCij2 --- content/docs/deployment/index.mdx | 1 + content/docs/deployment/meta.json | 1 + content/docs/deployment/self-hosting.mdx | 254 +++++++++++++++++ content/docs/getting-started/index.mdx | 1 + content/docs/getting-started/meta.json | 1 + .../getting-started/your-first-project.mdx | 259 ++++++++++++++++++ packages/create-objectstack/README.md | 97 ++++--- 7 files changed, 563 insertions(+), 51 deletions(-) create mode 100644 content/docs/deployment/self-hosting.mdx create mode 100644 content/docs/getting-started/your-first-project.mdx diff --git a/content/docs/deployment/index.mdx b/content/docs/deployment/index.mdx index 4435bf1ef1..add495d69b 100644 --- a/content/docs/deployment/index.mdx +++ b/content/docs/deployment/index.mdx @@ -167,6 +167,7 @@ managed KMS / Vault provider behind the same `ICryptoProvider` seam. ## Related +- [Self-Hosted Deployment](/docs/deployment/self-hosting) - [Single-Environment Mode](/docs/deployment/single-project-mode) - [Environment-Scoped Routing](/docs/api/environment-routing) - [Publish, Versioning & Preview](/docs/deployment/publish-and-preview) diff --git a/content/docs/deployment/meta.json b/content/docs/deployment/meta.json index 31e07869a7..c1d71d1422 100644 --- a/content/docs/deployment/meta.json +++ b/content/docs/deployment/meta.json @@ -3,6 +3,7 @@ "icon": "Cloud", "pages": [ "index", + "self-hosting", "vercel", "production-readiness", "publish-and-preview", diff --git a/content/docs/deployment/self-hosting.mdx b/content/docs/deployment/self-hosting.mdx new file mode 100644 index 0000000000..3b09e3f92d --- /dev/null +++ b/content/docs/deployment/self-hosting.mdx @@ -0,0 +1,254 @@ +--- +title: Self-Hosted Deployment +description: Run a compiled ObjectStack app on your own infrastructure — bare Node.js, systemd, Docker, and Docker Compose with Postgres, including health checks, reverse-proxy wiring, and the secrets you must pin. +--- + +# Self-Hosted Deployment + +This guide takes the artifact produced by `os build` / `os compile` and runs it +on infrastructure **you** operate: a Linux host, a Docker container, or a +compose stack with Postgres. It complements the platform-specific +[Vercel guide](/docs/deployment/vercel) and assumes you have read +[Deployment Modes](/docs/deployment). + +The deployment model is deliberately simple: + +``` +objectstack.config.ts ──(os build, CI)──▶ dist/objectstack.json ──(os start, server)──▶ running app +``` + +- The **artifact** (`dist/objectstack.json`) is a portable, self-describing + JSON file — your entire app. Build it once in CI; the host needs no + TypeScript and no build step. +- **`os start`** boots a production server directly from that artifact + ([reference](/docs/getting-started/cli#os-start)). +- **Deployment config stays outside the artifact.** Database URL, secrets, and + environment identity are injected via `OS_*` environment variables or flags. + +## The minimum viable production environment + +Four values every self-hosted deployment must pin — everything else has a +workable default: + +| Variable | Why it must be set | +|:---|:---| +| `OS_DATABASE_URL` | Without it, data lands in a SQLite file under the ObjectStack home directory (`~/.objectstack`, or `/.objectstack` next to a project config) — fine for one box, wrong for containers. Use `postgres://…`, `libsql://…`, or a mounted `file:…` path. | +| `OS_AUTH_SECRET` | Session secret for the auth plugin (`AUTH_SECRET` is the legacy alias). Without it, `/api/v1/auth/*` is **silently skipped** — the server runs unauthenticated. | +| `OS_SECRET_KEY` | 32-byte master key encrypting every stored secret (`openssl rand -hex 32`). On a container's ephemeral filesystem the auto-minted key is **lost on restart**, making previously-encrypted secrets undecryptable. | +| `OS_PORT` | `os start` **fails loudly** if the port is busy (it never auto-shifts like `os dev`). Pin it and keep your reverse-proxy upstream in sync. | + +Generate strong values once and store them in your secret manager: + +```bash +OS_AUTH_SECRET=$(openssl rand -hex 32) +OS_SECRET_KEY=$(openssl rand -hex 32) +``` + +The full catalog is in +[Environment Variables](/docs/deployment/environment-variables). + +## Option 1 — Bare Node.js (systemd) + +The simplest deployment: Node 18+ and the CLI on a Linux host. + +```bash +# On the host — no repo clone, just the CLI and your artifact +npm install -g @objectstack/cli +scp dist/objectstack.json server:/opt/my-app/objectstack.json +``` + +```ini title="/etc/systemd/system/my-app.service" +[Unit] +Description=My ObjectStack App +After=network.target postgresql.service + +[Service] +Type=simple +User=objectstack +WorkingDirectory=/opt/my-app +Environment=NODE_ENV=production +Environment=OS_ARTIFACT_PATH=/opt/my-app/objectstack.json +Environment=OS_PORT=8080 +EnvironmentFile=/opt/my-app/secrets.env # OS_DATABASE_URL, OS_AUTH_SECRET, OS_SECRET_KEY +ExecStart=/usr/bin/os start +Restart=on-failure + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo systemctl enable --now my-app +curl -fsS http://localhost:8080/api/v1/health +``` + +Upgrades are atomic: replace the artifact file and restart the service. Roll +back by restoring the previous artifact. + +## Option 2 — Docker + +The artifact model maps cleanly onto containers: the image contains Node, the +CLI, and one JSON file. + +```dockerfile title="Dockerfile" +# ── Build stage: compile TypeScript metadata to the artifact ───────── +FROM node:22-slim AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npx os build # → dist/objectstack.json + +# ── Runtime stage: CLI + artifact only ─────────────────────────────── +FROM node:22-slim +WORKDIR /srv/app +RUN npm install -g @objectstack/cli +COPY --from=build /app/dist/objectstack.json ./objectstack.json + +ENV NODE_ENV=production \ + OS_ARTIFACT_PATH=/srv/app/objectstack.json \ + OS_PORT=8080 +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=15s \ + CMD node -e "fetch('http://localhost:8080/api/v1/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))" + +CMD ["os", "start"] +``` + +```bash +docker build -t my-app . +docker run -p 8080:8080 \ + -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ + -e OS_AUTH_SECRET \ + -e OS_SECRET_KEY \ + my-app +``` + + +**Never bake `OS_AUTH_SECRET` / `OS_SECRET_KEY` into the image.** Pass them at +runtime from your orchestrator's secret store. And never rely on the +auto-minted dev crypto key inside a container — it lives on the ephemeral +filesystem and dies with it. + + +## Option 3 — Docker Compose with Postgres + +A complete single-host production stack: + +```yaml title="docker-compose.yml" +services: + app: + build: . + ports: + - "8080:8080" + environment: + OS_DATABASE_URL: postgres://objectstack:${POSTGRES_PASSWORD}@db:5432/myapp + OS_AUTH_SECRET: ${OS_AUTH_SECRET} + OS_SECRET_KEY: ${OS_SECRET_KEY} + depends_on: + db: + condition: service_healthy + restart: unless-stopped + + db: + image: postgres:17 + environment: + POSTGRES_USER: objectstack + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: myapp + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U objectstack -d myapp"] + interval: 5s + timeout: 3s + retries: 10 + restart: unless-stopped + +volumes: + pgdata: +``` + +```bash +# .env next to docker-compose.yml (never committed) +POSTGRES_PASSWORD=… +OS_AUTH_SECRET=… +OS_SECRET_KEY=… + +docker compose up -d +curl -fsS http://localhost:8080/api/v1/health +``` + +Prefer SQLite on a single small host? Skip the `db` service, mount a volume, +and point `OS_DATABASE_URL` at it: `file:/srv/data/app.db` (with +`- appdata:/srv/data` on the app service). See +[Drivers](/docs/data-modeling/drivers) for when to reach for which database. + +## Health checks & orchestration + +Every runtime exposes two probe endpoints — wire them into Docker +`HEALTHCHECK`, Kubernetes probes, or your load balancer: + +| Endpoint | Meaning | Use as | +|:---|:---|:---| +| `GET /api/v1/health` | Process is up and serving HTTP | Liveness probe | +| `GET /api/v1/ready` | Kernel booted, ready for traffic | Readiness probe | + +On Kubernetes, the same image works unchanged: mount the secrets as env vars, +point `readinessProbe` at `/api/v1/ready`, and scale — but read the multi-node +note below first. + +## Reverse proxy & TLS + +Terminate TLS in front of the app (Caddy, nginx, Traefik, or your cloud LB) +and keep three things in sync with the public origin: + +```bash +OS_AUTH_URL=https://app.example.com # auth callbacks / cookie origin +OS_TRUSTED_ORIGINS=https://app.example.com # CORS allow-list +OS_PORT=8080 # must match the proxy upstream +``` + +```caddyfile title="Caddyfile" +app.example.com { + reverse_proxy localhost:8080 +} +``` + +A drifted port or origin is the classic self-hosting failure: the app runs, +but logins bounce and browsers block API calls. Enable HSTS and tune security +headers only after TLS is confirmed — see +[Production Readiness](/docs/deployment/production-readiness). + +## Scaling beyond one node + +The default in-process coordination (locks, queues, schedules) is +single-node. Before running replicas, set `OS_CLUSTER_DRIVER` — the runtime +then treats the deployment as multi-node and **refuses to boot without an +explicit `OS_SECRET_KEY`** rather than minting per-node keys that can't +decrypt each other's secrets. All replicas must share the same +`OS_SECRET_KEY`, `OS_AUTH_SECRET`, and database. See +[Cluster](/docs/kernel/cluster). + +## Go-live + +Before pointing real users at the deployment, walk the +[Production Readiness](/docs/deployment/production-readiness) checklist — +security headers, rate limits, metrics, error reporting, backup/restore +drills, and data-retention windows. + + +**Your self-hosted app is AI-operable out of the box:** every deployment +serves an MCP server at `/api/v1/mcp` under the same permissions and RLS. +Disable with `OS_MCP_SERVER_ENABLED=false`. See +[Your app as an MCP server](/docs/api#your-app-as-an-mcp-server). + + +## Related + +- [Deployment Modes](/docs/deployment) — the map of local / standalone / Cloud +- [`os start` reference](/docs/getting-started/cli#os-start) — every flag and env var +- [Environment Variables](/docs/deployment/environment-variables) — the full catalog +- [Deploy to Vercel](/docs/deployment/vercel) — the serverless alternative +- [Troubleshooting & FAQ](/docs/deployment/troubleshooting) diff --git a/content/docs/getting-started/index.mdx b/content/docs/getting-started/index.mdx index da6e34d8f0..9d650cc352 100644 --- a/content/docs/getting-started/index.mdx +++ b/content/docs/getting-started/index.mdx @@ -197,4 +197,5 @@ For more troubleshooting, see the [Troubleshooting & FAQ](/docs/deployment/troub - [Architecture](/docs/concepts/architecture) — The three-layer protocol stack - [Glossary](/docs/getting-started/glossary) — Key terminology - [Build with Claude Code](/docs/getting-started/build-with-claude-code) — Build your first app end-to-end with an agent +- [Your First Project](/docs/getting-started/your-first-project) — The hands-on path: scaffold from npm, extend by hand, call the API - [Anatomy of an ObjectStack App](/docs/getting-started/quick-start) — Read the metadata an agent writes, so you can verify it diff --git a/content/docs/getting-started/meta.json b/content/docs/getting-started/meta.json index 4958782f6b..4306d0f4f6 100644 --- a/content/docs/getting-started/meta.json +++ b/content/docs/getting-started/meta.json @@ -6,6 +6,7 @@ "index", "how-ai-development-works", "build-with-claude-code", + "your-first-project", "quick-start", "examples", "cli", diff --git a/content/docs/getting-started/your-first-project.mdx b/content/docs/getting-started/your-first-project.mdx new file mode 100644 index 0000000000..cfd2d98b3a --- /dev/null +++ b/content/docs/getting-started/your-first-project.mdx @@ -0,0 +1,259 @@ +--- +title: Your First Project +description: Scaffold a standalone ObjectStack project with npm, understand what was generated, extend the data model, call the REST API, and build a deployable artifact — no monorepo checkout, no AI agent required. +--- + +# Your First Project + +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. + + +**Two ways to build.** ObjectStack is designed AI-first: normally +[an agent authors the metadata and you verify it](/docs/getting-started/build-with-claude-code). +This page walks the same ground *manually* so you understand every file the +agent would touch — useful for evaluating the platform, wiring CI, or working +without an agent. The two paths produce identical projects. + + +## Prerequisites + +| Tool | Minimum | Check | +|:---|:---|:---| +| **Node.js** | 18+ | `node --version` | +| A package manager | npm 9+ / pnpm 8+ / 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 + +```bash +npm create objectstack@latest my-app +cd my-app +``` + +The scaffolder (`create-objectstack`) does four things: + +1. **Copies a template** into `my-app/` and rewrites its identity — the npm + package name, the manifest `name`/`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 what + `os validate` later enforces. +2. **Installs dependencies** (pnpm if available, otherwise npm). +3. **Installs the AI skills bundle** (`npx skills add objectstack-ai/framework --all`) + so a coding agent is productive in the project from the first prompt. +4. **Writes `AGENTS.md`** (and `.github/copilot-instructions.md`) with the + project conventions — including the rule to run `npm run validate` after + every metadata change. + +Steps 3–4 cost nothing if you never use an agent; skip them with +`--skip-skills` if you prefer. + +### Templates + +| Template | Source | Description | +|:---|:---|:---| +| `blank` *(default)* | bundled, works offline | One object, REST API — a clean slate | +| `todo` | remote | Universal task & project management starter | +| `compliance` | remote | Compliance posture & evidence management (SOC2 / ISO27001) | +| `content` | remote | Content marketing pipeline — editorial calendar & channel ROI | +| `contracts` | remote | Post-signature CLM — approvals, obligations, renewals | +| `procurement` | remote | Source-to-pay — vendors, POs, receipts, invoice matching | + +```bash +npm create objectstack@latest my-app -- --template todo +``` + +Remote templates are fetched from the +[`objectstack-ai/templates`](https://github.com/objectstack-ai/templates) +repository at scaffold time and need network access; `blank` is bundled inside +the npm package and always works offline. + + +`os init` (from `@objectstack/cli`) is an alternative scaffolder with `app` / +`plugin` / `empty` templates — see the [CLI reference](/docs/getting-started/cli#os-init). +`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, version +├── package.json +├── tsconfig.json +├── AGENTS.md # conventions for coding agents +└── src/ + └── objects/ + ├── index.ts # barrel — re-exports every object + └── note.object.ts # a sample object +``` + +Two 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: + +```typescript title="objectstack.config.ts" +import { defineStack } from '@objectstack/spec'; +import * as objects from './src/objects/index.js'; + +export default defineStack({ + manifest: { + id: 'my-app', + namespace: 'my_app', + version: '0.1.0', + type: 'app', + name: 'My App', + }, + objects: Object.values(objects), + api: { + rest: { enabled: true, basePath: '/api' }, + }, +}); +``` + +**`src/objects/note.object.ts`** is a complete data model — schema, validation, +API surface, and searchability in one typed definition: + +```typescript title="src/objects/note.object.ts" +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.longText({ label: 'Body' }), + }, + + 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 | Role | +|:---|:---| +| `@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. | +| `@objectstack/driver-memory` | In-memory database driver for development. Swap for SQLite / Postgres / MongoDB in production — [no code changes](/docs/data-modeling/drivers). | +| `@objectstack/plugin-hono-server` | The HTTP server that mounts the generated REST API. | +| `@objectstack/cli` *(dev)* | The `os` / `objectstack` CLI: `dev`, `validate`, `build`, `start`. | + +## 3. Run it + +```bash +npm run dev # objectstack dev — hot reload +``` + +Or with the visual Console (data browser, metadata explorer, API docs): + +```bash +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. With +the dev server running: + +```bash +# Create a record +curl -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 "http://localhost:3000/api/v1/data/my_app_note?select=title&sort=-created_at&top=10" +``` + +See the [Data API](/docs/api/data-api) for the full CRUD, batch, and analytics +surface, and the [Client SDK](/docs/api/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. + +```typescript title="src/objects/note.object.ts" + fields: { + title: Field.text({ label: 'Title', required: true, searchable: true, maxLength: 200 }), + body: Field.longText({ 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/.object.ts` with +a `name` of `_`, then re-export it from +`src/objects/index.ts` (the barrel is what the config imports). + +After **every** metadata change, run the gate: + +```bash +npm run validate +``` + +`os validate` checks schema compliance, CEL predicate scoping, and widget +bindings — the classes of mistakes that otherwise fail *silently* at runtime. +See [Validating Metadata](/docs/getting-started/validating-metadata). + +## 6. Build the deployable artifact + +```bash +npm run build # objectstack build → dist/objectstack.json +``` + +The 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: + +```bash +npx os start --artifact ./dist/objectstack.json --port 8080 +``` + +Database 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. + +## Next steps + + + + + + + diff --git a/packages/create-objectstack/README.md b/packages/create-objectstack/README.md index 55db066257..11898bbda2 100644 --- a/packages/create-objectstack/README.md +++ b/packages/create-objectstack/README.md @@ -1,15 +1,19 @@ # create-objectstack -Scaffold a new [ObjectStack](https://objectstack.com) project in seconds. +Scaffold a new [ObjectStack](https://objectstack.ai) project in seconds. + +```bash +npm create objectstack@latest my-app +``` ## Usage ```bash -# Interactive — creates project in a new directory +# Create a project in a new directory (blank template) npx create-objectstack my-app # Use a specific template -npx create-objectstack my-app --template full-stack +npx create-objectstack my-app --template todo # Scaffold in the current directory npx create-objectstack @@ -20,80 +24,71 @@ npx create-objectstack my-app --skip-install ## Templates -| Template | Description | -| --- | --- | -| `minimal-api` *(default)* | Server + memory driver + 1 object + REST API | -| `full-stack` | Server + UI + auth + 3 CRM objects (Contact, Company, Deal) | -| `plugin` | Reusable plugin skeleton with test setup | +| Template | Source | Description | +| --- | --- | --- | +| `blank` *(default)* | bundled (offline) | Minimal starter — one object, REST API, ready to extend | +| `todo` | remote | Universal task & project management starter | +| `compliance` | remote | Compliance posture & evidence management (SOC2 / ISO27001) | +| `content` | remote | Content marketing pipeline — editorial calendar & channel ROI | +| `contracts` | remote | Post-signature CLM — approvals, obligations, renewals | +| `procurement` | remote | Source-to-pay — vendors, POs, receipts, invoice matching | + +Remote templates are fetched from +[`objectstack-ai/templates`](https://github.com/objectstack-ai/templates) at +scaffold time and require network access; `blank` is bundled and always works +offline. ## Options | Option | Description | | --- | --- | | `[name]` | Project name (defaults to current directory name) | -| `-t, --template ` | Project template (default: `minimal-api`) | +| `-t, --template ` | Project template (default: `blank`) | | `--skip-install` | Skip dependency installation | +| `--skip-skills` | Skip installing the ObjectStack AI skills bundle | | `-V, --version` | Show version | | `-h, --help` | Show help | -## What Gets Generated +## What it does + +1. Copies the template into the target directory and rewrites its identity: + `package.json` name, `objectstack.manifest.json` name/displayName, the + `objectstack.config.ts` manifest literals, and the README title. A + **namespace** is derived from the project name (`my-app` → `my_app`) and + every object name in the template is re-prefixed to match + (`blank_note` → `my_app_note`). +2. Installs dependencies (pnpm if available, otherwise npm). +3. Installs the ObjectStack AI skills bundle for coding agents + (`npx skills add objectstack-ai/framework --all`). +4. Writes `AGENTS.md` and `.github/copilot-instructions.md` with the project + conventions — unless the template ships its own. -### minimal-api +## What Gets Generated (blank) ``` my-app/ -├── objectstack.config.ts +├── objectstack.config.ts # defineStack() entry point +├── objectstack.manifest.json # name, namespace, version ├── package.json ├── tsconfig.json ├── .gitignore ├── README.md +├── AGENTS.md # conventions for coding agents └── src/ └── objects/ ├── index.ts - └── task.ts + └── note.object.ts ``` -### full-stack +Next steps inside the project: +```bash +npm run dev # start the dev server +npm run validate # verify metadata: schema + predicates + bindings ``` -my-app/ -├── objectstack.config.ts -├── package.json -├── tsconfig.json -├── .gitignore -├── README.md -└── src/ - ├── objects/ - │ ├── index.ts - │ ├── contact.ts - │ ├── company.ts - │ └── deal.ts - ├── views/ - │ ├── contact_list.ts - │ ├── company_list.ts - │ └── deal_list.ts - └── apps/ - ├── index.ts - └── crm.ts -``` - -### plugin -``` -my-plugin/ -├── objectstack.config.ts -├── package.json -├── tsconfig.json -├── .gitignore -├── README.md -├── src/ -│ ├── index.ts -│ └── objects/ -│ ├── index.ts -│ └── sample.ts -└── test/ - └── sample.test.ts -``` +See the docs: +[Your First Project](https://docs.objectstack.ai/docs/getting-started/your-first-project). ## License From 2a0eb7054696cb880e7b95786b8468afb43551b5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 06:26:15 +0000 Subject: [PATCH 2/5] docs: avoid reserved word 'role' in package table header (ADR-0090 D3) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Em3ky9uu6zw2cYzVhqCij2 --- content/docs/getting-started/your-first-project.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/getting-started/your-first-project.mdx b/content/docs/getting-started/your-first-project.mdx index cfd2d98b3a..10d8afe553 100644 --- a/content/docs/getting-started/your-first-project.mdx +++ b/content/docs/getting-started/your-first-project.mdx @@ -144,7 +144,7 @@ export const Note = ObjectSchema.create({ A standalone project uses a handful of published packages — this is the entire framework surface you install: -| Package | Role | +| 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. | From 254074ec7ca51d61109495e7898e583106890955 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 06:29:08 +0000 Subject: [PATCH 3/5] docs: use plain text fence for Caddyfile sample (shiki has no caddyfile grammar) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Em3ky9uu6zw2cYzVhqCij2 --- content/docs/deployment/self-hosting.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/deployment/self-hosting.mdx b/content/docs/deployment/self-hosting.mdx index 3b09e3f92d..e34f41f82f 100644 --- a/content/docs/deployment/self-hosting.mdx +++ b/content/docs/deployment/self-hosting.mdx @@ -210,7 +210,7 @@ OS_TRUSTED_ORIGINS=https://app.example.com # CORS allow-list OS_PORT=8080 # must match the proxy upstream ``` -```caddyfile title="Caddyfile" +```text title="Caddyfile" app.example.com { reverse_proxy localhost:8080 } From ce9dd789bbf249655ff581531742d5ea39d16f3b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 08:15:14 +0000 Subject: [PATCH 4/5] fix(create-objectstack,cli,docs): make the third-party onboarding path actually work on the current release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified the published npm onboarding flow end-to-end as an external user (npx create-objectstack@latest → install → validate → dev → REST → build → os start) and fixed everything that broke: - create-objectstack: the bundled blank template pinned ^6.0.0 while the registry publishes 14.x, so fresh projects installed a framework eight majors behind the docs. Template bumped to the current API (Field.longText → Field.textarea, drop the removed api.rest config key, declare sharingModel per ADR-0090) and the scaffolder now rewrites every @objectstack/* range in the generated package.json to its own release version, so generated projects track the registry even if the committed template drifts. Consistency tests ratchet the template major and the README template table against the TEMPLATES registry. - cli: os validate now runs the ADR-0090 D7 security posture check — previously a stack passed validate and then failed the identical gate in os build, breaking validate's documented same-gates contract. - docs: quick-start / build-with-claude-code / your-first-project examples updated to the current field API + explicit sharingModel; the tutorial's curl flow now includes the seeded dev-admin sign-in that the data API requires; cli.mdx names npm create objectstack as the recommended scaffolder with os init for plugin/bare-config cases. - deployment: new Backup & Restore guide (state inventory incl. the OS_SECRET_KEY escrow trap, per-driver recipes, restore drill); Kubernetes reference manifests in self-hosting; ready-to-copy Docker packaging in examples/docker linked from the guide. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Em3ky9uu6zw2cYzVhqCij2 --- .../create-objectstack-current-major.md | 5 + .../validate-security-posture-parity.md | 5 + content/docs/deployment/backup-restore.mdx | 126 ++++++++++++++++++ content/docs/deployment/meta.json | 1 + content/docs/deployment/self-hosting.mdx | 57 +++++++- .../build-with-claude-code.mdx | 3 +- content/docs/getting-started/cli.mdx | 9 +- content/docs/getting-started/quick-start.mdx | 8 +- .../getting-started/your-first-project.mdx | 26 ++-- examples/docker/.dockerignore | 6 + examples/docker/Dockerfile | 39 ++++++ examples/docker/README.md | 36 +++++ examples/docker/docker-compose.yml | 39 ++++++ packages/cli/src/commands/validate.ts | 37 ++++- packages/create-objectstack/package.json | 8 +- packages/create-objectstack/src/index.ts | 11 +- packages/create-objectstack/src/pkg-utils.ts | 27 ++++ .../src/template-consistency.test.ts | 87 ++++++++++++ .../src/templates/blank/README.md | 12 +- .../src/templates/blank/objectstack.config.ts | 6 - .../src/templates/blank/package.json | 12 +- .../blank/src/objects/note.object.ts | 6 +- packages/create-objectstack/vitest.config.ts | 8 ++ pnpm-lock.yaml | 3 + 24 files changed, 540 insertions(+), 37 deletions(-) create mode 100644 .changeset/create-objectstack-current-major.md create mode 100644 .changeset/validate-security-posture-parity.md create mode 100644 content/docs/deployment/backup-restore.mdx create mode 100644 examples/docker/.dockerignore create mode 100644 examples/docker/Dockerfile create mode 100644 examples/docker/README.md create mode 100644 examples/docker/docker-compose.yml create mode 100644 packages/create-objectstack/src/pkg-utils.ts create mode 100644 packages/create-objectstack/src/template-consistency.test.ts create mode 100644 packages/create-objectstack/vitest.config.ts diff --git a/.changeset/create-objectstack-current-major.md b/.changeset/create-objectstack-current-major.md new file mode 100644 index 0000000000..ad8a289619 --- /dev/null +++ b/.changeset/create-objectstack-current-major.md @@ -0,0 +1,5 @@ +--- +"create-objectstack": patch +--- + +Scaffolded projects now install the current framework release instead of a stale major. The bundled `blank` template had `^6.0.0` ranges frozen in while the registry was publishing 14.x, so `npm create objectstack` produced a project eight majors behind the docs — and the template's code no longer compiled against 14.x anyway (`Field.longText` removed, `api.rest` no longer a `defineStack` key, `sharingModel` now required by the ADR-0090 security gate). The template is updated to the current API, and the scaffolder now rewrites every `@objectstack/*` range in the generated `package.json` to `^` (all packages version in lockstep), so generated projects track the release even if the committed template drifts again. A consistency test ratchets the template's major and the README's template table against the registry. The template README also documents the seeded dev-admin sign-in that data-API curls need. diff --git a/.changeset/validate-security-posture-parity.md b/.changeset/validate-security-posture-parity.md new file mode 100644 index 0000000000..6284e44a3a --- /dev/null +++ b/.changeset/validate-security-posture-parity.md @@ -0,0 +1,5 @@ +--- +"@objectstack/cli": patch +--- + +`os validate` now runs the ADR-0090 D7 security posture check, restoring its documented contract of being the artifact-free run of the same gates as `os compile`/`os build`. Previously a stack could pass `validate` and then fail the build — e.g. a custom object with no explicit `sharingModel` (OWD), which the posture linter rejects at compile. Error findings gate validation; advisory findings print as warnings (and join the `--json` warnings array). diff --git a/content/docs/deployment/backup-restore.mdx b/content/docs/deployment/backup-restore.mdx new file mode 100644 index 0000000000..8f0097bc62 --- /dev/null +++ b/content/docs/deployment/backup-restore.mdx @@ -0,0 +1,126 @@ +--- +title: Backup & Restore +description: What state an ObjectStack deployment actually holds, how to back up each piece per database driver, and the restore drill to rehearse before you need it. +--- + +# Backup & Restore + +The [go-live checklist](/docs/deployment/production-readiness#go-live-checklist) +requires a documented, tested backup/restore drill. This page is that drill: +what to back up, how, and how to prove the restore works. + +## What state a deployment holds + +An ObjectStack app is deliberately easy to back up because almost everything +lives in one of four places: + +| State | Where it lives | Backup strategy | +|:---|:---|:---| +| **Business data** (records, users, orgs, runtime-authored metadata, encrypted `sys_secret` values) | The database behind `OS_DATABASE_URL` | Database-native backup — see below | +| **Authored metadata** (objects, views, flows, …) | Your project source + the compiled artifact | Git. The artifact is rebuildable with `os build`; no runtime backup needed | +| **Secrets** (`OS_SECRET_KEY`, `OS_AUTH_SECRET`, DB credentials) | Your secret manager | Escrow the values themselves — see the warning below | +| **The ObjectStack home dir** (`~/.objectstack` or `/.objectstack`: default SQLite DB under `data/`, uploaded files under `data/uploads/`, dev-minted keys) | Local filesystem | Include in file backups on single-host deployments; empty on stateless containers that set `OS_DATABASE_URL` + `OS_SECRET_KEY` explicitly | + + +**A database backup without `OS_SECRET_KEY` is an incomplete backup.** Every +`sys_secret` value (encrypted settings, `secret` fields, datasource +credentials) is encrypted under that one key. Restoring the database on a new +host without the original key leaves those values permanently undecryptable. +Escrow the key in your secret manager the day you generate it, and treat +key + database as one backup unit. + + +## Backing up the database + +Use the native tooling of whatever `OS_DATABASE_URL` points at — ObjectStack +adds no extra backup surface on top of the database. + +### PostgreSQL + +```bash +# Logical backup (small/medium databases; restore with pg_restore) +pg_dump --format=custom "$OS_DATABASE_URL" > backup-$(date +%F).dump + +# Larger deployments: continuous archiving / point-in-time recovery +# (WAL archiving, or your provider's managed snapshots) +``` + +### SQLite (single host) + +The database is a single file — but never copy it while the server is +writing. Use SQLite's online backup instead: + +```bash +sqlite3 /srv/data/app.db ".backup '/backups/app-$(date +%F).db'" +``` + +The default location when `OS_DATABASE_URL` is unset is +`/data/objectstack.db` under the ObjectStack home directory. + +### Turso / libSQL and managed databases + +Use the provider's snapshot, branching, or point-in-time-restore facilities. +Nothing ObjectStack-specific applies. + +## Backing up uploaded files + +The local storage adapter keeps uploads under `OS_STORAGE_ROOT` (default +`./.objectstack/data/uploads`). On single-host deployments, include that +directory in the same schedule as the database so records and their +attachments restore to the same point in time. Deployments using an external +storage service (S3-compatible, etc.) inherit that service's durability and +versioning instead. + +## The restore drill + +Rehearse this on a scratch host **before** go-live, and again after any +storage change. A backup you haven't restored is a hope, not a backup. + + + +### Provision a clean host + +Fresh container or VM. Install Node 18+ and `@objectstack/cli`. + +### Restore the database + +```bash +pg_restore --clean --if-exists -d "$OS_DATABASE_URL" backup-2026-07-14.dump +# or copy the SQLite file / restore the provider snapshot +``` + +### Provide the original secrets + +Set `OS_SECRET_KEY` and `OS_AUTH_SECRET` to the **escrowed originals** — not +freshly generated values. Restore `OS_STORAGE_ROOT` contents if you use local +file storage. + +### Boot from the artifact and verify + +```bash +OS_ARTIFACT_PATH=./objectstack.json OS_PORT=8080 os start +curl -fsS http://localhost:8080/api/v1/health +curl -fsS http://localhost:8080/api/v1/ready +``` + +Then prove the restore end-to-end: sign in with a real account, open a record +that existed before the backup, and read a value that lives in an encrypted +setting (which exercises `OS_SECRET_KEY`). + + + +## Retention is not backup + +The platform's `lifecycle` declarations bound telemetry data (activity 14d, +job runs 30d, notifications 90d, audit 90d-hot) — deleted-by-policy data is +gone from backups taken afterwards. If compliance needs longer, extend +`lifecycle.retention_overrides` **before** go-live, and register an `archive` +datasource if audit data must move to cold storage instead of being retained +hot. See [Production Readiness](/docs/deployment/production-readiness). + +## Related + +- [Self-Hosted Deployment](/docs/deployment/self-hosting) +- [Production Readiness](/docs/deployment/production-readiness) +- [Environment Variables](/docs/deployment/environment-variables) +- [Drivers](/docs/data-modeling/drivers) diff --git a/content/docs/deployment/meta.json b/content/docs/deployment/meta.json index c1d71d1422..bc2345ac15 100644 --- a/content/docs/deployment/meta.json +++ b/content/docs/deployment/meta.json @@ -4,6 +4,7 @@ "pages": [ "index", "self-hosting", + "backup-restore", "vercel", "production-readiness", "publish-and-preview", diff --git a/content/docs/deployment/self-hosting.mdx b/content/docs/deployment/self-hosting.mdx index e34f41f82f..c1e83f60fd 100644 --- a/content/docs/deployment/self-hosting.mdx +++ b/content/docs/deployment/self-hosting.mdx @@ -88,7 +88,10 @@ back by restoring the previous artifact. ## Option 2 — Docker The artifact model maps cleanly onto containers: the image contains Node, the -CLI, and one JSON file. +CLI, and one JSON file. The Dockerfile below (plus the compose stack in the +next section and a `.dockerignore`) also ships ready-to-copy in +[`examples/docker`](https://github.com/objectstack-ai/framework/tree/main/examples/docker) +— drop the files into your scaffolded project. ```dockerfile title="Dockerfile" # ── Build stage: compile TypeScript metadata to the artifact ───────── @@ -195,9 +198,54 @@ Every runtime exposes two probe endpoints — wire them into Docker | `GET /api/v1/health` | Process is up and serving HTTP | Liveness probe | | `GET /api/v1/ready` | Kernel booted, ready for traffic | Readiness probe | -On Kubernetes, the same image works unchanged: mount the secrets as env vars, -point `readinessProbe` at `/api/v1/ready`, and scale — but read the multi-node -note below first. +### Kubernetes + +The same image works unchanged. A minimal reference Deployment — secrets from +a `Secret`, probes on the two endpoints above: + +```yaml title="deployment.yaml" +apiVersion: apps/v1 +kind: Deployment +metadata: + name: my-app +spec: + replicas: 1 # >1 requires OS_CLUSTER_DRIVER — see below + selector: + matchLabels: { app: my-app } + template: + metadata: + labels: { app: my-app } + spec: + containers: + - name: app + image: registry.example.com/my-app:latest + ports: + - containerPort: 8080 + envFrom: + - secretRef: + name: my-app-secrets # OS_DATABASE_URL, OS_AUTH_SECRET, OS_SECRET_KEY + livenessProbe: + httpGet: { path: /api/v1/health, port: 8080 } + periodSeconds: 30 + readinessProbe: + httpGet: { path: /api/v1/ready, port: 8080 } + initialDelaySeconds: 5 + periodSeconds: 10 +--- +apiVersion: v1 +kind: Service +metadata: + name: my-app +spec: + selector: { app: my-app } + ports: + - port: 80 + targetPort: 8080 +``` + +`/api/v1/ready` returns 503 while the kernel is booting *and* during graceful +shutdown, so rolling restarts drain cleanly. Before setting `replicas > 1`, +read the multi-node note below. ## Reverse proxy & TLS @@ -247,6 +295,7 @@ Disable with `OS_MCP_SERVER_ENABLED=false`. See ## Related +- [Backup & Restore](/docs/deployment/backup-restore) — what to back up, and the restore drill - [Deployment Modes](/docs/deployment) — the map of local / standalone / Cloud - [`os start` reference](/docs/getting-started/cli#os-start) — every flag and env var - [Environment Variables](/docs/deployment/environment-variables) — the full catalog diff --git a/content/docs/getting-started/build-with-claude-code.mdx b/content/docs/getting-started/build-with-claude-code.mdx index 1af7dcac0a..5c2bf5368f 100644 --- a/content/docs/getting-started/build-with-claude-code.mdx +++ b/content/docs/getting-started/build-with-claude-code.mdx @@ -114,7 +114,7 @@ export const Ticket = ObjectSchema.create({ fields: { subject: Field.text({ label: 'Subject', required: true, searchable: true, maxLength: 200 }), - description: Field.longText({ label: 'Description' }), + description: Field.textarea({ label: 'Description' }), priority: Field.select({ label: 'Priority', @@ -139,6 +139,7 @@ export const Ticket = ObjectSchema.create({ }), }, + sharingModel: 'private', // org-wide default (OWD) — required by the security gate enable: { apiEnabled: true, searchable: true }, }); ``` diff --git a/content/docs/getting-started/cli.mdx b/content/docs/getting-started/cli.mdx index cdbae50955..0731d1c7de 100644 --- a/content/docs/getting-started/cli.mdx +++ b/content/docs/getting-started/cli.mdx @@ -22,11 +22,11 @@ The CLI is available as `objectstack` or the shorter alias `os`. Installed as a ### Create a project ```bash -npx @objectstack/cli init my-app +npm create objectstack@latest my-app cd my-app ``` -This scaffolds a working project with `objectstack.config.ts`, a sample object, and all dependencies installed. +This scaffolds a working project with `objectstack.config.ts`, a sample object, and all dependencies installed — plus the AI skills bundle and an `AGENTS.md` for coding agents. (`os init` is the CLI's own scaffolder for plugin skeletons and bare configs — see [below](#os-init).) ### Add more metadata @@ -80,6 +80,11 @@ predicate/schema/binding mistakes that fail silently at runtime), and `os dev -- Scaffolds a new ObjectStack project with configuration, TypeScript setup, and initial metadata files. +> **Which scaffolder?** For a new app, prefer **`npm create objectstack@latest`** — it +> also derives your namespace, pins the framework packages to the current release, and +> installs the AI skills bundle + `AGENTS.md`. Reach for `os init` when you want a +> **plugin** skeleton or a **bare config** in an existing directory. + ```bash os init my-app # Create with default "app" template os init my-plugin -t plugin # Create a plugin project diff --git a/content/docs/getting-started/quick-start.mdx b/content/docs/getting-started/quick-start.mdx index 71ba54326c..84324a545f 100644 --- a/content/docs/getting-started/quick-start.mdx +++ b/content/docs/getting-started/quick-start.mdx @@ -61,7 +61,7 @@ export const Ticket = ObjectSchema.create({ fields: { subject: Field.text({ label: 'Subject', required: true, searchable: true, maxLength: 200 }), - description: Field.longText({ label: 'Description' }), + description: Field.textarea({ label: 'Description' }), status: Field.select({ label: 'Status', @@ -75,6 +75,7 @@ export const Ticket = ObjectSchema.create({ }), }, + sharingModel: 'private', // org-wide default (OWD) — required by the security gate enable: { apiEnabled: true, searchable: true }, }); ``` @@ -83,11 +84,14 @@ export const Ticket = ObjectSchema.create({ - **Names** are `snake_case` and namespace-prefixed (`support_desk_ticket`) — the scaffolder derives the prefix from your project name. -- **Field types** are `Field.*` builders (`text`, `longText`, `select`, `date`, +- **Field types** are `Field.*` builders (`text`, `textarea`, `select`, `date`, `lookup`, …), not raw strings. A `select`'s `options` carry the exact labels, values, and colors you'll see in the UI — verify these match your intent. - **`required`, `default`, `searchable`** encode behavior the UI and API inherit automatically. +- **`sharingModel`** is the org-wide default for records the user doesn't own — + an explicit, authored security decision (`private` unless you have a reason + otherwise). The validation gate rejects custom objects that omit it. This one definition powers the REST API, the Console UI, **and the MCP tools diff --git a/content/docs/getting-started/your-first-project.mdx b/content/docs/getting-started/your-first-project.mdx index 10d8afe553..9acac917e2 100644 --- a/content/docs/getting-started/your-first-project.mdx +++ b/content/docs/getting-started/your-first-project.mdx @@ -112,12 +112,12 @@ export default defineStack({ name: 'My App', }, objects: Object.values(objects), - api: { - rest: { enabled: true, basePath: '/api' }, - }, }); ``` +The REST API needs no configuration — it is on by default for every object +with `apiEnabled`. + **`src/objects/note.object.ts`** is a complete data model — schema, validation, API surface, and searchability in one typed definition: @@ -132,9 +132,10 @@ export const Note = ObjectSchema.create({ fields: { title: Field.text({ label: 'Title', required: true, searchable: true, maxLength: 200 }), - body: Field.longText({ label: 'Body' }), + body: Field.textarea({ label: 'Body' }), }, + sharingModel: 'private', // org-wide default — an explicit, authored decision enable: { apiEnabled: true, searchable: true }, }); ``` @@ -166,17 +167,24 @@ 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. With -the dev server running: +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`): ```bash +# 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 -X POST http://localhost:3000/api/v1/data/my_app_note \ +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 "http://localhost:3000/api/v1/data/my_app_note?select=title&sort=-created_at&top=10" +curl -b cookies.txt "http://localhost:3000/api/v1/data/my_app_note?select=title&sort=-created_at&top=10" ``` See the [Data API](/docs/api/data-api) for the full CRUD, batch, and analytics @@ -190,7 +198,7 @@ Add a field and a second object by hand — the same edit an agent would make. ```typescript title="src/objects/note.object.ts" fields: { title: Field.text({ label: 'Title', required: true, searchable: true, maxLength: 200 }), - body: Field.longText({ label: 'Body' }), + body: Field.textarea({ label: 'Body' }), status: Field.select({ label: 'Status', options: [ diff --git a/examples/docker/.dockerignore b/examples/docker/.dockerignore new file mode 100644 index 0000000000..0880b59cb0 --- /dev/null +++ b/examples/docker/.dockerignore @@ -0,0 +1,6 @@ +node_modules +dist +.git +*.log +.env +cookies.txt diff --git a/examples/docker/Dockerfile b/examples/docker/Dockerfile new file mode 100644 index 0000000000..8dc6d44ca8 --- /dev/null +++ b/examples/docker/Dockerfile @@ -0,0 +1,39 @@ +# Reference Dockerfile for a standalone ObjectStack app. +# +# Copy this file (plus .dockerignore and docker-compose.yml) into the root of +# a project scaffolded with `npm create objectstack@latest`, then: +# +# docker build -t my-app . +# docker run -p 8080:8080 \ +# -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ +# -e OS_AUTH_SECRET -e OS_SECRET_KEY \ +# my-app +# +# Docs: https://docs.objectstack.ai/docs/deployment/self-hosting + +# ── Build stage: compile TypeScript metadata to the artifact ───────── +FROM node:22-slim AS build +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npx os build # → dist/objectstack.json + +# ── Runtime stage: CLI + artifact only ─────────────────────────────── +FROM node:22-slim +WORKDIR /srv/app +RUN npm install -g @objectstack/cli +COPY --from=build /app/dist/objectstack.json ./objectstack.json + +ENV NODE_ENV=production \ + OS_ARTIFACT_PATH=/srv/app/objectstack.json \ + OS_PORT=8080 +EXPOSE 8080 + +# Liveness: /api/v1/health. For orchestrated readiness use /api/v1/ready. +HEALTHCHECK --interval=30s --timeout=3s --start-period=15s \ + CMD node -e "fetch('http://localhost:8080/api/v1/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))" + +# OS_DATABASE_URL, OS_AUTH_SECRET, and OS_SECRET_KEY are injected at runtime — +# never bake them into the image. +CMD ["os", "start"] diff --git a/examples/docker/README.md b/examples/docker/README.md new file mode 100644 index 0000000000..465e85d371 --- /dev/null +++ b/examples/docker/README.md @@ -0,0 +1,36 @@ +# Docker Reference for Standalone ObjectStack Apps + +Reference container packaging for a project scaffolded with +`npm create objectstack@latest`. These files are **meant to be copied into +your app**, not built from this directory — the example apps in this repo use +`workspace:*` dependencies and are not standalone-buildable. + +```bash +npm create objectstack@latest my-app +cd my-app +cp /examples/docker/{Dockerfile,docker-compose.yml,.dockerignore} . + +# Image only +docker build -t my-app . + +# Or the full app + Postgres stack +cat > .env <); + const securityErrors = securityFindings.filter((f) => f.severity === 'error'); + const securityAdvisories = securityFindings.filter((f) => f.severity !== 'error'); + if (securityErrors.length > 0) { + if (flags.json) { + console.log(JSON.stringify({ + valid: false, + errors: securityErrors, + duration: timer.elapsed(), + }, null, 2)); + this.exit(1); + } + console.log(''); + printError(`Security posture check failed (${securityErrors.length} issue${securityErrors.length > 1 ? 's' : ''})`); + for (const f of securityErrors.slice(0, 50)) { + console.log(` • ${f.where}: ${f.message}`); + console.log(chalk.dim(` ${f.hint}`)); + console.log(chalk.dim(` rule: ${f.rule} at ${f.path}`)); + } + this.exit(1); + } + if (!flags.json) { + for (const f of securityAdvisories.slice(0, 50)) { + console.log(chalk.yellow(` ⚠ ${f.where}: ${f.message}`)); + console.log(chalk.dim(` ${f.hint}`)); + } + } + // 4. Collect and display stats const stats = collectMetadataStats(config); @@ -340,7 +375,7 @@ export default class Validate extends Command { valid: true, manifest: config.manifest, stats, - warnings: [...exprWarnings, ...widgetWarnings, ...styleWarnings, ...jsxWarnings, ...capWarnings], + warnings: [...exprWarnings, ...widgetWarnings, ...styleWarnings, ...jsxWarnings, ...capWarnings, ...securityAdvisories], duration: timer.elapsed(), }, null, 2)); return; diff --git a/packages/create-objectstack/package.json b/packages/create-objectstack/package.json index 1357c22f92..c1c37577de 100644 --- a/packages/create-objectstack/package.json +++ b/packages/create-objectstack/package.json @@ -1,13 +1,14 @@ { "name": "create-objectstack", "version": "14.7.0", - "description": "Create a new ObjectStack project — npx create-objectstack", + "description": "Create a new ObjectStack project \u2014 npx create-objectstack", "bin": { "create-objectstack": "./bin/create-objectstack.js" }, "scripts": { "build": "tsup", - "dev": "tsup --watch" + "dev": "tsup --watch", + "test": "vitest run" }, "keywords": [ "objectstack", @@ -27,7 +28,8 @@ "@types/node": "^26.1.1", "@types/tar": "^7.0.87", "tsup": "^8.5.1", - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "vitest": "^4.1.10" }, "repository": { "type": "git", diff --git a/packages/create-objectstack/src/index.ts b/packages/create-objectstack/src/index.ts index e94a90c6e1..53989302bf 100644 --- a/packages/create-objectstack/src/index.ts +++ b/packages/create-objectstack/src/index.ts @@ -41,6 +41,8 @@ import { mkdtemp, rm } from 'node:fs/promises'; // eslint-disable-next-line import/no-unresolved import * as tar from 'tar'; +import { syncObjectStackDeps } from './pkg-utils.js'; + const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const BUNDLED_TEMPLATES_DIR = path.resolve(__dirname, 'templates'); @@ -276,12 +278,19 @@ function rewriteProjectIdentity( } } - // package.json — set .name + // package.json — set .name and pin @objectstack/* deps to this scaffolder's + // own release line. All @objectstack packages (including create-objectstack) + // version in lockstep, so `^` always resolves and always matches + // the framework the docs describe. Without this, a template whose literal + // ranges have gone stale scaffolds a project several majors behind the + // published framework (the `^6.0.0`-era templates installed 6.x while the + // registry was at 14.x). const pkgPath = path.join(targetDir, 'package.json'); if (fs.existsSync(pkgPath)) { try { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); pkg.name = projectName; + syncObjectStackDeps(pkg, readCliVersion()); fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); } catch { // leave the file alone if it isn't valid JSON diff --git a/packages/create-objectstack/src/pkg-utils.ts b/packages/create-objectstack/src/pkg-utils.ts new file mode 100644 index 0000000000..f6f1959ebe --- /dev/null +++ b/packages/create-objectstack/src/pkg-utils.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. +// +// Pure helpers for rewriting a scaffolded project's package.json. Kept out of +// index.ts because that module calls `program.parse()` on import — anything a +// test needs must be importable without running the CLI. + +/** + * Rewrite every `@objectstack/*` range in dependencies/devDependencies to + * `^`. All @objectstack packages version in lockstep, so the + * scaffolder's own version always resolves and always matches the framework + * the docs describe. No-op when the version is unknown (`0.0.0` fallback) so + * a broken version read never corrupts the template's committed baseline. + */ +export function syncObjectStackDeps( + pkg: { dependencies?: Record; devDependencies?: Record }, + version: string, +): void { + if (!/^\d+\.\d+\.\d+/.test(version) || version.startsWith('0.')) return; + for (const deps of [pkg.dependencies, pkg.devDependencies]) { + if (!deps) continue; + for (const dep of Object.keys(deps)) { + if (dep.startsWith('@objectstack/')) { + deps[dep] = `^${version}`; + } + } + } +} diff --git a/packages/create-objectstack/src/template-consistency.test.ts b/packages/create-objectstack/src/template-consistency.test.ts new file mode 100644 index 0000000000..5be0bb520a --- /dev/null +++ b/packages/create-objectstack/src/template-consistency.test.ts @@ -0,0 +1,87 @@ +// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license. +// +// Drift ratchets for the scaffolder's user-facing surfaces. Both of these +// rotted silently once before (#2899 follow-up): the bundled template pinned +// `^6.0.0` while the registry was publishing 14.x, and the README advertised +// a template set (`minimal-api`/`full-stack`/`plugin`) that never shipped. + +import { describe, it, expect } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { syncObjectStackDeps } from './pkg-utils.js'; + +const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const ownPkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8')); +const ownMajor = Number(ownPkg.version.split('.')[0]); + +// The TEMPLATES registry, read from src/index.ts as text — importing the +// module would run the CLI (it calls program.parse() on import). Parse only +// the lines between the TEMPLATES declaration and its closing brace. +const REGISTRY_SOURCE = fs.readFileSync(path.join(pkgRoot, 'src', 'index.ts'), 'utf8'); +const registryBlock = + /const TEMPLATES: Record = \{([\s\S]*?)\n\};/.exec( + REGISTRY_SOURCE, + )?.[1] ?? ''; +const registryTemplates = [...registryBlock.matchAll(/^ ([a-z][a-z0-9_]*): \{$/gm)].map( + (m) => m[1], +); + +describe('blank template package.json', () => { + const templatePkg = JSON.parse( + fs.readFileSync( + path.join(pkgRoot, 'src', 'templates', 'blank', 'package.json'), + 'utf8', + ), + ); + + it('pins every @objectstack/* dep to the current major', () => { + const allDeps = { ...templatePkg.dependencies, ...templatePkg.devDependencies }; + const stackDeps = Object.entries(allDeps).filter(([name]) => + name.startsWith('@objectstack/'), + ); + expect(stackDeps.length).toBeGreaterThan(0); + for (const [name, range] of stackDeps) { + const match = /^\^(\d+)\./.exec(String(range)); + expect(match, `${name} range "${range}" must be ^.x`).not.toBeNull(); + expect( + Number(match![1]), + `${name} pins ^${match![1]}.x but create-objectstack is v${ownMajor} — ` + + 'bump the template with the release (scaffold-time sync only fixes ' + + 'generated projects, not this committed baseline)', + ).toBe(ownMajor); + } + }); +}); + +describe('README template table', () => { + it('lists exactly the templates in the TEMPLATES registry', () => { + const readme = fs.readFileSync(path.join(pkgRoot, 'README.md'), 'utf8'); + // Table rows under "## Templates": | `name` ... | source | description | + const section = readme.split(/^## Templates$/m)[1]?.split(/^## /m)[0] ?? ''; + const documented = [...section.matchAll(/^\| `([a-z][a-z0-9_-]*)`/gm)].map( + (m) => m[1], + ); + expect(documented.sort()).toEqual([...registryTemplates].sort()); + }); +}); + +describe('syncObjectStackDeps', () => { + it('rewrites @objectstack/* ranges in deps and devDeps', () => { + const pkg = { + dependencies: { '@objectstack/spec': '^6.0.0', chalk: '^5.0.0' }, + devDependencies: { '@objectstack/cli': '^6.0.0', typescript: '^6.0.0' }, + }; + syncObjectStackDeps(pkg, '14.7.0'); + expect(pkg.dependencies['@objectstack/spec']).toBe('^14.7.0'); + expect(pkg.devDependencies['@objectstack/cli']).toBe('^14.7.0'); + expect(pkg.dependencies.chalk).toBe('^5.0.0'); + expect(pkg.devDependencies.typescript).toBe('^6.0.0'); + }); + + it('is a no-op on the 0.0.0 fallback version', () => { + const pkg = { dependencies: { '@objectstack/spec': '^6.0.0' } }; + syncObjectStackDeps(pkg, '0.0.0'); + expect(pkg.dependencies['@objectstack/spec']).toBe('^6.0.0'); + }); +}); diff --git a/packages/create-objectstack/src/templates/blank/README.md b/packages/create-objectstack/src/templates/blank/README.md index 919c08fa33..1b0c5eaeeb 100644 --- a/packages/create-objectstack/src/templates/blank/README.md +++ b/packages/create-objectstack/src/templates/blank/README.md @@ -9,7 +9,17 @@ pnpm install pnpm dev ``` -The REST API is served at `http://localhost:3000/api`. +The REST API is served at `http://localhost:3000/api/v1`. Data endpoints +require a session — the dev server seeds a login-ready admin +(`admin@objectos.ai` / `admin123`) on an empty database: + +```bash +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"}' + +curl -b cookies.txt "http://localhost:3000/api/v1/data/" +``` ## Layout diff --git a/packages/create-objectstack/src/templates/blank/objectstack.config.ts b/packages/create-objectstack/src/templates/blank/objectstack.config.ts index d4fd0da76f..48abaddd16 100644 --- a/packages/create-objectstack/src/templates/blank/objectstack.config.ts +++ b/packages/create-objectstack/src/templates/blank/objectstack.config.ts @@ -11,10 +11,4 @@ export default defineStack({ description: 'Minimal ObjectStack environment — a clean slate for building.', }, objects: Object.values(objects), - api: { - rest: { - enabled: true, - basePath: '/api', - }, - }, }); diff --git a/packages/create-objectstack/src/templates/blank/package.json b/packages/create-objectstack/src/templates/blank/package.json index 13db4acb6b..07be83fd06 100644 --- a/packages/create-objectstack/src/templates/blank/package.json +++ b/packages/create-objectstack/src/templates/blank/package.json @@ -11,13 +11,13 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@objectstack/spec": "^6.0.0", - "@objectstack/runtime": "^6.0.0", - "@objectstack/driver-memory": "^6.0.0", - "@objectstack/plugin-hono-server": "^6.0.0" + "@objectstack/spec": "^14.0.0", + "@objectstack/runtime": "^14.0.0", + "@objectstack/driver-memory": "^14.0.0", + "@objectstack/plugin-hono-server": "^14.0.0" }, "devDependencies": { - "@objectstack/cli": "^6.0.0", - "typescript": "^5.3.0" + "@objectstack/cli": "^14.0.0", + "typescript": "^6.0.0" } } diff --git a/packages/create-objectstack/src/templates/blank/src/objects/note.object.ts b/packages/create-objectstack/src/templates/blank/src/objects/note.object.ts index c018dc9669..9197ac8470 100644 --- a/packages/create-objectstack/src/templates/blank/src/objects/note.object.ts +++ b/packages/create-objectstack/src/templates/blank/src/objects/note.object.ts @@ -16,11 +16,15 @@ export const Note = ObjectSchema.create({ searchable: true, maxLength: 200, }), - body: Field.longText({ + body: Field.textarea({ label: 'Body', }), }, + // Org-wide default (OWD): who can see records they don't own. The security + // posture gate (ADR-0090) requires an explicit, authored decision here. + sharingModel: 'private', + enable: { apiEnabled: true, searchable: true, diff --git a/packages/create-objectstack/vitest.config.ts b/packages/create-objectstack/vitest.config.ts new file mode 100644 index 0000000000..bd4f0bf346 --- /dev/null +++ b/packages/create-objectstack/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['test/**/*.test.ts', 'src/**/*.test.ts'], + environment: 'node', + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fa464c69d..366f61d18c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -733,6 +733,9 @@ importers: typescript: specifier: ^6.0.3 version: 6.0.3 + vitest: + specifier: ^4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.2)(msw@2.14.6(@types/node@26.1.1)(typescript@6.0.3))(vite@8.0.16(@types/node@26.1.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) packages/dogfood: dependencies: From 3e8bffadfaeabef61f0c9e6d31fd99542e11fabe Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 08:20:30 +0000 Subject: [PATCH 5/5] docs: list the security-posture gate in the validate/build parity table Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Em3ky9uu6zw2cYzVhqCij2 --- content/docs/getting-started/validating-metadata.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/content/docs/getting-started/validating-metadata.mdx b/content/docs/getting-started/validating-metadata.mdx index 2dfe28513f..6c87b369d8 100644 --- a/content/docs/getting-started/validating-metadata.mdx +++ b/content/docs/getting-started/validating-metadata.mdx @@ -57,6 +57,7 @@ dangling one. | Protocol schema (Zod) | ✓ | ✓ | | CEL / predicate validation | ✓ | ✓ | | Widget-binding integrity | ✓ | ✓ | +| Security posture (ADR-0090 — e.g. every custom object declares `sharingModel`) | ✓ | ✓ | | Emits `dist/objectstack.json` | — | ✓ | So `os validate` is the fast inner-loop check (no artifact); `os build` is what