← All articles Engineering

The page you're reading is a database record

How OBTO hosts its own website on OBTO — pages as rows, deploys as checksums, and the one build step we kept on purpose.

OBTO Engineering · Insights from the Glass Box

There is no index.html behind this page. No repository, no CI pipeline, no nginx config with our name in it. The article you are reading is a row in a database — collection pltf_page, app ob_www, domain www — and it became a web page because a build was asked to compile it. We tell people OBTO treats an application as data. Our own website is where we prove we mean it, so this build log walks through how that actually works, including the parts that are inconvenient.

Where the site actually lives

Every page on www.obto.co is a pltf_page record. The home page is a record. The pricing page is a record. This article is a record, and its name is its address: a record called our-website-is-a-database-record serves at /site/ob/our-website-is-a-database-record. Nothing maps names to URLs except the convention itself.

The whole site, right now, is 46 URLs in the sitemap. Twenty-five of them are blog articles, counting this one — publishing this record added both numbers while the paragraph was being written. Each one is a single addressable row — pltf_page::ob_www::www::<slug> — that any tool speaking the platform's protocol can read, search, or patch. The sitemap itself is generated from those records: publish a row and it shows up. Nobody edits an XML file, because there is no XML file.

It goes further than the pages. The social-preview card at the top of this article's link embeds is a PNG in the same platform file store, uploaded through the same tools, served by the same viewer. The image that decorates the record lives next to the record.

The one build step we kept on purpose

If you read our runtime piece, you know the platform's thesis: application artifacts resolve per request, and hot reload is the default, not a dev mode. So it is fair to ask why the marketing site has a vite build at all.

Because anonymous public traffic is a different problem. A stranger on a phone doesn't benefit from per-request artifact resolution; they benefit from minified, cache-busted static assets that arrive fast. Marketing pages don't need live mutability. They need to load. So the public site is the platform's one deliberate exception: records are still the single source of truth, and a production build compiles them into the assets a browser actually receives.

Here is what that exception costs, stated plainly. A build step reintroduces the oldest question in web publishing: is it live yet? A record write is not the live site until the build succeeds — forget the build, and you are proofreading a page nobody can see. Caches downstream compound it. This week, one of our own tools fetched a three-week-old copy of our blog index from an intermediate cache and presented it with complete confidence. The rule we run by now: nothing is called shipped until a real browser, on the production URL, renders the change. Not the tool's opinion. The browser's.

Editing a website that has no files

The point of this was never that pages are stored as data instead of files — every CMS built since 2003 can say that. The point is who is allowed to write to those records, and how safely. On this platform, an agent writes directly to a production page. No human runs git push. No CI gate stands between the conversation and the live URL. That should worry you unless something is doing the job code review normally does — here is what does.

There are two write paths, and they solve two different problems.

Small edits are line-level patches. A patch names the record, the line range, the replacement text — and an anchor: the first characters of the line you believe you are replacing. If the record changed since you last read it, the anchor doesn't match and the patch is refused. That guard exists because line numbers go stale, and a patch that lands on the wrong line is worse than no patch at all.

New pages take the other path: deploy by reference. The page's bytes are uploaded to the file store, and the platform is told to fetch them from that URL along with the sha256 hash they are expected to have. If the fetched bytes hash differently — truncated upload, mangled encoding, anything — the deploy is refused. It lands byte-exact or it fails loudly. There is no "mostly deployed."

obto_stage_chunk({ action: "from_url", url, expectedSha256 })
obto_upsert_record({ collection_name: "pltf_page",
  name: "our-website-is-a-database-record",
  appName: "ob_www", domain: "www", uploadId, sha256 })
obto_build_app({ appName: "ob_www", domain: "www" })

Those three calls are not pseudocode. They are the calls that shipped the page you are reading.

Those two checks — the anchor match, the hash match — are the irreducible structural floor for a write like this one. They are not the entire safety model, and it would be dishonest to imply otherwise. Every write also carries tenant-scoped authorization and runs through policy validation, and different artifact types — pages, routes, scripts, policies — each have their own enforced shape, so a write can't cross into a contract it doesn't own. Underneath that, every write lands as an immutable record with a version snapshot, and a rollback path exists if one is ever needed. Today those three are platform-level guarantees, not something the agent writing this sentence can invoke itself — exposing version history and rollback as callable tools is on the roadmap, not shipped yet. We would rather say that plainly than let the anchor and the hash sound like more than they are.

One honest quirk, since this is a build log. The file store's public viewer refuses to serve raw .html — a safety posture for a store that also holds user uploads — but serves .txt byte-exact. So a new page's bytes travel to the platform dressed as a text file before the deploy call ingests them as a page. It is a workaround, we know exactly how it looks, and it has shipped every recent article on this blog without corrupting a byte. We would rather publish the workaround than pretend the pipeline is prettier than it is.

A website you can query

Storage choices usually stay invisible. This one pays rent weekly.

Because the site is rows, you can ask it questions. Semantic search runs over the site's own source. You can grep production — the actual serving records, not a checkout that might have drifted — and get line numbers back. Our recurring site health check is not a crawler duct-taped to a spreadsheet; it is a set of queries: which pages carry a broken social image, which meta description crept past 155 characters, whether the sitemap picked up this week's article. This morning's sweep found four legal-and-contact pages still referencing a dead image URL. They are rows too. Fixing them is a patch, not a redeploy.

And because every write is scoped and receipted — who changed which record, when — the website has the same audit trail as everything else on the platform. We wrote about that discipline in How We Built the Glass Receipt; the point here is that it applies to our own public site, not just to customer workloads.

The numbers

Forty-six URLs. Twenty-five articles. The largest page record on the site is about 21 KB of HTML, and it deployed in one verified call. Articles move through this pipeline twice a week now. The entire marketing site is one ordinary app on the platform — the same kind of app a free Builder tenant gets, with the same tools and the same limits. There is no special internal tier that makes any of this easier for us than it would be for you.

Why bother

The uncomfortable version of the question: wouldn't a static site generator and a $0 GitHub Pages deploy have been less work? Probably, at first. But then our website would be the one production system in the building that our own platform doesn't run — and everything we publish about agents, tools, and MCP would be advice we don't take.

The tools that maintain this site are the public ones. Same contract, same guards, same receipts a customer gets. When the pipeline has a rough edge — the .txt costume, the stale cache — we hit it before you do, and it becomes a fix or a paragraph like this one. Describe it, ship it, own it: this page is the receipt.

— The OBTO engineering team

Your app is data here too

Build on the same substrate this website runs on — queryable, patchable, receipted — with flat, auditable pricing.

Get started

More from the OBTO blog