NAI OS
A collaborative workspace for building ontologies and filling them with data —
worked three ways: in the browser, as .nai code from a terminal, and over MCP by an
agent. Next.js + Supabase, its own repository, live at nai-onto.vercel.app. This is
the one page describing it.
nai-onto (github.com/udopnink/nai-onto) is a second, separate web
application. Each ontology has a model layer (classes in a single-inheritance
hierarchy plus mixins, directed relations, stored and derived properties, classification
dimensions, tags), an instance layer (nodes of a class linked along the relations
the model declares), and an evaluation layer (path and query expressions over the
instance graph, JavaScript library functions executed in a QuickJS sandbox, and validation rules
built on them). The interface and every entity name and description are localized in English and
German. “The same model can be worked three ways: in the web app, as code through the
nai CLI and the .nai DSL, and remotely by MCP agents through
/api/mcp.”
What this page corrected. The previous version of this page described
nai-onto at phases 1–3, pre-deployment, with Row Level Security not yet enabled
and a migration granting full table access to the anon role. All of that has since
shipped past: the app is deployed, RLS is the wall, and anon holds no grants at all.
Everything below is read from the repository at commit 1bc7439
(“docs: refresh README and overviews to the shipped product (NAI Onto, F3 state)”).
Two naming notes. The product is NAI Onto (NAI = Nink AI); the in-app title still reads “NAI Ontology”, and “the rename to ‘NAI Onto’ is queued for the next UI phase.”
“Production: https://nai-onto.vercel.app (Vercel, hosted Supabase project).” The app deploys to Vercel from main; Supabase migrations are pushed separately with npx supabase db push against the production project.
As of 2026-09-03: “phases 1 through 6 complete (schema, modeling UI, graph and filtering, instances, evaluation, scripting, JSON/YAML/Turtle transfer, transactional guarded writes, renameable keys with canonical ids, roles and RLS, comments and issues), plus ontology as code, remote modeling over MCP, and the UI navigation refactor F1–F3.”
20 migrations under supabase/migrations/, 0001_core through 0020_ontology_header_fields.
106 unit test files and 30 integration test files (Vitest); “integration tests run against the real local Postgres, not mocks.”
admin, editor, reader. Row Level Security in Postgres is the coarse wall — “a non-member cannot read, write, or even learn that an ontology exists, and both the app and the agent layer answer ‘not found’ for it.” The finer matrix lives in src/lib/domain/rbac.ts.
anon role holds nothing“The anon role holds no table or function grants (a migration-time assertion guards that), so the only ways in are a session or a personal access token.” The RBAC design calls this the ACL trap and revokes the earlier blanket grants explicitly.
A manually seeded platform_admins flag grants every-ontology access for operations, “invisible to normal users”. Whoever creates an ontology becomes its first admin; an ontology can never be left with zero admins.
Email + password or Google OAuth through Supabase Auth. “There is no self-service sign-up; accounts are provisioned in Supabase.”
Personal tokens created at /tokens with scope read, write, or operator. “A token acts as that user with their ontology roles; the scope only narrows further.” The plaintext is shown exactly once.
Threads on any entity or the ontology itself; issues resolve and reopen, the sidebar entry carries an unread badge computed per member, and readers may take part. “Deleting an author's account keeps their posts.”
Independent ontologies with a default language, localized names, per-ontology slug keys, and “a header block of description, source link, keywords, and notes” — the three header fields added by migration 0020_ontology_header_fields.
/ontologies: “your ontologies, ontologies shared with you, and archived ones, as cards or a sortable list with keyword chips and source links; name filter, bulk archive and bulk delete.” Not a public landing page — it is membership-scoped.
“Each ontology opens on an overview page: kind cards with counts, a status strip, and the header fields (description, source, keywords, notes).”
Archive ontologies to move them to the archived group on the home page without deleting; restore anytime, or permanently delete — including all contents — from the archive view. One click, no confirmation, fully reversible, admin only.
Bulk actions “appear only when a filter is active, and the server refuses an empty one outright”; the forms post the filter text, not a list of ids, so the scope cannot be tampered with or go stale.
Single-inheritance hierarchy plus mixins; feature exclusion per class; cycle-safe by construction. What a class effectively has is computed rather than stored.
Directed, typed relations between classes with their own properties; effective for the source hierarchy, subclass-conformant targets.
Stored (string, number, boolean, date, enum; single or multi cardinality; required) and derived kinds — path, query, and scripted, computed on read and never persisted.
Orthogonal classification dimensions with ordered values, assignable per class.
Ontology-scope and instance-scope tags, assignable to classes, relations, classifications, and instances.
Markdown descriptions with [[class:…]], [[relation:…]], and [[instance:…]] mentions; automatic backlinks in both directions.
“Keys can be renamed; ids do not move, so links, expressions, exports, and the code workspace survive a rename.” Structural writes run as transactional SECURITY INVOKER RPCs under a per-ontology advisory lock.
Per-class tables of instances with stored and derived (ƒ) property columns, tag chips, property-value filters, and pagination.
“The instantiated relations as their own list — every link between nodes, with the shared filter vocabulary and pagination; links are created on a node's page.” Its own route, /edges.
Create/edit forms generated from the class's effective stored properties — typed inputs, multi-value row editors, required enforcement, localized errors.
Typed instance-to-instance links per effective relation, with link-level property values and conformance guards on every write.
Path and query expressions evaluated over the live instance graph; scripted properties run in the sandbox. “Errors render inline — never break the page.”
“Every kind as a table with a scope header, column filters, and pagination; the List entry in the sidebar reopens the last list you were on.”
The class/relation graph in hierarchy or flattened form, and the filtered instance set as a graph — class-badged nodes, relation-labeled edges, detail panels with on-demand derived values.
“The live model, or the instances, rendered as .nai source in an editor; unmanaged ontologies can review and apply edits from there.” List / Graph / Text is a switcher on every scope page, not a separate area.
.nai DSLA readable, line-oriented text format for the whole model, plus an instance statement form. “The DSL is a syntax skin over OntologyDoc” — a fourth adapter over the existing transfer spine. The formatter is canonical, so git diffs carry meaning.
Offline validation, a semantic diff against the database with detected contexts (renames, enum removals), corrections declared in .nai/changeset.yaml, then a guarded apply that records the git SHA as provenance. “Apply refuses on validation errors, a stale fingerprint, or unresolved contexts.”
“Renames are declared, never inferred”: a rename carries an explicit was: <old-key> marker. “No marker, no match: the entity plans as CREATE plus a loud REMOVE of the old key.”
nai manage makes the files the single source of truth: the web app refuses model mutations and renders the forms read-only. manage --instances is a second, separate opt-in. “Unmanaging is a logged, admin-only action in the app”, never a CLI verb.
For a managed ontology: which SHA the database is at, and the semantic diff between the model files bundled with the running deployment and the live database.
“One-way instance bulk-in from a disposable compact-YAML or .nai seed file, validated against the live model; create-or-update by key, never delete.”
nai translate --to <locale> writes a skeleton of every missing localized name and description into a sidecar file, so translation becomes its own reviewable diff.
The app exposes one agent-facing surface: POST /api/mcp, “a stateless
streamable-HTTP MCP endpoint authenticated with a personal access token
(Authorization: Bearer nai_…, created at /tokens).” The token's
scope decides which tools tools/list offers. The nai CLI is the second
client of the same contract: “the CLI speaks the /api/mcp tools/call contract
directly (src/cli/remote.ts), configured by NAI_REMOTE_URL +
NAI_TOKEN env vars only — no credentials in files.”
| Scope | Adds | What that lane is |
|---|---|---|
read | list_ontologies, export_model | Pull the live model into a workspace. |
write | import_merge, seed | Additive bulk on-ramp for model and instances. Never deletes. |
operator | plan, apply, manage | The real modeling loop: diff, corrections, guarded removals, and the managed-as-code lock. |
Rules that hold everywhere, quoted from docs/remote-modeling.md:
“A token never grants what your role wouldn't; unknown ontologies and non-membership both answer ‘not found’.”
“import_merge and seed create-or-update only — they
never delete; removals go through plan/apply. Both are refused on managed
ontologies (model / instances respectively).”
“Payload caps: 10 MB per doc or seed, 64 KB per changeset.”
“check, fmt, bundle are offline; plan --interactive
and apply --dry-run are local-only.”
The design that produced this lane rejected the obvious shortcut by name: handing Supabase
service-role credentials to an agent sandbox “grants RLS-bypassing power to every session;
rejected as anything but a consciously accepted stopgap.” The network policy follows from
that — “Agent environments must allowlist the app host; nothing else (no Supabase host,
no DB port) — that is the point.” On a managed-as-code ontology, remote
apply requires an ontology admin; import_merge stays refused
there entirely.
JSON, YAML, or RDF Turtle (T-box and A-box, IRIs under NAI_ONTOLOGY_BASE_IRI) with portable canonical ids, optionally with instances.
Create a new ontology from a file on the home page, or merge a file into an existing one; identity-preserving when the ids match. “Merge never deletes — removals go through the code workspace's plan / apply.”
Hop chains over the instance graph, optionally filtered on a target property, ending in a property read and optionally an aggregate: count, sum, min, max, avg, first, concat. “The walk is bounded by the number of written hops — a cyclic graph cannot loop it.”
Library functions in JavaScript, “executed server-side in a QuickJS sandbox with hard limits: 250 ms of wall clock, 32 MiB of memory, a 200 KiB guest stack, and at most eight nested function calls.” One host function is injected and then deleted from the global scope.
Run a function from the editor with typed parameters and an optional instance context; see the result and how long it took.
Model- and instance-scope rules producing error/warning/info findings with entity links. Save-time runs happen “after the write commits and never undo it”.
Six groups — Model · Categories · Logic · Instances · Views · Admin — as a rail on wide screens and a collapsible menu on small ones. “The sidebar mirrors the language”: the three model-side groups are exactly the seven top-level statement kinds of the .nai model lane.
One URL-driven filter vocabulary across all tables and both graphs — text with wildcards and synonyms, kinds, dimensions, tags, expressions, property values — so “a filtered view is a link”. Malformed input is ignored, never thrown on.
Full EN/DE interface, localized entity names and descriptions, coded errors translated at the UI edge with ICU catalogs — including sandbox timeouts and script failures.
“A help panel on every page opens the topic for the screen you are on; /help renders all topics in one document.”
Light and dark mode, Tailwind v4 design tokens, Radix primitives.
Three mechanisms, deliberately not conflated: archiving (ontologies only, reversible), soft delete with dependency guards (classes, relations, instances), and hard delete behind a two-step confirmation — with RLS as the outer wall.
%%{init: {'theme':'neutral', 'flowchart':{'htmlLabels':false,'nodeSpacing':45,'rankSpacing':55}}}%%
flowchart TD
W["Web app<br/>signed-in Supabase session"] --> SA
C["nai CLI (remote mode)<br/>NAI_REMOTE_URL + NAI_TOKEN"] --> MCP
A["MCP agent<br/>bearer token nai_..."] --> MCP
subgraph L1 ["deployed app on Vercel"]
SA["Server actions<br/>the web write path"]
MCP["POST /api/mcp<br/>stateless streamable HTTP<br/>scope decides the tool list"]
OPS["One server-side contract<br/>same operations, permission checks,<br/>payload caps, refusal reasons"]
RB["rbac.ts matrix<br/>admin / editor / reader"]
end
SA --> OPS
MCP --> OPS
OPS --> RB
RB --> RLS
subgraph L2 ["Supabase Postgres"]
RLS["Row Level Security<br/>coarse membership wall<br/>non-member reads as not found"]
DB[("20 migrations<br/>0001_core to 0020_ontology_header_fields")]
end
RLS --> DB
classDef orch fill:#dcefe9,stroke:#0f6f63,stroke-width:2px
classDef tool fill:#f4f1ea,stroke:#8a7a5a
classDef human fill:#fdf3e0,stroke:#8a5a00,stroke-width:2px
class W,C,A human
class SA,MCP orch
class OPS,RB,RLS,DB tool
src/lib/domain/rbac.ts, “because the matrix is the part that will churn with
product decisions and should not live in migrations”. Full detail:
docs/overview.html and docs/remote-modeling.md in its own
repository.Structurally it is still the same shape as
nai-analysis: Next.js on the frontend, Supabase
(Postgres + Auth) as the sole backend, no agent orchestration inside the app. What changed
is the direction of travel: the app now serves agents rather than containing one.
“Next.js 16 App Router / React 19, with server actions as the write path — the only
outside API is the MCP endpoint.” Its design record is one specification and one implementation
plan per phase under docs/superpowers/, the same phase-based discipline as
nai-analysis and not this workspace's orchestrator/subagent/tool pattern — there
is no command file and there are no agent files for it anywhere.
Next.js 16 App Router with React 19, deployed on Vercel from main.
Postgres and Auth (email + password, Google OAuth), run locally via the Supabase CLI stack and hosted in production; “RLS on every table”.
quickjs-emscripten for the server-side script sandbox.
React Flow and ELK for the model and instance graphs; CodeMirror for the script editor and the Text view; react-markdown with sanitization; N3 for Turtle.
ICU message catalogs for interface text and coded error messages; light and dark mode.
Two configurations: 106 unit test files (jsdom for components) and 30 integration test files against the real local Postgres.
There is one, and it is documentation rather than operation.
/nai-document keeps a product map, and its first row
is | `nai-onto` | `docs/architecture/product-nai-onto.html` | `github.com/udopnink/nai-onto` |
— this page, tied to the repository it is surveyed from. That row is the seam in its entirety,
and the page you are reading is its output. The command's rule for that branch says so itself:
“the product's own repository is the source, and it is not this repo. There is
nothing here to survey: no command file, no agent files, and the code is somewhere else
entirely.”
It runs in one direction only. The sources are “that product's own as-built documentation,
in a local checkout of its repository: its README.md, its overview and
feature pages under docs/, and its design specs”, and “The agents read those
paths and write, as always, only inside this repo.” Nothing here reaches the running
application: the command “uses no web tools, and neither does any agent it dispatches”,
and “It never runs an agent system to find out what it does. Everything comes from reading
files.” The deployment at nai-onto.vercel.app, the /api/mcp endpoint
described above, and the database behind both are all outside what a documentation run can touch.
“A product page documents someone else's building. Every claim on it ages the moment that repository moves, which is why the product branch insists on a checkout and a commit rather than letting the run proceed on whatever is nearest to hand.” Hence the 1bc7439 in the footer.
It said this app had “no seam to this workspace at all — no slash command reads it, writes to it, or knows it exists.” One slash command now reads it — to regenerate this page, and for nothing else.
“workspace_counts (migration 0021), sidebar badges, detail-page cross-links + new facets, jump palette.” The shipped schema stops at 0020_ontology_header_fields, so none of these is live yet. The ontology home page's kind cards with counts are a different thing and did ship, with F1.
The remote-modeling spec's status line records that “rider 2 (OAuth) and per-token rate quotas remain open”. Personal access tokens are the whole authentication story for the CLI and MCP today.
The in-app title still reads “NAI Ontology”. Both the README and the overview state this deliberately: it is a known-pending rename, not drift.
F3 was “Merged to main 2026-09-03 (104c13d, fast-forward); migration 0020 must be applied to production with the deploy.”
“There is no sign-up, password reset, or magic link, and no way to create an account from the app; the Google provider must be configured on the Supabase project.”
Create-or-update only. “Deleting through a file requires the code workspace's plan / apply. Excel/CSV and general third-party OWL ingest are out of scope.”
Guarded model writes are transactional; “other multi-step imports are still non-transactional and report partial writes.”
At most 50 ontology cards on the home page; derived columns stop evaluating above 200 visible rows; the instance graph refuses past 300 nodes; one export carries at most 50,000 instances — “a refusal rather than a truncated file.”
“Soft-deleted classes, relations, and instances cannot be restored from the UI — only archived ontologies have a restore path.”
expr filterPer-hop filters and aggregates “parse but are rejected as unsupported in that context, even though the same syntax works in a query property.”
A delete is blocked by key alone, without owner scoping, so two owners sharing a property key can block each other. “Deliberate: a spurious block is safe, a silently broken expression is not.”
unmanage over the wire“It is an admin action in the app so that the divergence between files and database is always a logged decision.”
.nai is not a web transfer format“It lives in the CLI and the Text view” — not offered as a fourth web export/import format.