Skip to content

New how-to: How to self-host ObjectStack - #156

Open
hotlong wants to merge 5 commits into
mainfrom
claude/issue-92-how-to-self-host-objectstack
Open

New how-to: How to self-host ObjectStack#156
hotlong wants to merge 5 commits into
mainfrom
claude/issue-92-how-to-self-host-objectstack

Conversation

@hotlong

@hotlong hotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #92

New English-only how-to at content/blog/how-to-self-host-objectstack/index.mdx, cover-en.svg, and one body diagram (what-runs-where-en.svg).

This branch resumes a draft that an earlier session left unpushed. The content commit is 0ed52a4; this round's work is the verification pass on top of it (6c477b7), which re-checked every command, endpoint, environment variable, version and boundary claim and corrected the ones that did not survive, plus an SVG patch round (58df4e5).

What ships

A runnable end-to-end procedure: the compiled-artifact mental model, prerequisites, scaffold and compile, the database driver step, the four environment values that decide whether a deployment survives its second boot, starting the server, five verification checks that each fail differently, and an honest bill (upgrades, backups, on-call) plus a "when not to do this" section.

Primary keyword self-host ai app platform; secondary self-hosted internal app platform, run ai agents on own infrastructure. Slug how-to-self-host-objectstack. Answer in the first 100 words. Description 203 chars (≤ 230).

Facts verified against shipping tooling

Sources: objectstack-ai/objectstack at origin/main (7da4cc2b), the objectstack-ai/objectos docs repo, the published npm package, and a live run — scaffold → os buildos start against PostgreSQL 16 on Node 22.22.2 with @objectstack/cli@17.2.0.

Reproduced exactly, not merely read:

Claim Evidence
CLI version 17.2.0 npm view @objectstack/cli version17.2.0; packages/cli/package.json
Node 22 or newer required engines in packages/cli and packages/create-objectstack
Scaffold writes config, starter object, AGENTS.md, Dockerfile, docker-compose.yml, skills bundle real npm create objectstack@latest run
os build is an alias of os compile packages/cli/src/commands/build.ts extends Compile
Artifact 2.2 KB, Data: 1 Objects 2 Fields, Runtime: 3 plugins real build; artifact is 2223 bytes
Missing pg failure block reproduced verbatim, exit 1
pg / mysql2 / tedious are optional peers packages/drivers/driver-sql/package.json peerDependenciesMeta
Banner Mode: production, Driver: SqlDriver(pg), Tenancy: single, Plugins: 33 loaded real boot
68 tables on an empty database SELECT count(*) …68
/api/v1/ready body matched byte for byte
/health liveness vs /ready readiness split packages/runtime/src/route-ledger.ts
Anonymous deny is 401 + UNAUTHENTICATED live curl; packages/core/src/security/anonymous-deny.ts
Port never auto-shifts in production portAutoShiftAllowed is true only for dev or NODE_ENV=development
One-major metadata conversion window ADR-0087 §Window
MongoDB is single-tenant only MONGODB_MULTI_TENANT_UNSUPPORTED + tenancy guard
Image is multi-arch, tag == CLI version, ships only the CLI docker/Dockerfile, .github/workflows/docker-publish.yml

