Architecture

Contracts first, rendering second

The framework's value is not its components. It is the set of contracts every Ottili product agrees to: how a session resolves, how a company is proven, how a manifest declares a route, how a policy becomes a rendered state and how an action reaches the backend.

Bootstrap · SoT §10

One deterministic startup path

Every Foundation product boots the same way. The order is not a suggestion — rendering the shell before the company is proven is the bug the sequence exists to prevent.

1

Session resolve

The app asks Ottili Auth who the user is. No token, no bootstrap — the shell renders the unauthenticated state instead of a half-populated dashboard.

2

Company resolve

The company in the URL is validated against the verified token server-side. A slug in the path is a hint, never an authority.

3

Bootstrap payload

One call to /api/v1/foundation/bootstrap returns product manifests, navigation, widgets, commands, settings, capabilities, extensions, preferences, customization and the context revision.

4

Contribution merge

Product, extension and platform contributions are merged deterministically. Conflicts are resolved by declared precedence, not by import order.

5

Feature-state resolution

Permission, entitlement and rollout collapse into one feature state so the UI renders visible, hidden, locked, upgrade-required or degraded consistently.

6

Shell render

Only then does the shell mount, already themed by the company's published customization — no flash of the default brand.

GET /api/v1/foundation/bootstrap { "context": { company, product, locale, revision }, "session": { state, expiresAt, accounts }, "manifests": [ productManifest, ... ], "navigation": [ group, ... ], "widgets": [ widget, ... ], "commands": [ command, ... ], "capabilities": { ai, files, flows, approvals, ... }, "extensions": [ installation, ... ], "customization": { effective, revision }, "preferences": { theme, density, locale, timezone } }

One request, one revision number. When the revision changes — a company switch, a published branding change, an installed extension — caches invalidate together instead of drifting apart.

Unified API · /api/v1/foundation

32 routes carry the whole runtime

Foundation does not own a second backend. It speaks to the Unified API like every other Ottili consumer, through a generated client with response validation, cursor pagination, SSE and streaming uploads.

GET · 19PATCH · 2POST · 9PUT · 1DELETE · 1
MethodRoute
GET/api/v1/foundation/bootstrap
GET/api/v1/foundation/products
GET/api/v1/foundation/companies
GET/api/v1/foundation/manifest-revisions
GET/api/v1/foundation/navigation
GET/api/v1/foundation/widgets
GET/api/v1/foundation/commands
GET/api/v1/foundation/settings
GET/api/v1/foundation/capabilities
GET/api/v1/foundation/extensions
GET/api/v1/foundation/help/{page_id}
GET/api/v1/foundation/translations/{locale}
GET/api/v1/foundation/preferences
PATCH/api/v1/foundation/preferences
GET/api/v1/foundation/customization/effective
GET/api/v1/foundation/customization/draft
PATCH/api/v1/foundation/customization/draft
POST/api/v1/foundation/customization/preview
POST/api/v1/foundation/customization/publish
POST/api/v1/foundation/customization/rollback
GET/api/v1/foundation/customization/history
POST/api/v1/foundation/customization/export
POST/api/v1/foundation/customization/import
GET/api/v1/foundation/dashboards/{dashboard_id}
PUT/api/v1/foundation/dashboards/{dashboard_id}
GET/api/v1/foundation/favorites
POST/api/v1/foundation/favorites
DELETE/api/v1/foundation/favorites/{favorite_id}
GET/api/v1/foundation/recents
POST/api/v1/foundation/recents
POST/api/v1/foundation/actions/{action_id}/preflight
POST/api/v1/foundation/actions/{action_id}/execute

Product manifest · SoT §9

A product declares itself, it does not wire itself

Routes, navigation, widgets, commands, settings and capability configuration are declared. The runtime resolves them, so navigation, search, the palette and the Workbench all see the same truth.

export const hqManifest: ProductManifest = { productId: "hq", name: "Ottili HQ", accent: "#7C3AED", routes: [ { id: "hq.customers", path: "/customers", permissions: ["hq.customers.read"] }, ], navigation: [ { id: "hq.nav.sales", labelKey: "hq.nav.sales", routeIds: ["hq.customers"] }, ], widgets: ["hq.open-items", "hq.revenue"], commands: ["hq customers list", "hq deal create"], capabilities: { ai: { profile: "hq.default", toolNamespaces: ["business.*"] }, files: { roots: ["hq"] }, flows: { nodePacks: ["hq.core"] }, }, };

Non-negotiables

Rules the framework enforces

RuleWhy it exists
Company scope comes from the verified tokenInferring it from the first membership or a URL segment is how cross-tenant leaks happen.
Frontend permissions are never authoritativeThey exist to render the right state. Every action is re-checked server-side before it executes.
localStorage is not a session or tenant authorityIt is a convenience cache for preferences. Losing it must never change who you are or which company you see.
Product code never reads raw palette valuesSemantic tokens are the only interface, which is precisely what makes deep white label possible.
Third-party backend code never runs in the Unified API processExtensions get a sandboxed UI and scoped RPC, not a foothold in the platform runtime.
Audit records are produced server-side onlyA compliance history a client can write is not a compliance history.

Forbidden · SoT §42

Anti-patterns that fail review

Written down so they can be pointed at, rather than rediscovered in a post-mortem.

Do not

  • localStorage as session or tenant authority
  • Raw palette values in product code
  • Product code reaching into another product's database
  • Frontend permissions treated as authoritative
  • Duplicating Console or Auth inside a product
  • Unrestricted SQL or shell behind a customer-facing surface
  • Circular dependencies between Foundation packages
  • Copying the shell instead of consuming it