NAI OS

NAI Onto — the one you can model as code

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.”

Where it stands

Production

“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.

Phases complete

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.”

Schema

20 migrations under supabase/migrations/, 0001_core through 0020_ontology_header_fields.

Tests

106 unit test files and 30 integration test files (Vitest); “integration tests run against the real local Postgres, not mocks.”

Access & collaboration

Three roles

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.

The 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.

Platform admins

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.

Sign-in

Email + password or Google OAuth through Supabase Auth. “There is no self-service sign-up; accounts are provisioned in Supabase.”

Access tokens

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.

Comments & issues

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.”

Ontology management

Multiple ontologiesF3 · 260903

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.

Home page

/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.

Ontology overview

“Each ontology opens on an overview page: kind cards with counts, a status strip, and the header fields (description, source, keywords, notes).”

Archiving

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.

Filter-gated bulk actions

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.

Modeling

Classes & inheritance

Single-inheritance hierarchy plus mixins; feature exclusion per class; cycle-safe by construction. What a class effectively has is computed rather than stored.

Relations

Directed, typed relations between classes with their own properties; effective for the source hierarchy, subclass-conformant targets.

Properties

Stored (string, number, boolean, date, enum; single or multi cardinality; required) and derived kinds — path, query, and scripted, computed on read and never persisted.

Dimensions & classifications

Orthogonal classification dimensions with ordered values, assignable per class.

Tags

Ontology-scope and instance-scope tags, assignable to classes, relations, classifications, and instances.

Mentions & backlinks

Markdown descriptions with [[class:…]], [[relation:…]], and [[instance:…]] mentions; automatic backlinks in both directions.

Stable identity

“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.

Instances

Nodes

Per-class tables of instances with stored and derived (ƒ) property columns, tag chips, property-value filters, and pagination.

EdgesF2 · 260902

“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.

Dynamic forms

Create/edit forms generated from the class's effective stored properties — typed inputs, multi-value row editors, required enforcement, localized errors.

Links

Typed instance-to-instance links per effective relation, with link-level property values and conformance guards on every write.

Derived values

Path and query expressions evaluated over the live instance graph; scripted properties run in the sandbox. “Errors render inline — never break the page.”

Views

List

“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.”

Graph

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.

TextF2 · 260902

“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.

Ontology as code

The .nai DSL

A 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.

check / plan / apply

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

“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.”

Managed lock

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.

Text Drift

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.

Seedshipped 260829

“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.”

Translations

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.

Remote modeling and agents

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.”

ScopeAddsWhat that lane is
readlist_ontologies, export_modelPull the live model into a workspace.
writeimport_merge, seedAdditive bulk on-ramp for model and instances. Never deletes.
operatorplan, apply, manageThe 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.

Transfer

Export

JSON, YAML, or RDF Turtle (T-box and A-box, IRIs under NAI_ONTOLOGY_BASE_IRI) with portable canonical ids, optionally with instances.

Import

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.”

Evaluation & scripting

Expression engine

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.”

Script sandbox

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.

Function console

Run a function from the editor with typed parameters and an optional instance context; see the result and how long it took.

Validation rules

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”.

Everywhere

Grouped sidebar

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.

Filtering

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.

Localization

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.

Help

“A help panel on every page opens the topic for the screen you are on; /help renders all topics in one document.”

Theme

Light and dark mode, Tailwind v4 design tokens, Radix primitives.

Safety

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.

How it's built

%%{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
Three clients, one contract. The enforcement split is deliberate: RLS carries only the coarse, membership-based wall, and the fine role matrix is portable app-layer TypeScript in 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 / React

Next.js 16 App Router with React 19, deployed on Vercel from main.

Supabase + RLS

Postgres and Auth (email + password, Google OAuth), run locally via the Supabase CLI stack and hosted in production; “RLS on every table”.

QuickJS

quickjs-emscripten for the server-side script sandbox.

Graphs and editors

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.

next-intl / next-themes

ICU message catalogs for interface text and coded error messages; light and dark mode.

Vitest

Two configurations: 106 unit test files (jsdom for components) and 30 integration test files against the real local Postgres.

The seam to this workspace

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.

Why the commit is stamped

“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.

What the previous version said

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.

Designed, not yet shipped

F4 — Orientation

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.

OAuth for tokens

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 rename

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.

Migration 0020 on production

F3 was “Merged to main 2026-09-03 (104c13d, fast-forward); migration 0020 must be applied to production with the deploy.”

Known limitations

Sign-in only

“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.”

Merges and seeds never delete

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.”

Partial writes on import

Guarded model writes are transactional; “other multi-step imports are still non-transactional and report partial writes.”

Display caps

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.”

No restore for soft deletes

“Soft-deleted classes, relations, and instances cannot be restored from the UI — only archived ontologies have a restore path.”

Navigation-only expr filter

Per-hop filters and aggregates “parse but are rejected as unsupported in that context, even though the same syntax works in a query property.”

Key-scoped delete blocks

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.”

No 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.

NextPrestolex