Skip to content

Commit eaff014

Browse files
authored
fix(create-objectstack,cli,docs): make the third-party onboarding path actually work on the current release (#2907)
Verified the npm onboarding flow end-to-end as an external user and fixed what broke: the blank template pinned ^6.0.0 while the registry publishes 14.x (scaffolder now syncs @objectstack/* deps to its own release), the template code was updated to the current API (textarea, no api.rest key, explicit sharingModel), os validate gained the ADR-0090 security posture check to restore validate/build gate parity, tutorial docs were corrected (current field API, seeded dev-admin sign-in for data-API curls), and the deployment section gained Backup & Restore, Kubernetes manifests, and copy-ready Docker packaging in examples/docker.
1 parent 16b4bf6 commit eaff014

25 files changed

Lines changed: 541 additions & 37 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-objectstack": patch
3+
---
4+
5+
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.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`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).
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
---
2+
title: Backup & Restore
3+
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.
4+
---
5+
6+
# Backup & Restore
7+
8+
The [go-live checklist](/docs/deployment/production-readiness#go-live-checklist)
9+
requires a documented, tested backup/restore drill. This page is that drill:
10+
what to back up, how, and how to prove the restore works.
11+
12+
## What state a deployment holds
13+
14+
An ObjectStack app is deliberately easy to back up because almost everything
15+
lives in one of four places:
16+
17+
| State | Where it lives | Backup strategy |
18+
|:---|:---|:---|
19+
| **Business data** (records, users, orgs, runtime-authored metadata, encrypted `sys_secret` values) | The database behind `OS_DATABASE_URL` | Database-native backup — see below |
20+
| **Authored metadata** (objects, views, flows, …) | Your project source + the compiled artifact | Git. The artifact is rebuildable with `os build`; no runtime backup needed |
21+
| **Secrets** (`OS_SECRET_KEY`, `OS_AUTH_SECRET`, DB credentials) | Your secret manager | Escrow the values themselves — see the warning below |
22+
| **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 |
23+
24+
<Callout type="warn">
25+
**A database backup without `OS_SECRET_KEY` is an incomplete backup.** Every
26+
`sys_secret` value (encrypted settings, `secret` fields, datasource
27+
credentials) is encrypted under that one key. Restoring the database on a new
28+
host without the original key leaves those values permanently undecryptable.
29+
Escrow the key in your secret manager the day you generate it, and treat
30+
key + database as one backup unit.
31+
</Callout>
32+
33+
## Backing up the database
34+
35+
Use the native tooling of whatever `OS_DATABASE_URL` points at — ObjectStack
36+
adds no extra backup surface on top of the database.
37+
38+
### PostgreSQL
39+
40+
```bash
41+
# Logical backup (small/medium databases; restore with pg_restore)
42+
pg_dump --format=custom "$OS_DATABASE_URL" > backup-$(date +%F).dump
43+
44+
# Larger deployments: continuous archiving / point-in-time recovery
45+
# (WAL archiving, or your provider's managed snapshots)
46+
```
47+
48+
### SQLite (single host)
49+
50+
The database is a single file — but never copy it while the server is
51+
writing. Use SQLite's online backup instead:
52+
53+
```bash
54+
sqlite3 /srv/data/app.db ".backup '/backups/app-$(date +%F).db'"
55+
```
56+
57+
The default location when `OS_DATABASE_URL` is unset is
58+
`<home>/data/objectstack.db` under the ObjectStack home directory.
59+
60+
### Turso / libSQL and managed databases
61+
62+
Use the provider's snapshot, branching, or point-in-time-restore facilities.
63+
Nothing ObjectStack-specific applies.
64+
65+
## Backing up uploaded files
66+
67+
The local storage adapter keeps uploads under `OS_STORAGE_ROOT` (default
68+
`./.objectstack/data/uploads`). On single-host deployments, include that
69+
directory in the same schedule as the database so records and their
70+
attachments restore to the same point in time. Deployments using an external
71+
storage service (S3-compatible, etc.) inherit that service's durability and
72+
versioning instead.
73+
74+
## The restore drill
75+
76+
Rehearse this on a scratch host **before** go-live, and again after any
77+
storage change. A backup you haven't restored is a hope, not a backup.
78+
79+
<Steps>
80+
81+
### Provision a clean host
82+
83+
Fresh container or VM. Install Node 18+ and `@objectstack/cli`.
84+
85+
### Restore the database
86+
87+
```bash
88+
pg_restore --clean --if-exists -d "$OS_DATABASE_URL" backup-2026-07-14.dump
89+
# or copy the SQLite file / restore the provider snapshot
90+
```
91+
92+
### Provide the original secrets
93+
94+
Set `OS_SECRET_KEY` and `OS_AUTH_SECRET` to the **escrowed originals** — not
95+
freshly generated values. Restore `OS_STORAGE_ROOT` contents if you use local
96+
file storage.
97+
98+
### Boot from the artifact and verify
99+
100+
```bash
101+
OS_ARTIFACT_PATH=./objectstack.json OS_PORT=8080 os start
102+
curl -fsS http://localhost:8080/api/v1/health
103+
curl -fsS http://localhost:8080/api/v1/ready
104+
```
105+
106+
Then prove the restore end-to-end: sign in with a real account, open a record
107+
that existed before the backup, and read a value that lives in an encrypted
108+
setting (which exercises `OS_SECRET_KEY`).
109+
110+
</Steps>
111+
112+
## Retention is not backup
113+
114+
The platform's `lifecycle` declarations bound telemetry data (activity 14d,
115+
job runs 30d, notifications 90d, audit 90d-hot) — deleted-by-policy data is
116+
gone from backups taken afterwards. If compliance needs longer, extend
117+
`lifecycle.retention_overrides` **before** go-live, and register an `archive`
118+
datasource if audit data must move to cold storage instead of being retained
119+
hot. See [Production Readiness](/docs/deployment/production-readiness).
120+
121+
## Related
122+
123+
- [Self-Hosted Deployment](/docs/deployment/self-hosting)
124+
- [Production Readiness](/docs/deployment/production-readiness)
125+
- [Environment Variables](/docs/deployment/environment-variables)
126+
- [Drivers](/docs/data-modeling/drivers)

content/docs/deployment/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"pages": [
55
"index",
66
"self-hosting",
7+
"backup-restore",
78
"vercel",
89
"production-readiness",
910
"publish-and-preview",

content/docs/deployment/self-hosting.mdx

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@ back by restoring the previous artifact.
8888
## Option 2 — Docker
8989

9090
The artifact model maps cleanly onto containers: the image contains Node, the
91-
CLI, and one JSON file.
91+
CLI, and one JSON file. The Dockerfile below (plus the compose stack in the
92+
next section and a `.dockerignore`) also ships ready-to-copy in
93+
[`examples/docker`](https://github.com/objectstack-ai/framework/tree/main/examples/docker)
94+
— drop the files into your scaffolded project.
9295

9396
```dockerfile title="Dockerfile"
9497
# ── Build stage: compile TypeScript metadata to the artifact ─────────
@@ -195,9 +198,54 @@ Every runtime exposes two probe endpoints — wire them into Docker
195198
| `GET /api/v1/health` | Process is up and serving HTTP | Liveness probe |
196199
| `GET /api/v1/ready` | Kernel booted, ready for traffic | Readiness probe |
197200

198-
On Kubernetes, the same image works unchanged: mount the secrets as env vars,
199-
point `readinessProbe` at `/api/v1/ready`, and scale — but read the multi-node
200-
note below first.
201+
### Kubernetes
202+
203+
The same image works unchanged. A minimal reference Deployment — secrets from
204+
a `Secret`, probes on the two endpoints above:
205+
206+
```yaml title="deployment.yaml"
207+
apiVersion: apps/v1
208+
kind: Deployment
209+
metadata:
210+
name: my-app
211+
spec:
212+
replicas: 1 # >1 requires OS_CLUSTER_DRIVER — see below
213+
selector:
214+
matchLabels: { app: my-app }
215+
template:
216+
metadata:
217+
labels: { app: my-app }
218+
spec:
219+
containers:
220+
- name: app
221+
image: registry.example.com/my-app:latest
222+
ports:
223+
- containerPort: 8080
224+
envFrom:
225+
- secretRef:
226+
name: my-app-secrets # OS_DATABASE_URL, OS_AUTH_SECRET, OS_SECRET_KEY
227+
livenessProbe:
228+
httpGet: { path: /api/v1/health, port: 8080 }
229+
periodSeconds: 30
230+
readinessProbe:
231+
httpGet: { path: /api/v1/ready, port: 8080 }
232+
initialDelaySeconds: 5
233+
periodSeconds: 10
234+
---
235+
apiVersion: v1
236+
kind: Service
237+
metadata:
238+
name: my-app
239+
spec:
240+
selector: { app: my-app }
241+
ports:
242+
- port: 80
243+
targetPort: 8080
244+
```
245+
246+
`/api/v1/ready` returns 503 while the kernel is booting *and* during graceful
247+
shutdown, so rolling restarts drain cleanly. Before setting `replicas > 1`,
248+
read the multi-node note below.
201249

202250
## Reverse proxy & TLS
203251

@@ -247,6 +295,7 @@ Disable with `OS_MCP_SERVER_ENABLED=false`. See
247295

248296
## Related
249297

298+
- [Backup & Restore](/docs/deployment/backup-restore) — what to back up, and the restore drill
250299
- [Deployment Modes](/docs/deployment) — the map of local / standalone / Cloud
251300
- [`os start` reference](/docs/getting-started/cli#os-start) — every flag and env var
252301
- [Environment Variables](/docs/deployment/environment-variables) — the full catalog

content/docs/getting-started/build-with-claude-code.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export const Ticket = ObjectSchema.create({
114114

115115
fields: {
116116
subject: Field.text({ label: 'Subject', required: true, searchable: true, maxLength: 200 }),
117-
description: Field.longText({ label: 'Description' }),
117+
description: Field.textarea({ label: 'Description' }),
118118

119119
priority: Field.select({
120120
label: 'Priority',
@@ -139,6 +139,7 @@ export const Ticket = ObjectSchema.create({
139139
}),
140140
},
141141

142+
sharingModel: 'private', // org-wide default (OWD) — required by the security gate
142143
enable: { apiEnabled: true, searchable: true },
143144
});
144145
```

content/docs/getting-started/cli.mdx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ The CLI is available as `objectstack` or the shorter alias `os`. Installed as a
2222
### Create a project
2323

2424
```bash
25-
npx @objectstack/cli init my-app
25+
npm create objectstack@latest my-app
2626
cd my-app
2727
```
2828

29-
This scaffolds a working project with `objectstack.config.ts`, a sample object, and all dependencies installed.
29+
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).)
3030

3131
### Add more metadata
3232

@@ -80,6 +80,11 @@ predicate/schema/binding mistakes that fail silently at runtime), and `os dev --
8080

8181
Scaffolds a new ObjectStack project with configuration, TypeScript setup, and initial metadata files.
8282

83+
> **Which scaffolder?** For a new app, prefer **`npm create objectstack@latest`** — it
84+
> also derives your namespace, pins the framework packages to the current release, and
85+
> installs the AI skills bundle + `AGENTS.md`. Reach for `os init` when you want a
86+
> **plugin** skeleton or a **bare config** in an existing directory.
87+
8388
```bash
8489
os init my-app # Create with default "app" template
8590
os init my-plugin -t plugin # Create a plugin project

content/docs/getting-started/quick-start.mdx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const Ticket = ObjectSchema.create({
6161

6262
fields: {
6363
subject: Field.text({ label: 'Subject', required: true, searchable: true, maxLength: 200 }),
64-
description: Field.longText({ label: 'Description' }),
64+
description: Field.textarea({ label: 'Description' }),
6565

6666
status: Field.select({
6767
label: 'Status',
@@ -75,6 +75,7 @@ export const Ticket = ObjectSchema.create({
7575
}),
7676
},
7777

78+
sharingModel: 'private', // org-wide default (OWD) — required by the security gate
7879
enable: { apiEnabled: true, searchable: true },
7980
});
8081
```
@@ -83,11 +84,14 @@ export const Ticket = ObjectSchema.create({
8384

8485
- **Names** are `snake_case` and namespace-prefixed (`support_desk_ticket`) — the
8586
scaffolder derives the prefix from your project name.
86-
- **Field types** are `Field.*` builders (`text`, `longText`, `select`, `date`,
87+
- **Field types** are `Field.*` builders (`text`, `textarea`, `select`, `date`,
8788
`lookup`, …), not raw strings. A `select`'s `options` carry the exact labels,
8889
values, and colors you'll see in the UI — verify these match your intent.
8990
- **`required`, `default`, `searchable`** encode behavior the UI and API inherit
9091
automatically.
92+
- **`sharingModel`** is the org-wide default for records the user doesn't own —
93+
an explicit, authored security decision (`private` unless you have a reason
94+
otherwise). The validation gate rejects custom objects that omit it.
9195

9296
<Callout type="info">
9397
This one definition powers the REST API, the Console UI, **and the MCP tools

content/docs/getting-started/validating-metadata.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ dangling one.
5757
| Protocol schema (Zod) |||
5858
| CEL / predicate validation |||
5959
| Widget-binding integrity |||
60+
| Security posture (ADR-0090 — e.g. every custom object declares `sharingModel`) |||
6061
| Emits `dist/objectstack.json` |||
6162

6263
So `os validate` is the fast inner-loop check (no artifact); `os build` is what

content/docs/getting-started/your-first-project.mdx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,12 @@ export default defineStack({
112112
name: 'My App',
113113
},
114114
objects: Object.values(objects),
115-
api: {
116-
rest: { enabled: true, basePath: '/api' },
117-
},
118115
});
119116
```
120117

118+
The REST API needs no configuration — it is on by default for every object
119+
with `apiEnabled`.
120+
121121
**`src/objects/note.object.ts`** is a complete data model — schema, validation,
122122
API surface, and searchability in one typed definition:
123123

@@ -132,9 +132,10 @@ export const Note = ObjectSchema.create({
132132

133133
fields: {
134134
title: Field.text({ label: 'Title', required: true, searchable: true, maxLength: 200 }),
135-
body: Field.longText({ label: 'Body' }),
135+
body: Field.textarea({ label: 'Body' }),
136136
},
137137

138+
sharingModel: 'private', // org-wide default — an explicit, authored decision
138139
enable: { apiEnabled: true, searchable: true },
139140
});
140141
```
@@ -166,17 +167,24 @@ npx os dev --ui # then open http://localhost:3000/_console/
166167

167168
## 4. Call your API
168169

169-
The REST API is derived from the object metadata — you wrote no routes. With
170-
the dev server running:
170+
The REST API is derived from the object metadata — you wrote no routes. Data
171+
endpoints run under the same permission model as the UI, so first sign in as
172+
the dev admin the server seeds on an empty database
173+
(`admin@objectos.ai` / `admin123`):
171174

172175
```bash
176+
# Sign in once — stores the session cookie
177+
curl -c cookies.txt -X POST http://localhost:3000/api/v1/auth/sign-in/email \
178+
-H "Content-Type: application/json" \
179+
-d '{"email": "admin@objectos.ai", "password": "admin123"}'
180+
173181
# Create a record
174-
curl -X POST http://localhost:3000/api/v1/data/my_app_note \
182+
curl -b cookies.txt -X POST http://localhost:3000/api/v1/data/my_app_note \
175183
-H "Content-Type: application/json" \
176184
-d '{"title": "Hello ObjectStack", "body": "Created via the generated API"}'
177185

178186
# Query records (filtering, sorting, pagination built in)
179-
curl "http://localhost:3000/api/v1/data/my_app_note?select=title&sort=-created_at&top=10"
187+
curl -b cookies.txt "http://localhost:3000/api/v1/data/my_app_note?select=title&sort=-created_at&top=10"
180188
```
181189

182190
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.
190198
```typescript title="src/objects/note.object.ts"
191199
fields: {
192200
title: Field.text({ label: 'Title', required: true, searchable: true, maxLength: 200 }),
193-
body: Field.longText({ label: 'Body' }),
201+
body: Field.textarea({ label: 'Body' }),
194202
status: Field.select({
195203
label: 'Status',
196204
options: [

0 commit comments

Comments
 (0)