Corrections made to the draft

  1. OS_AUTH_SECRET — the draft's headline warning was wrong. It claimed a missing secret means "auth routes are silently skipped and the server runs unauthenticated". os start in fact mints and persists one to auth-secret in the runtime home directory (mode 0600) via readOrCreateAuthSecret. Measured: with the variable unset, an anonymous read still returned 401. The real failure mode is an ephemeral filesystem — I removed the home directory, rebooted, and got a different secret, which invalidates every session — plus per-replica divergence. The canonical reference/environment-variables doc states the same contract ("must be identical on every replica").
  2. Verification check 4 no longer claims to catch a missing OS_AUTH_SECRET (it cannot). It now verifies the anonymous gate, includes the code key the REST seam actually returns, and warns against a tolerant reader that accepts both the flat and wrapped envelopes.
  3. /api/v1/health sample body was missing the timestamp field the handler returns.
  4. Observability was understated. The runtime ships an OTLP/HTTP exporter (OS_OBS_EXPORTER=otlp), defaulting to noop, with no @opentelemetry dependency anywhere in the tree. What is genuinely absent is a Prometheus scrape endpoint — push, not pull.
  5. Audit. sys_audit_log is declared append-only and refuses edits and deletes through the API; the draft said no immutability layer ships at all. What is missing is storage-level immutability against a direct UPDATE.
  6. Bootstrap admin scoped to the single-tenancy posture; walled postures remove that path and refuse startup without OS_PLATFORM_OWNER_EMAIL.
  7. Both docs links were broken — wrong host and wrong path (objectstack.ai/docs/data-modeling/drivers, .../deployment/self-hosting; the docs tree has deploy/, not deployment/). Now docs.objectos.ai/docs/{configure/data-sources, deploy, reference/environment-variables}, matching both the site's own convention (SecurityPage.astro) and the docs repo layout. These were the only two objectstack.ai/docs links on the entire site.
  8. Bare-artifact bootnpx os start does not resolve outside a project, and a freshly installed CLI has no pg either. Both now stated; the draft implied it simply worked.
  9. os build output block restored to its real shape; MySQL's two integrity gaps named concretely (no upsert conflict target; unique index bounded by key length); container caveat now reports what was read from the published image build definition rather than only what was not run.

