Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/create-objectstack-current-major.md
Original file line number Diff line number Diff line change
@@ -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 `^<its own version>` (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.
5 changes: 5 additions & 0 deletions .changeset/validate-security-posture-parity.md
Original file line number Diff line number Diff line change
@@ -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).
126 changes: 126 additions & 0 deletions content/docs/deployment/backup-restore.mdx
Original file line number Diff line number Diff line change
@@ -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 `<cwd>/.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 |

<Callout type="warn">
**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.
</Callout>

## 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
`<home>/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.

<Steps>

### 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`).

</Steps>

## 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)
1 change: 1 addition & 0 deletions content/docs/deployment/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"pages": [
"index",
"self-hosting",
"backup-restore",
"vercel",
"production-readiness",
"publish-and-preview",
Expand Down
57 changes: 53 additions & 4 deletions content/docs/deployment/self-hosting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────────
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion content/docs/getting-started/build-with-claude-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 },
});
```
Expand Down
9 changes: 7 additions & 2 deletions content/docs/getting-started/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions content/docs/getting-started/quick-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 },
});
```
Expand All @@ -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.

<Callout type="info">
This one definition powers the REST API, the Console UI, **and the MCP tools
Expand Down
1 change: 1 addition & 0 deletions content/docs/getting-started/validating-metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 17 additions & 9 deletions content/docs/getting-started/your-first-project.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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 },
});
```
Expand Down Expand Up @@ -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
Expand All @@ -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: [
Expand Down
6 changes: 6 additions & 0 deletions examples/docker/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
dist
.git
*.log
.env
cookies.txt
Loading