App System
Status: ✅ Decided (grill session, 2026-07-04) — not yet implemented; this page is the contract the implementation workstream builds against (backlog:
TODO.md§ App layer). Companions: Surface System (the document layer this composes; this page amends its D18/D19), Surface Platform (SUR-1..14), and the vision pages The Everything App and Malleable Software (the L0–L4 authoring ladder apps must serve). Foil: Palantir Foundry (Workshop / Carbon / Marketplace / Object Explorer) — researched 2026-07-04; its validated lessons are cited inline and collected in the foil.
The thesis
Vinxi is an operating system, so it runs many apps: what a site sees depends on which packages its Universe installs, and each app brings its own screens, tools, and shortcuts. An app is the launchable unit — a named, entitled, slug-mounted composition of Surfaces. The layering is strict:
- Packages install (the distribution + entitlement unit, ADR-0024),
- apps launch (the navigation + identity unit, this page),
- Surfaces render (the content unit, Surface System),
- and one URL grammar addresses all of it: path = identity, query = state.
Apps are documents, not code — the same doctrine as Surfaces. A package ships app documents; a user or partner authors one in the studio; the shell treats both identically.
Decision record
Format: decision → rationale → consequence. All decided 2026-07-04.
A1 · An app is a layer between package and Surface
An app is a first-class object between the package (install unit) and the Surface
(content unit): a package provides one or more apps; an app composes Surfaces. It is
userland/shell composition declared as data — the kernel never grows an “app” concept
beyond the registry documents (D17’s “frontend modules/apps are userland composition”
stands). The word is “app”, unhedged: buyers purchase apps, partners build apps, users
launch apps; the vision canon already committed (“field workflows are simply the first
‘apps’ that run on an OS-grade shell”). Alternatives rejected: module (retired here, and
Workshop’s name for its apps — confusing in competitive conversations), program (kernel-pure
but wrong in every buyer-facing sentence), workspace (taken by the IR node).
A2 · URL grammar: path is identity, query is state
/<app-slug>/<surface-slug>/<subpage>/<subpage>…?<dials>/ → the launcher app/<app-slug> → the app's declared home SurfaceThe path answers where you are (app → Surface → drill-in chain); the query answers what
state it’s in — the existing dial codec unchanged (f= a= t= l= ar= v= d.<id>=,
defaults elide). Consequences:
- Subpages are promoted into the path. The subpage chain (D18) moves from the
pagedial (p=a/b/c) to path segments; the resolver’s chain semantics (graceful tail truncation, breadcrumb, deepest-active resolution) are unchanged — only fed from the path. The reservedpagedial retires; barep=leaves the codec. - Parallel subpage stacks get the reserved query namespace
p.<region>=<chain>— a secondary navigable region (inspector, aside, overlay) with its own independent chain, sharing the Surface’s dials (e.g.?p.inspector=cable-42/history). Reserved now, designed when the first two-stack screen ships. - This resolves the Lexicon’s route-grammar residual completely: one URL grammar for the whole OS, no per-app route tables, no authored routes anywhere.
A3 · Flat app namespace; slug is presentation, id is identity
- Apps are a flat namespace — exactly one path level, never nested. Structure inside an app is Surfaces + subpages; structure across apps is the launcher. The launcher is a peer app, not a parent.
- App id (e.g.
networkaccess.fiber) is the stable global identity — used by entitlement, references,openIn, config. Slug (e.g.fiber) is the URL mount the app declares, defaulting to the id’s last segment; slugs are globally unique per install (collision = loud install error, same doctrine as duplicate Surfaces). Renames ship analiases: []list — old slugs redirect, bookmarks and field-printed QR codes survive. Tenant-side slug overrides (white-label mounts) are a declared install-time extension, deferred; the id, never the slug, is the portable identity. /is the launcher — itself a built-in app (A11), a plain Surface querying installed × entitled apps. Deep links go straight to their app, never bounce through the launcher. A tenant may configure a default landing app (per-capability-group defaults are a noted door, after Foundry’s per-group homepages); the default is the launcher./<app-slug>renders the app’s declared home Surface directly — no redirect; the clean URL is the app’s home.
A4 · Apps declare rosters; nav derives from roster × capabilities
The relationship inverts D19’s grain: the app composes a roster of Surfaces by reference; Surfaces stay pure content with global ids. Each roster entry carries the app-local slug (what appears in the path) and the nav metadata (label, icon, order, capability gate, form factors). Consequences:
- Clean app-local URLs —
/fiber/noc, not/fiber/networkaccess.noc. - Surfaces are reusable across apps: a shared map/settings/admin Surface can be rostered by several apps, each assigning its own label and position.
modulesretire. The cross-package nav bucket (D19’sprovides.modules, merged by id) is superseded: the app roster is the nav grouping. In-app “sections” (sidebar sub-grouping inside one app) are a deferred roster extension; v0 rosters are flat.- Nav stays derived, never authored — now
f(roster × session capabilities)per app, plus the launcher levelf(installed apps × capabilities). Capability-gated hiding is unchanged from D19. - Cross-app entity navigation stays computed and nearly free: data is one shared graph,
apps are views over it, so “Open in Ticketing” is a path change carrying the same
app-agnostic
f=focus dial.openIn(entityKind)returns(app, surface)pairs, preferring the current app’s roster (the same curation Foundry’s Carbon applies to its “Open In” targets).
A5 · History and deep links: portable, form-factor-invariant, never blank
- One undoable user intent = one history entry. App switch, Surface switch, subpage
drill-in, opening a parallel region, discrete dial change →
pushState; continuous adjustment (viewport pan/zoom) → debouncedreplaceState. D8’s model, extended to the path. - URLs are form-factor-invariant. A deep link encodes logical state; the shell decides
rendering (desk shows
p.inspectorside-by-side, phone collapses it to a pushed sheet; back pops it either way). One link works on every device. The rejected alternative — promoting phone overlays into path segments — buys a purer native back-stack at the cost of non-portable links. (Foundry forks separate mobile modules per app; we refuse the fork entirely.) - Cold-start degrades to the deepest valid state, never blank: unknown app slug → launcher, unknown Surface slug → app home, unknown subpage tail → deepest valid prefix (already implemented for chains). Loud at install, graceful at render — now for the whole path.
- Every app state is canonically URL-addressable by default. This is a competitive claim, not hygiene: Workshop’s deep-linking is opt-in, partial, and load-time-only (“changing the URL after the module has loaded does not dynamically update variable values”), producing wrapper-module folk workarounds. Vinxi’s posture is the direct counter.
A6 · The app document
An app is its own diffable, agent-authorable document — *.app.json — listed by (not
inlined in) the package manifest:
{ "app": "networkaccess.fiber", // global id = identity "type": "surfaces", // discriminant; "code" reserved (A12) "slug": "fiber", // URL mount; defaults to id's last segment "label": "Fiber", "icon": "cable", "home": "noc", // app-local slug rendered at /fiber "aliases": [], // prior slugs, redirected (A3) "commands": [], // reserved: command-bar (⌘K) contributions "surfaces": [ { "slug": "noc", "surface": "networkaccess.noc", "formFactors": ["desk", "phone"], "nav": { "label": "Network Ops", "icon": "activity", "order": 1, "capability": "noc.reroute.approve" } } ]}The package manifest’s provides gains apps and loses modules; the per-surface nav
block moves off SurfaceEntry onto roster entries. The roster resolves against the union
of all installed Surfaces — an app may roster a Surface another package ships (same
no-requires, deps-derived-from-refs doctrine as D17/D19); a roster entry naming an
absent Surface fails install loudly.
A7 · Routability and the canonical-app rule
A Surface no app rosters is unroutable — there is no bare-surface URL form (the playground/studio can still open any document). When several apps roster the same Surface: links always carry the explicit app; where the system must choose (openIn fallback, notifications), prefer the app from the Surface’s own package, then roster order.
A8 · Entitlement is installation
“You bought it” means “your Universe installs it.” No subscription/license primitive exists in the kernel or shell: a bundle is a factory-side named set of packages (ADR-0024 — sites pull governed packages), the launcher simply shows installed apps × the session’s capabilities, and per-user differences within a tenant remain capabilities (D7). Enforcement is honest — what you didn’t buy is not latent behind a flag; it isn’t there. In-product upsell/trials, if ever wanted, are a launcher feature fed by a catalog outside the Universe — never a runtime entitlement check.
A9 · Registry-first: apps, Surfaces, and realms are kernel documents
The kernel-side document registry is the design target now: app documents, Surface
documents, and realm schemas are registered documents — graph citizens — and
install/publish/uninstall are kernel Acts (audit + rollback native; Foundry lacked
product uninstall until 2025 — ours is a free consequence). Every document carries an
origin: bundled (arrived in a factory package, the files in apps/*/surfaces/ are
just the transport the factory replays into the registry) or local (authored in this
Universe — the malleable-software path made real). No shell feature may be designed that
only works with files. Corollary: installed apps are entities (system.App), so the
launcher is a plain Surface querying the graph — no special machinery.
A10 · Vendor documents are immutable; tenant customization is an overlay
A tenant never edits a bundled document. Customization (reorder nav, relabel, add a local
Surface to a vendor app’s roster) is a separate overlay document that patches the
roster — config, not forks (ADR-0024). This is the direct counter to the foil’s sharpest
pain: Foundry upgrades overwrite local edits (“these edits will be overwritten by the
content in the new product version”) and unlocking an installation is officially framed as
forking. Package upgrades replace the vendor document and re-apply the overlay; overlay
conflicts (patched entry no longer exists) degrade gracefully and report loudly. Overlay
mechanism design lands with the registry build; the principle is settled.
A11 · Lifecycle: draft → published now; proposals later
Registry documents are draft (author-visible only — their studio, their launcher’s
drafts shelf) or published (tenant-visible, modulo nav capabilities). Publishing is
one Act; editing a published document creates a draft revision; publish swaps the
pointer; prior revisions are retained (Acts make history free). Whether a user may publish
at all is one capability — registry.publish — granted narrowly or broadly per tenant.
There is deliberately no team-only visibility tier in v0. The upgrade path to
review-gated publishing is ADR-0017 proposals (“proposal to publish”) composing on
top of this two-state model, never replacing it. Two studio conveniences noted from
Workshop’s mature flow: an auto-publish-on-save toggle, and a visual changelog diff.
A12 · The system namespace, and one namespace only
The OS namespace is system — package id system, realm system, app ids
system.launcher|studio|console|reality|explorer, kinds system.App, system.Package,
system.User, system.Site. The install gate reserves system, sys, and vinxi:
non-factory packages claiming any of them (as package id, realm, or app-id prefix) are
rejected loudly. sys exists only as a rejected alias, never a live one — two spellings of
the OS namespace would be a permanent “is it sys.User or system.User?” tax. This
amends the site & identity decision record (2026-07-04, parallel workstream), which
named its kernel-native realm sys (sys.Site, sys.User): that design stands in full,
spelled system.*. (The kernel’s HTTP route family /sys/act/…, /sys/query/…,
/sys/errors is a separate, already-implemented wire convention cited by ADR-0014 — left
as-is; renaming it would be an ADR-0014 supersession, not a side effect of this page.)
An app’s type is a closed discriminant: surfaces (a roster, everything above) —
and code, reserved: a custom bundle mounted at a slug with the same identity,
entitlement, and nav treatment (the OSDK-style escape hatch; first user is our own studio,
below). Reserved now so the escape hatch never needs a schema break.
A13 · The built-in apps: the system package
Built-ins are an ordinary package the factory pulls into every Universe — same manifests, registry, Acts; dogfood, no special machinery — with one extra property: non-removable (the factory always includes it). Capability gates do all per-persona hiding.
| App | Slug | What it is | v0 |
|---|---|---|---|
system.launcher | / | The grid/sidebar of entitled apps — a plain Surface over system.App | ✅ |
system.studio | /studio | The authoring studio — the playground grown up: edit/draft/publish apps & Surfaces, builder agent. The first type: "code" app | ✅ |
system.console | /console | Admin: installed packages, registry state, publish rights | thin, read-only |
system.reality | /reality | The ontology workbench — browse the merged schema snapshot (entity types, traits, links, derivations per realm); later, edit by drafting a new SchemaDocument version installed through the existing Act (schema versions are already immutable kernel-side) | read-only browser |
system.explorer | /explorer | Canonical entity viewer — when built, ships a zero-config default view for every entity type, with custom Surface-backed views layered non-destructively on top (the Core-Object-Views pattern) | slug reserved |
“Reality” is the established canon word for the ontology bundle (realities/,
vinxi reality install) — no synonym (datamodel, model) is introduced. The
studio-as-app call is deliberate: if the app-maker can’t be expressed as an app, the model
has a hole.
Per-user shell state — pinned/recent apps in the sidebar — is a fourth state category
(not dials, not install, not capability): per-actor preferences, shell-owned;
localStorage in v0, kernel-asserted later.
A14 · Empty-is-loud
Separating app/Surface visibility (capabilities) from data visibility (query results)
creates the silent-blank-screen failure mode — bad enough that Workshop ships a dedicated
“Check access” diagnostic panel for it. Doctrine here: a shell must always be able to
explain why a view is empty. The resolved tree already carries the machinery (the
dropped-conditions X-ray); the diagnostic affordance lands in the studio/inspector first,
and no shell may render an empty state it cannot account for.
The Palantir foil
Researched 2026-07-04 (Workshop, Slate, Carbon, Marketplace/DevOps, Object Explorer, OSDK — official docs + community threads). What it settled:
| Foundry finding | Vinxi posture |
|---|---|
| Workshop URL state: opt-in, partial, load-time-only; object sets encode as single RIDs; wrapper-module workarounds for stable URLs | Every app state canonically URL-addressable by default (A2/A5) — the win condition |
| Carbon workspace = curated module collection, per-group defaults, “Open In” scoped to the workspace’s modules | Convergent with A4 rosters + A3 launcher; per-group default app noted as a door |
| Marketplace upgrade overwrites local edits; unlock = fork; staff guidance: don’t edit installed resources | A10 overlays — vendor-immutable + tenant config is the differentiator |
| Workshop save/publish: versions, revert, changelog diffs, publish-designates-viewer-version | A11 adopts the shape; auto-publish toggle + visual diff noted for the studio |
| Every object type gets a default view + canonical URL; custom Workshop-backed views layer non-destructively | A13 system.explorer: zero-config default view per entity type, Surface-backed custom views |
| Separate mobile modules per app; desktop widgets disabled on mobile; separate mobile launcher | Rejected — one URL, one document, shell adapts (A5) |
| Module-ACL vs data-ACL split → silently blank widgets → dedicated “Check access” panel | A14 empty-is-loud doctrine |
| Slate: free-form HTML/JS aged into a decade of sandboxing and soft-deprecation; survives only for public apps | Vindicates IR-not-code; public apps noted as a door; escape hatch is the governed type: "code" tier (A12), not free-form documents |
Ontology collisions across products silently renamed (Truck_1); no overlap tooling; uninstall shipped 2025 | D17 loud reference validation already prevents; uninstall is an Act from day one (A9) |
| Marketplace install-time inputs (stub resources, column mapping) | The well-designed version of install-time package config — parked door, shape known |
Build order
Phase A — schema + router (surface-runtime). App document schema + parse/install gates
(provides.apps, roster union-resolution, slug uniqueness, reserved namespaces); path
spine /<app>/<surface>/<chain> feeding the existing subpage resolver; page dial
retired; launcher + shell nav derived from rosters. Fixtures migrate off modules. Done
when: the networkaccess + trafficure fixture packages install as two apps, the launcher
lists them, and every golden-fixture state round-trips through the new URL grammar.
Phase B — registry (kernel). system.App/system.Package/document kinds; install/
publish/uninstall Acts; origin bundled|local; draft/published lifecycle +
registry.publish; factory replay of package files into the registry; frontend install
gate becomes a projection consumer. This is the “file swap, not machinery” moment the
manifest schema header promised.
Phase C — the system package. Launcher as a Surface over system.App; playground
migrates to system.studio (first type: "code" app); thin system.console; read-only
system.reality. Overlay documents (A10) land here with the first real customization ask.
Residuals (parked, not decided)
| # | Item | Re-entry point |
|---|---|---|
| AR1 | Parallel-region (p.<region>) full design | First real two-stack screen |
| AR2 | In-app roster sections (sidebar sub-grouping) | First app whose flat sidebar overflows |
| AR3 | Embedded Surfaces with dial mapping (Workshop’s embed-with-interface, Loop) | First dashboard-composition ask; relates to R3 |
| AR4 | Tenant slug overrides / white-label mounts | First white-label deployment |
| AR5 | Public/unauthenticated apps (capability-less session at a slug) | First public status-board ask; Slate’s one retained superpower |
| AR6 | Per-capability-group default landing app | Operator feedback post-launch |
| AR7 | Install-time package configuration (inputs/stubs) | First package needing site-specific binding at install |
| AR8 | commands semantics (⌘K contributions) | Command-bar workstream |
| AR9 | type: "code" substrate (sandbox, mount contract) | system.studio migration forces the minimum; partner code apps set the rest |
| AR10 | Team-scoped visibility between draft and published | ADR-0017 proposal flow (A11’s door) |
| AR11 | Overlay document mechanism (A10 principle → schema) | Phase C, first customization ask |
Naming note
App (this page) is the product concept — a launchable composition of Surfaces. The
repo’s apps/ directory (apps/network-access, apps/book) contains Vite frontends —
build artifacts, not Vinxi apps; the collision is dev-facing only and tolerated. Per the
naming canon: Vinxi is the kernel, Lepton the company; the OS namespace is system and
sys/vinxi are reserved-rejected (A12).