Dedup and consistency

  • self-hosted-ai-app-platform and power-platform-lock-in-dataverse-self-host are linked as the argument, not restated.
  • No contradiction found with src/components/SecurityPage.astro or content/pages/*/trust-center.ts. They independently corroborate the post: "The open-source ObjectStack runtime does not need a license callback or product telemetry channel to operate", and the residency table's audit-logs / AI-context rows match the diagram's boundary.

Gates

Union run after the content commit, at head 6c477b7, through the shared verify lock (VERDICT command-exit 0 · held the lock 83s · waited 0s):

Gate Its own verdict line
pnpm content:lint passes; only Date is in the future: Tue Sep 15 2026 (expected for a future slot)
pnpm check Result (133 files): 0 errors, 0 warnings, 0 hints
pnpm build 867 page(s) built
pnpm seo:smoke SEO smoke test passed (866 HTML pages checked)

git status --porcelain empty after the build — nothing generated for this English-only post.

Browser verification

Served dist/ and drove Chromium at 1440×900 and 390×844, scrolling lazy images into view and decode()-ing before judging:

  • HTTP 200; scrollWidth === clientWidth at both widths (no horizontal scroll)
  • zero page errors, zero failed requests
  • both SVGs decode; cover appears exactly once and is not repeated in the body
  • all 8 internal article links return 200, including /en/glossary/ontology/ and both dedup posts
  • both SVGs rendered standalone at native size and inspected as PNGs — every ground is light and every text node is legible. This check was not sufficient and missed a colour-fidelity bug; see Patch round below.

Patch round (58df4e5) — 15 shadowed text fills

Review caught a real defect the round above missed. Both SVGs declare text{fill:#173a36} in their <style> block while 15 <text> elements carried a fill="…" presentation attribute. A presentation attribute has specificity 0, so any CSS rule outranks it: the type selector won on 11 of them, and .small{fill:#657872} won on the other 4. Every one rendered the default dark teal or grey instead of the brown/green it was authored as — the same dead-attribute pattern #125/#133 swept out of 29 SVGs.

Not a legibility bug: all grounds are light, so the earlier dark-on-dark check passed correctly. What was lost was the colour coding that carries the diagram's meaning — BUILD vs RUN captions, the mono artifact paths, the endpoint line, and the "outside your boundary" note.

Confirmed mechanically rather than by eye, with getComputedStyle in Chromium:

Before After
grep -o '<text[^>]*fill="[^"]*"' cover-en.svg what-runs-where-en.svg | wc -l 15 (5 + 10) 0
computed fill matches authored colour 0 / 15 — all SHADOWED 15 / 15 — all honoured

Sample of the before reading: "IN YOUR REPO — BUILD" want #8a6a3a renders #173a36; "Receives only minimal, approved" want #a8703a renders #657872.

Fix is fill="X"style="fill:X" on <text> elements only, so an inline style outranks both rules. The 22 <rect> / <path> / marker fills are not shadowed and are untouched — after the change the only remaining fill=" attributes are 19 <rect> and 3 <path>. No wording, geometry or colour value changed; the diff is exactly 15 insertions and 15 deletions.

Both SVGs re-rendered standalone at native size and inspected: captions brown/green, mono paths coloured, external-model note now matching its dashed arrow, nothing moved.

Gates for this round: SVG-only change, so pnpm content:lint and pnpm check were re-run (VERDICT command-exit 0; Result (133 files): 0 errors, 0 warnings, 0 hints). The full union is not owed again — no .mdx, route, or page-generating input changed.

Cadence

date: 2026-09-15T10:00:00+08:00, no updated, status: published, topic: governance, audience: it.

Out-of-scope findings


🤖 Generated with Claude Code

Generated by Claude Code

New English how-to at content/blog/how-to-self-host-objectstack/:
index.mdx plus cover-en.svg and one body diagram. Every command
verified against ObjectStack 17.2.0 / Node 22 / PostgreSQL 16.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
New English-only how-to at content/blog/how-to-self-host-objectstack/:
index.mdx, cover-en.svg, and one body diagram (what runs where).

Every command was verified against what ships: @objectstack/cli 17.2.0
on Node 22 with PostgreSQL 16 — scaffold, os build, os start, and the
five-step install verification. The container path is labelled as unrun
(no Docker daemon available) rather than presented as verified.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Re-verified every command, endpoint, env var and boundary claim in the
draft against @objectstack/cli 17.2.0 (published on npm), the framework
repo at origin/main, and the objectos docs repo — plus a live run:
scaffold, os build, and os start against PostgreSQL 16 on Node 22.

Reproduced exactly: the 2.2 KB artifact, "Runtime: 3 plugins", the
"Plugins: 33 loaded" / "Driver: SqlDriver(pg)" / "Tenancy: single"
banner, 68 tables created on an empty database, the /api/v1/ready body,
and the missing-pg boot failure verbatim.

Corrections:

- OS_AUTH_SECRET did NOT behave as the draft claimed. `os start` mints
  and persists a secret to <home>/auth-secret (0600) via
  readOrCreateAuthSecret, so a server with no OS_AUTH_SECRET still
  authenticates — measured: an anonymous read returned 401 either way.
  The real failure is an ephemeral filesystem (next boot mints a
  different secret; sessions invalidated) and multi-replica divergence.
  Verified by booting with the variable unset, then removing the home
  directory and rebooting: different secret both times. The canonical
  Environment Variables doc states the same contract.
- Verification check 4 no longer claims to catch a missing
  OS_AUTH_SECRET; it verifies the anonymous gate. Added the `code` key
  the REST seam actually returns, and the flat-vs-wrapped envelope note.
- /api/v1/health sample body was missing the `timestamp` field.
- Observability was understated: the runtime ships an OTLP/HTTP exporter
  (OS_OBS_EXPORTER=otlp), defaulting to noop, with no @opentelemetry
  dependency in the tree. What is absent is a Prometheus scrape endpoint.
- Audit: sys_audit_log IS declared append-only and refuses API edits and
  deletes; what is missing is storage-level immutability. The draft said
  no immutability layer ships at all.
- Bootstrap admin scoped to the single-tenancy posture; walled postures
  require OS_PLATFORM_OWNER_EMAIL and refuse startup without it.
- Docs links pointed at objectstack.ai/docs paths that do not exist. Now
  docs.objectos.ai/docs/{configure/data-sources,deploy,reference/
  environment-variables}, matching the site's own convention and the
  docs repo tree.
- Bare-artifact boot: `npx os start` does not resolve outside a project,
  and a freshly installed CLI has no pg either. Stated both.
- os build output block restored to the real shape; MySQL's two
  integrity gaps named concretely; container caveat now reports what was
  read from the published image build definition.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Both SVGs declared `text{fill:#173a36}` in their <style> block while 15
<text> elements carried a `fill="…"` presentation attribute. A
presentation attribute has specificity 0, so any CSS rule beats it: the
type selector won on 11 of them and the `.small{fill:#657872}` class won
on the other 4. Every one rendered the default dark teal or grey instead
of the brown/green it was authored as.

Measured with getComputedStyle in Chromium before the change — all 15
reported SHADOWED, e.g. "IN YOUR REPO — BUILD" want #8a6a3a renders
#173a36, and "Receives only minimal, approved" want #a8703a renders
#657872 — and all 15 report honoured after it.

Fix is `fill="X"` -> `style="fill:X"` on <text> elements only, so an
inline style outranks both rules. The 22 <rect>/<path>/marker fills are
not shadowed and are untouched; no wording, geometry or colour value
changed. Re-rendered both at native size and inspected: the BUILD/RUN
captions, the mono artifact paths, the endpoint line and the
external-model note now carry their intended colours.

Not a legibility bug — every ground is light, so the earlier
dark-on-dark check passed correctly and the rendered text was always
readable. What was lost was the colour coding that carries the diagram's
meaning.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
… vocabulary

Closed vocabulary from PR #160: every tag is either one of the seven
tier-2 canonicals in this locale's rendering, or the proper noun of a
product / company / protocol the post actually compares against.

None qualifies. The post is an operations walkthrough; MCP, AI agents and
the open business ontology appear as endpoint, closing framing and a
glossary link rather than as its subject. PostgreSQL and Docker are the
database and the container runtime this install uses, not products it
compares against, and ObjectStack / ObjectOS are the site's own brand
names, dropped by the PR #160 ruling. Empty is the measured outcome
recorded on #158.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr

hotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Tags reconciled with the #78 closed vocabulary (#163): none — this post ends with tags: [].

It carries no tier-2 canonical by aboutness: MCP appears as an endpoint in the boot banner and the CTA, AI agents as closing framing, and the open business ontology as a one-sentence glossary link — none is the post's subject, which is the self-hosting procedure. PostgreSQL and Docker are the database and the container runtime this install uses, not products the post compares against, so neither qualifies as a tier-3 proper noun; ObjectStack / ObjectOS are the site's own brand names, dropped by the PR #160 ruling. Empty is the measured consequence recorded on #158, not a gap to paper over.

Commit 1d1ed67 on this branch, on top of 58df4e5, no amend/force/rebase. tags: is the only line it touches — proof at that head:

git diff 58df4e5..HEAD -U0 | grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' | grep -vE '^[+-]\s*(tags:|- )'

returns 0 lines. pnpm content:lint at that head printed ✓ content lint passed (335 files checked), with the one expected advisory Date is in the future: Tue Sep 15 2026.

Why this is a comment and not the one-line note in the PR body, which is what #163 asks for: the body cannot be read back in full through the GitHub MCP read path. Two consecutive pull_request_read calls both returned it truncated at the identical byte, mid-sentence in the "Patch round" section (Both SVGs declare ... in their — cut just before the style-element name). update_pull_request replaces the whole body, so re-sending what I can read would permanently drop everything after that point. The other four PRs (#151, #153, #152, #147) read back whole and got the line in their bodies as specified. Recorded on #163 for the PM; a body edit here needs someone who can read the tail.

Generated by Claude Code


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New how-to: How to self-host ObjectStack

2 participants