Skip to content

Template Rules (for AI) ​

This project is built to be maintained largely by an AI assistant. This page is the contract the AI follows when it writes code in a Host or a Remote. An app is written in one of the ecosystem's template languages β€” Vue or Nuxt β€” declared by the type field in its mono.config.ts, and part of these rules depends on which one you're in. The rules are grouped into:

  • Common Rules: General β€” language-neutral; apply to every app, Host or Remote.
  • Common Rules: Vue / Nuxt β€” one section per template language. Read only the one matching your app's type.
  • Host template β€” what the AI may / may not do in the Host.
  • Remote template β€” what the AI may / may not do in a Remote.

Start here and follow the General rules on every change, plus your language's section.

New to the repo? Setup shows how a Host (Vue or Nuxt) and a Remote are wired together. This page is the day-to-day contract for writing code inside them.

The General rules are ordered as a learning path: understand the machine (1–4) β†’ shared config (5) β†’ naming conventions (6) β†’ environment (7) β†’ where code & data live (8–10) β†’ capabilities: pick a component flavor, fetch, type, and build UI (11–15) β†’ put it together and build a feature (16) β†’ remember what you learned (17). Read top-to-bottom once, then read your language section top-to-bottom once; after that, route by the tables below.

How to use these rules ​

Don't read everything every time. Identify your language first, then route by what you're doing, obey the hard prohibitions, and stop to ask at the marked decision points. Every rule below also opens with a When / Do / Don't lead so you can scan it in one line.

Pick your language first ​

Open mono.config.ts and read the top-level type. It decides which second section you read β€” and where this app's source lives:

type in mono.config.tsThe app isSourceRead
'vue'a Vue + Vite app β€” a Vue Host, or any Remote (a Remote is always Vue)src/General + Vue
'nuxt'a Nuxt app β€” only a Host can be Nuxtapp/General + Nuxt

Rules are numbered by section: General rules are plain numbers (Rule 12), Vue rules are V-prefixed (Rule V1), Nuxt rules N-prefixed (Rule N5). A bare "Rule n" anywhere on this page always means a General rule.

Don't read the wrong language section

The Vue and Nuxt sections contradict each other on purpose β€” different build config, different source folder, different mono-* component build. Applying a Nuxt rule in a Vue app (or the reverse) will produce code that doesn't build. Check type before you start.

Start here β€” intent β†’ rule ​

You're about to…Read
Build a feature / page16 (+ 8, 11, 12, 14)
Name a variable, function, or handler args6
Consume an OData URL / entity13
Fetch data (REST or OData)12
Get one record / detail from a DataSource12 (reuse the source β€” store().load({ take: 1 }), no 2nd fetcher)
Import across apps, or from datas / types / odata2
Create a store or composable8
Add static / constant data9
Add a global helper10
Need validation / notif / Excel / JSON helpers15
Add an API endpoint, cookie, JWT, or menu entry5
Handle a secret / base URL / port7 (+ V6 / N6 for the port & envPrefix)
Pick a light-DOM vs shadow mono-* component11 (+ V5 / N5)
Build UI14
Start complex work, or just finished meaningful work17 (when MONO_SKILLS is on)
Touch the build config (vite.config.ts / nuxt.config.ts)1 (+ V1 / N1) β€” and ask first
Work out where a folder lives (src/ vs app/)V2 / N2 (+ 2)
Register createMono / bootstrap the appV4 / N4

Never do ​

  • Install an external HTTP library (axios, ofetch, ky, raw fetch wrapper) β€” Rule 12.
  • Build a second fetcher for a row you already hold a DataSource for β€” reuse the source (source.store().load({ take: 1 })), don't call a fresh monoOdataFetch β€” Rule 12.
  • Edit anything in .mono/apps/ β€” Rule 3.
  • Hand-edit generated files (odata/DTO/, .mono/tsconfig.json, and your language's generated output β€” Vue: auto-imports.d.ts / components.d.ts / typed-router.d.ts; Nuxt: .nuxt/) β€” Rules 4, V3, N3.
  • Hand-write OData entity types β€” generate them β€” Rule 13.
  • Reuse a defineStore key or an export name across apps β€” Rule 8.
  • Use dataSource.store().byKey() for a single row (load({ take: 1 }) instead) β€” Rule 12.
  • Manage the theme from a Remote β€” don't import the theme CSS or call applyTheme / applyFlavor in a Remote; the Host owns the theme β€” Host template & Theme.
  • Rewire vite.config.ts / nuxt.config.ts aliases, or hand-edit tsconfig.json paths (they're generated by mono prepare) β€” Rules 1, 2, V1, N1.
  • Mix the two component builds β€” don't ship light-DOM mono-* as the primary UI of a Nuxt host (skeleton delay + reflow on hydration), and don't reach for the shadow build in a Vue app β€” Rules 11, V5, N5.
  • Apply another language's section β€” a Vue rule in a Nuxt app (or the reverse) β€” read the section that matches type.

When to stop and ask the user ​

  • Starting in a freshly-cloned app still using a default template name (mono-host / mono-vue / mono-nuxt-host / mono-vue-remote) β€” confirm whether to rename it (Rule 1).
  • Editing vite.config.ts / nuxt.config.ts or the @app-name alias paths (Rules 1, 2, V1, N1).
  • Touching mono.config.ts extends / apps (Rule 5).
  • Naming a new page/route (Rule 16).
  • Whether a new page should be in the menu / deployable to the other app (Rule 12).

Glossary ​

TermWhat it is
mono-utilsCore ecosystem package β€” repo wiring (monoRepo), config, env, fetching.
mono-utils/fetchingThe only sanctioned fetch layer (monoFetch, monoCreateFetcher, …). Rule 12.
mono-utils/runtimeRuntime helpers: useMonoUtility (validation, notif, data, OData, JSON) + MonoNotivue; plus useUtils, MonoOdataMapTypes + all validation / OData types; plus cookie/token/jwt/state. Rules 13, 15.
mono-utils/configdefineConfig for mono.config.ts. Rule 5.
mono-helperShared UI + table helpers (controlMonoTable).
mono-helper/uiThe <mono-*> Lit web components β€” light-DOM build. Rules 14, V5.
mono-helper/ui/shadowThe shadow-DOM (SSR) build of the same components β†’ <mono-shadow-*>. Rules 11, N5.
@mono-vue / @mono-host (+ -root)Role-based import aliases β€” own source dir vs the other app's. Rule 2.
monoRepomono-utils/vite β€” one call (const mono = await monoRepo()) that carries all mono wiring in a Vue app: mono.ecosystem(subs), mono.plugin, mono.nuxt(). The Vite twin of mono-utils/nuxt. Rules V1, V7.
mono.plugin / mono.ecosystem()The two things you actually use from that handle β€” plugin (register last) wires alias / config-expose / server.fs.allow / dep dedup; ecosystem(subs) discovers the other app's dirs, type-aware. Rules 1, V1.
monoEcosystem / mergeEcosystemThe older standalone dir-discovery helpers that mono.ecosystem() replaces (still exported for back-compat). Rule V1.
template languageWhich framework an app is written in β€” Vue or Nuxt β€” set by type. Decides which second Common Rules section applies.
type'vue' | 'nuxt' in mono.config.ts β€” the app's template language, which also decides whether its source is src/ or app/. Rules 2, 5, V2, N2.
mono-utils/nuxt / mono-helper/nuxtNuxt modules that replace vite.config.ts wiring for a Nuxt Host β€” aliases, server.fs.allow, config-expose, ecosystem merge (mono-utils/nuxt); SSR + base CSS + mono-* custom-element rule (mono-helper/nuxt). Rule N1.
mono.nuxt()Compat helpers a Vue app uses when the app it federates is Nuxt β€” hostResolver() strips definePageMeta, fixes layout <slot/>, shims useState / <NuxtLink>; extendRoute() injects page meta. Supersedes the standalone monoNuxtHost / monoExtendRoute. Rule V7.
mono.config.tsShared cookie / JWT / API / menu config + sync source. Rule 5.
mono.env.tsCommitted non-secret, per-environment config (base URLs, flags), read via resolveEnv / monoEnv. Rule 7.
.mono/apps/Synced copy of the other app. Read-only. Rule 3.
mono prepareCLI that generates .mono/tsconfig.json (the @mono-* TypeScript paths) and wires the root tsconfig.json extends. Runs on dev / postinstall. Rule 2.
.mono/One gitignored folder for everything mono generates β€” apps/ (synced remotes), tsconfig.json (aliases), sync cache, skills/ (session staging).
mono skillsCLI for MONO Skills: read centralized app knowledge/skills and save Claude session history to EJI-ICT/mono-skills. Opt-in via MONO_SKILLS. Rule 17, Skills.
DataSourceA live, reactive handle to an OData endpoint (paging/filter/sort). Rule 12, DataSource.

Common Rules: General ​

These rules apply to every app, whatever its template language. After them, read the one section that matches your type β€” Vue or Nuxt.

1. The build config is the core β€” ask before changing it ​

When: always be aware of it; act only when a change to build wiring seems needed. Do: treat your app's build config as read-only, and know which file that is β€” vite.config.ts in a Vue app (Rule V1), nuxt.config.ts in a Nuxt app (Rule N1). Don't: add/remove/rewire the ecosystem discovery, resolve.alias, or server.fs.allow without asking.

Every app has one build config that wires its whole source tree. It is fixed infrastructure β€” the programmer (and the AI) rarely touches it. Almost every convenience you rely on in your source folder is set up here, which is exactly why you don't edit it casually: a small change can silently break auto-imports, routing, or the link between Host and Remote.

Which file it is depends on your template language:

typeBuild configWhat's in it
'vue'vite.config.tsplugins + resolve.alias + server.fs.allow β€” Rule V1
'nuxt'nuxt.config.tsmodules + the mono key (there is no vite.config.ts) β€” Rule N1

WARNING

Treat the build config as read-only. If a change there seems necessary β€” ask the user first, especially for the three Host ↔ Remote settings below.

The Host ↔ Remote connection (do not touch without asking) ​

The Host and Remote are only joined in three places. These are the core of the whole mono-repo β€” changing them wrongly breaks the link between apps:

  1. Ecosystem discovery β€” merges the other app's pages, components, stores/shared, and composables/shared into this app's routing, auto-import, and component registration. This is how a Host sees the Remote's modules (and a Remote sees the Host's shared code). It is type-aware: each app's folders resolve to src/ or app/ from its type, so no path is hardcoded. In a Vue app you call mono.ecosystem(subs) (Rule V1); in a Nuxt app the mono-utils/nuxt module does the discovery for you (Rule N1). (Older Vue configs call the standalone monoEcosystem({ … }) β€” or mergeEcosystem, the primitive it wraps.)
  2. resolve.alias β€” an alias for the Host's source and one for the Remote's source (each with a project-root variant), generated for you (Rule 2) β€” not hand-listed. Inside either app, the role it is points at its own source folder, and the role it isn't points into the synced ./.mono/apps/<other>/. So you import by role from anywhere β€” the Host alias always reaches Host code and the Remote alias always reaches Remote code, no matter which app you're working in. This is the import connection β€” see Rule 2.
  3. server.fs.allow β€” grants the dev server filesystem access to ./.mono/apps so it can actually read the synced other app's source (see Sync).

You don't hand-write any of the three any more: a Vue app gets them from mono.plugin + mono.ecosystem() out of one await monoRepo() call (Rule V1), a Nuxt app from the mono-utils/nuxt module (Rule N1). The link is the same either way.

Everything else (server.port, build, dotenv) is plumbing. Do not add, remove, or rewire any of the above on your own β€” confirm with the user first.

Styling crosses apps too β€” uno.config.ts must scan .mono/apps ​

The three links above wire modules (routes, imports, components). Styling has a parallel requirement, in a separate file β€” uno.config.ts. UnoCSS only generates the utility classes it finds in the files it scans, and that scan must cover the synced other app, or none of its classes land in the CSS.

The scan is anchored on an absolute content.filesystem base, not a relative glob:

ts
// uno.config.ts
import { fileURLToPath } from 'node:url'
const monoApps = fileURLToPath(new URL('./.mono/apps', import.meta.url)).replace(/\\/g, '/')

content: {
  // absolute + eager, so federated classes land in uno.css upfront
  filesystem: [
    `${monoApps}/*/src/**/*.{js,ts,vue,html}`,   // vue remotes
    `${monoApps}/*/app/**/*.{js,ts,vue,html}`,   // nuxt remotes
    `${monoApps}/*/mono.config.ts`,              // menu `icon` classes
  ],
  pipeline: {
    include: [
      // UnoCSS's default include, restored
      /\.(vue|svelte|[jt]sx|vine.ts|mdx?|astro|elm|php|phtml|marko|html)($|\?)/,
      // …plus federated .ts/.js β€” a regex matches module ids where a glob cannot
      /[\\/]\.mono[\\/]apps[\\/].*\.(ts|js)($|\?)/,
    ],
  },
}

Note there is no entry for this app's own source β€” it flows through UnoCSS's default include, which the first regex restores (.vue by default, .ts via an //@unocss-include comment). This block is scoped to .mono/apps only.

Relative .mono/apps globs fail silently β‡’ the Host shell renders unstyled

A relative glob ('./.mono/apps/*/src/**') is resolved against Vite's root and then matched against the absolute / virtual module ids Vite actually hands UnoCSS β€” so it matches nothing. Under Nuxt's Vite it's worse: root is the srcDir (app/), so ./.mono/apps/** points at a non-existent app/.mono/apps. Either way there is no error, just missing CSS.

The symptom is misleading: a Remote loads the Host's home layout but every flex / min-h-screen / bg-* is gone, so the shell looks broken β€” while mono-* components still look fine (they ship their own CSS), making the breakage look partial.

Symptom β†’ fix: Host layout/sidebar unstyled (or federated menu icons missing) while mono-* components render styled β†’ open uno.config.ts and move the .mono/apps scan to the absolute content.filesystem form above. Correcting this is a plain fix (the value is known-correct) β€” it's not one of the ask-first cross-app links, which actually rewire how the apps connect.

The rule behind it: whenever a scanned path sits outside the Vite root, a relative content glob fails by matching nothing rather than by erroring. Anchor it absolutely.

First β€” confirm the app name (don't keep the template default) ​

When: the very first thing you do in a freshly-cloned app, before writing code. Do: check name in mono.config.ts; if it's still a template placeholder, ask the user whether to rename it, and suggest one or let them pick a custom name. Don't: keep a placeholder name without asking, rename silently, or change one file but not the other.

Apps are cloned from templates that ship with default names β€” mono-host / mono-vue (the name in mono.config.ts), and mono-host / mono-nuxt-host / mono-vue in package.json. Teams keep these placeholders by accident. So before you build anything, look at name in mono.config.ts. If it's still one of the defaults (mono-host, mono-vue, mono-nuxt-host, mono-vue-remote), stop and ask the user:

"This app is still using the template name mono-host. Do you want to rename it to something project-specific (e.g. acme-host / acme-portal), or keep it as-is?"

Suggest a name based on the project, but let the user give their own or keep the default β€” it's their call.

If the user renames, change the name in both files:

  1. mono.config.ts β†’ the top-level name. This is the app's identity: monoAlias() derives the @<name> / @<name>-root import aliases from it (Rule 2), so renaming mono-host β†’ acme-host turns @mono-host into @acme-host.
  2. package.json β†’ the "name" field, so the package identity matches.

Then re-run mono prepare so .mono/tsconfig.json regenerates against the new alias.

A rename cascades β€” do it at project start

Because the @<name> aliases come from mono.config.ts name, a rename changes every @<name> import, and the other app's apps[].name (and its extends import path) must match the new name (Rule 5). That's why you do this first, before code piles up on the old alias β€” and why you confirm with the user instead of guessing. See Config for the name field and Rule 2 for how the aliases are derived.

2. Import connection β€” the @app-name aliases ​

When: importing across apps, or from datas / types / odata (anything not auto-imported). Do: import by role β€” @mono-vue/... / @mono-host/...; let monoRepo() generate the bundler side and mono prepare generate the TypeScript side. Don't: hand-write the Vite alias paths or the tsconfig.json paths (both are generated); or assume datas/types/odata are auto-imported.

Very important β€” without this, apps can't reach each other's files

The @app-name aliases are how a Host and a Remote import each other's code, and how an app reaches its own folders that aren't auto-imported. If they're missing or wrong, cross-app imports β€” and even your own @mono-vue/types imports β€” stop resolving and the app won't build. Treat them as core infrastructure (part of Rule 1): don't rewire them without asking.

The same alias names live in two places, and both are generated β€” you never hand-write either:

  • The bundler alias β€” resolve.alias (plus the matching jiti alias used to parse mono.config.ts), so the import actually works at runtime. Generated automatically and applied for you β€” by mono.plugin from await monoRepo() in a Vue app (Rule V1), by the mono-utils/nuxt module in a Nuxt app (Rule N1). In neither case is it written in a config file.
  • tsconfig.json β†’ compilerOptions.paths β€” used by TypeScript and your editor so the types resolve (type-checking, go-to-definition). Generated by mono prepare into .mono/tsconfig.json, which the root tsconfig.json extends. Because both sides come from the same source (mono.config.ts + the .mono/apps/ scan), they can't drift.

monoRepo() builds the aliases for you ​

(Vue apps β€” a Nuxt app gets the same map injected by mono-utils/nuxt instead, Rule N1.)

vite.config.ts no longer hand-lists the alias map. One call derives it β€” the own name from mono.config.ts (name), the remotes by scanning .mono/apps/ β€” and mono.plugin applies it:

ts
import { monoRepo } from 'mono-utils/vite'

export default defineConfig(async () => {
  const mono = await monoRepo()

  return {
    // no `resolve.alias` block β€” mono.plugin sets it (along with server.fs.allow
    // and __MONO_CONFIG_EXPOSE__) from the same single config load
    plugins: [/* … */ mono.plugin], // ← last
  }
})

It emits, for an app named mono-host with mono-vue synced into .mono/apps/:

AliasPoints at
@mono-host / @mono-host-rootown ./src / ./
@mono-vue / @mono-vue-root./.mono/apps/mono-vue/src / ./.mono/apps/mono-vue
@mono-apps./.mono/apps

So adding or renaming a remote needs no build-config edit β€” sync it into .mono/apps/ (Rule 3) and the alias appears on its own. The matching tsconfig.json paths are equally hands-off: mono prepare regenerates .mono/tsconfig.json (it runs on dev / postinstall, or run it yourself) so TypeScript stays in sync without an edit.

What the aliases point at (role-based) ​

Each app aliases its own role to its own ./src, and the other app's role into the synced ./.mono/apps/<other>/src (see Sync). So you always import by role, from either app:

AliasIn the Remote (mono-vue)In the Host (mono-host)
@mono-vue./src (its own)./.mono/apps/mono-vue/src (synced)
@mono-host./.mono/apps/mono-host/src (synced)./src (its own)
@mono-vue-root / @mono-host-rootthe matching project-root variantsthe matching project-root variants

(The Host also exposes @mono-apps β†’ ./.mono/apps.) The upshot: @mono-host/... always reaches Host code and @mono-vue/... always reaches Remote code, no matter which app you're in.

Repo name vs app name: the GitHub repos are mono-vue-host (Host) and mono-vue-remote (Remote), but the internal app name in mono.config.ts stays mono-host / mono-vue β€” and the @mono-host / @mono-vue aliases are derived from that name, so they're unchanged.

src/ or app/ is decided by the app's type ​

The tables above assume every app keeps its source under src/ β€” true for a Vue app (Rule V2). A Nuxt app keeps its source under app/ (Rule N2), and monoAlias() accounts for this automatically: it reads each app's type from mono.config.ts (vue β†’ src/, nuxt β†’ app/, see Config) and points the alias at the right folder. You never encode the folder yourself β€” the alias already resolved it, which is why the same @mono-host/... import works from either kind of app. So in a Vue Remote synced to a Nuxt Host:

AliasPoints at
@mono-host./.mono/apps/mono-host/app (Nuxt host β†’ app/)
@mono-vue./src (its own, Vue β†’ src/)

mono prepare mirrors this on the TypeScript side automatically β€” it reads each app's type and points @mono-host/* at app/* (not src/*) when the host is Nuxt, in the generated .mono/tsconfig.json. Nothing to edit by hand.

Why aliases at all β€” what auto-import doesn't cover ​

Auto-import (Rules V1 / N1) only wires up stores, composables, and components. Everything else you reach with an explicit import. Folders like datas, types, and odata are deliberately not auto-imported β€” their names are far too generic and common (every app has a types and a datas), so auto-importing them would collide and pollute the global namespace. Instead you import them explicitly through the alias β€” and that same alias is what lets one app reach the other app's datas / types / odata:

ts
import type { DTO_BrandTypes } from '@mono-vue/types'        // a Remote type
import table from '@mono-vue/datas/table'                    // a Remote's static data
import { QDTO_Brand } from '@mono-vue/odata/DTO/QDefault'     // a Remote's generated OData

So the rule of thumb: auto-imported folders (stores / composables / components) need no import line; everything else β€” especially datas, types, and odata β€” is reached through the @app-name alias. That alias is the backbone connecting the two apps; keep it intact. (Those folder names are relative to your source dir β€” src/ in Vue, app/ in Nuxt.)

3. Never edit .mono/apps/ β€” it's synced ​

When: always β€” whenever you see a path under .mono/apps/. Do: change the other app in its own repo, then pnpm mono:sync. Don't: create, edit, or delete anything inside .mono/apps/ by hand.

.mono/apps/ is where this app keeps a copy of the other app it's connected to β€” a Host stores the Remote there, a Remote stores the Host there. It's generated content, and the whole cross-app setup reads from it: the @mono-host / @mono-vue aliases resolve into it, the ecosystem discovery pulls pages/components/stores from it, and server.fs.allow grants access to it (all of Rule 1's Host ↔ Remote links).

Do not touch .mono/apps/

Never create, edit, or delete files inside .mono/apps/ by hand. It feeds routing, auto-imports, and the build directly, so manual changes will break things β€” and the next sync overwrites them anyway. The only way to update it is to re-sync:

pnpm mono:sync

(the script lives in your package.json). See Sync. Need a change in the other app? Make it in that app's own repo and sync.

4. Never edit generated files ​

When: always β€” before touching any *.d.ts or odata/DTO/ file. Do: change the source (store/composable/component/page, or the OData metadata) and regenerate. Don't: hand-edit odata/DTO/, .mono/tsconfig.json, or your language's generated output (Rules V3 / N3).

Several files are produced by the tooling and regenerated on every dev / build β€” hand edits are silently lost and only hide the real problem. Treat them as read-only.

Every app, whatever its language:

  • everything under odata/DTO/ β€” generated by odata2ts from the OData $metadata
  • .mono/tsconfig.json β€” generated by mono prepare (the @mono-* TypeScript paths)
  • everything under .mono/apps/ β€” synced, not generated, but equally read-only (Rule 3)

Plus your language's own generated output β€” the auto-import / routing type files, which differ per language: see Rule V3 (Vue) or Rule N3 (Nuxt).

Don't hand-edit generated output

Never edit the files above. They're regenerated on dev/build, so your changes vanish β€” and editing the output instead of the source hides the actual cause. Commit them to version control, but to change what they contain, edit the source: a store / composable / component / page for the *.d.ts files, or the OData metadata (then regenerate) for the odata/DTO/ types.

Same principle as Rule 3 (.mono/apps/ is synced, not edited): generated or synced output is read-only β€” fix the thing that produces it.

5. mono.config.ts β€” the shared config + sync core ​

When: adding an API endpoint, cookie, JWT, or menu entry. Do: edit fetching.api / auth / cookie / jwt / menu freely. Don't: change extends or apps without asking β€” they wire the two apps together.

mono.config.ts (built with defineConfig from mono-utils/config) is the single source of truth that ties a Host and a Remote together and holds their shared cookie / JWT / API / menu config. Unlike the build config (Rule 1), you do edit this file regularly β€” adding an API endpoint, a cookie, or a menu entry is normal. Only two fields actually wire the two apps together; treat those with care.

What each field does ​

FieldWhat it does
nameThis app's identity within the ecosystem β€” monoAlias() derives the @<name> aliases from it. If it's still a template default, confirm a rename with the user first (Rule 1 β†’ Confirm the app name); a rename also touches package.json and the other app's apps[].name.
type'vue' or 'nuxt' β€” declares this app's template language, which decides both which Common Rules section applies (Vue / Nuxt) and whether its source is src/ or app/. Read statically by monoAlias / monoEcosystem, so it must be declared above apps. (Host ↔ Remote link β€” see Config and Rules 2, V2, N2.)
extendsMerges the other app's mono.config into this one, so cookies, JWT, APIs, and menu compose across Host and Remote. (Host ↔ Remote link.)
appsThe app(s) this one pulls when you run pnpm mono:sync β€” each entry's url is a GitHub ref, envToken names the env var holding the access token, and type declares that synced app's framework (so its folders resolve to .mono/apps/<name>/src or .mono/apps/<name>/app). (Host ↔ Remote link β€” see Sync.)
fetching.apiNamed REST / OData endpoints and their source constructors. Add your APIs here; call them by name with configBaseUrl. See Data Fetching.
fetching.authuse names the cookie sent on API requests (apiRequest) and the one that authenticates the refresh call (refreshTokenRequest) β€” both are entries of cookie[], so split is inherited. Adding requestRefreshTokenRequest turns on automatic token refresh. See Data Fetching.
cookieDeclares the cookies (name, split) whose values hydrate into mono state.
jwtDecodes cookies into JWT state (prod reads by cookie name; dev uses the decoded payload). token / refreshToken are the two keys mono knows by name, but any key is allowed β€” each hydrates into monoState().jwt.<key>. See Config.
menuThis app's navigation entries (title, url, icon, nested items) β€” how a Remote's modules show up in the Host's nav. See Config.

Host ↔ Remote: handle with care ​

type, extends, and apps are the link between the two apps β€” type decides src/ vs app/ resolution, extends merges the other app's config, and apps declares what mono:sync pulls. type reflects the app's framework and is set once; flipping any of these alters how Host and Remote connect, so confirm with the user before editing them (same as the three build-config settings in Rule 1).

Everything else β€” fetching.api, auth, cookie, jwt, menu β€” is normal day-to-day editing. Add the API, cookie, or menu entry your feature needs.

extends + apps must agree across every app β€” mirror the Host's other Remotes ​

mono.config.ts is loaded transitively: this app extends the Host's config, and the Host's config in turn extends each Remote it owns β€” via that Remote's @<remote>-root import (e.g. import monoVueConfig from '@mono-vue-root/mono.config'). Every one of those imports is resolved with this app's aliases, and monoAlias() only emits a @<x>-root alias for this app's own name and for each entry in this app's apps[] (Rule 2). So the invariant is:

Every @<x>-root referenced anywhere in the merged config chain must resolve to this app's own name or one of its apps[] entries.

In the common one-Host-one-Remote setup this is automatic: the Host imports the Remote as @<remote>-root, and inside the Remote that is its own-root alias (own name), so it resolves with nothing in apps[]. It only bites when the graph grows or a name moves:

  • The Host owns more than one Remote. If the Host extends Remote mono-vue and Remote acme-orders, then each of those Remotes must list the other in its own apps[] β€” otherwise, when acme-orders loads the Host config, the Host's @mono-vue-root import has no alias. Add the sibling Remote to apps[] so mono:sync pulls it into .mono/apps/<remote> and the alias appears on its own (Rule 2 β€” no build-config edit).
  • You renamed a Remote the Host still imports by the old name. Renaming mono-vue β†’ my-memo drops @mono-vue-root (Rule 1 β†’ a rename cascades). If the Host config still extends @mono-vue-root, either repoint the Host's import to @my-memo-root (and rename its apps[].name) β€” the clean fix β€” or re-add mono-vue to this app's apps[] to restore the alias.

Symptom: Cannot find module '@<remote>-root/mono.config' at startup

A missing sibling-Remote alias fails at config-load time: jiti (inside c12) can't resolve the Host's extends import, so vite / vue-tsc die before the dev server starts β€” and mono prepare won't help (it regenerates aliases from name + apps[], it doesn't invent the missing one):

failed to load config from …/vite.config.ts
Error: Cannot find module '@mono-vue-root/mono.config'

Fix the apps[] / extends graph so the name resolves, then pnpm mono:sync + pnpm mono:prepare. Trade-off to flag to the user: adding a Remote to apps[] also merges in its pages/routes and merges its mono.config (menu, fetching) into this app β€” and layers its env file (so a key like MONO_SKILLS in that Remote's .env can override yours). That's the cost of re-providing the alias this way; the cleaner path is to fix the Host's import to the current name.

A Vue app on monoRepo() softens this: its stubMissing option (default on) resolves the mono.config import of an app that isn't synced into .mono/apps/ to an empty config instead of throwing, and warns once naming what it stubbed. So a missing sync no longer hard-stops startup. It does not paper over a genuinely wrong apps[] / extends graph β€” only that specifier is stubbed, and app code importing a missing app still fails. Read the warning; don't ignore it.

6. Naming β€” consistent prefixes & object props ​

When: naming any variable / function, or passing arguments to a handler. Do: group by a shared leading prefix (data*, dataSource*, input*), name functions verb-first (openModal, addBudget), and pass arguments as one typed object β€” openModal({ item }: { item: Budget }). Don't: flip the prefix per entity (budgetData), pass positional scalars (openModal(row.Id, row.Name)), or pass a bare row (openModal(row)).

Consistency is the whole point: when every variable, function, and call site is named the same way, the code reads predictably and the AI (and the next programmer) never has to guess. Pick the leading word by role, keep it identical across entities, and the related names sort and scan together.

Variables β€” leading prefix is the role, suffix is the entity ​

The start of the name says what kind of thing it is; the end says which entity it belongs to. Same role β†’ same leading word, every time. A list is data<Entity>, its DataSource is dataSource<Entity>, an edit/form model is input<Entity>.

ts
// βœ… role first, entity last β€” groupable, instantly scannable
const dataBudget = ref<Budget[]>([])
const dataTransaction = ref<Transaction[]>([])

const dataSourceBudget = ref<any>(null)
const dataSourceTransaction = ref<any>(null)

const inputBudget = ref<Budget>({} as Budget)
const inputTransaction = ref<Transaction>({} as Transaction)

// ❌ entity-first / mixed prefixes β€” unrelated-looking, hard to scan
const budgetData = ref([])
const transactionList = ref([])
const budgetSource = ref(null)

So the leading word (data, dataSource, input) is the category and stays fixed; only the trailing entity changes. Two variables for the same role always share their prefix.

Functions β€” verb-first, same shape ​

Functions follow the same idea: a consistent leading verb is the action, the rest is the target. Pair openers with closers, and keep the verb identical across entities.

ts
const openModal = () => { /* … */ }
const closeModal = () => { /* … */ }
const addBudget = () => { /* … */ }
const addTransaction = () => { /* … */ }

Arguments β€” one typed object, always the whole row ​

When you call a handler β€” especially inside a table row / detail loop β€” pass a single object, never positional arguments, and pass the whole row, not hand-picked fields.

ts
// βœ… one object prop, whole row, typed β†’ self-documenting and future-proof
const openModal = ({ item }: { item: Budget }) => {
  inputBudget.value = item
}

// in the template, inside a row loop:
// <mono-button @click="openModal({ item: row })">Detail</mono-button>
ts
// ❌ positional scalars β€” order-sensitive, breaks the moment you need one more field
const openModal = (id: string, name: string) => { /* … */ }
openModal(row.Id, row.Name)

// ❌ bare row β€” works, but the call site can't tell what shape is expected
const openModal = (row) => { /* … */ }
openModal(row)

Why object-props-with-types wins:

  • Self-documenting call site β€” openModal({ item: row }) names what you pass.
  • Order-free & extensible β€” grow to { item, mode } later without touching callers.
  • Typed in one place β€” { item }: { item: Budget } gives autocomplete and catches a wrong row shape right at the call site. Declare the shape in types/ (Rule 16).
  • Whole row, not fragments β€” pass row, not row.Id / row.Name; the handler reaches whatever field it needs and you never go back to thread one more argument through.

Combine the two β€” object destructuring plus an inline type β€” for the cleanest DX: ({ item }: { item: Budget }).

7. Secrets & env β€” secrets in .env, non-secret config in mono.env.ts ​

When: handling any config value (API key, token, base URL, port, feature flag). Do: sort by sensitivity β€” secret β†’ .env / .env.dev; non-secret shared config (base URL, flag) β†’ a committed mono.env.ts, read via resolveEnv / monoEnv; dev-server-only (port, HTTPS) β†’ the build config (Rules V6 / N6). Don't: put a non-secret base URL in .env (it vanishes when a dev forgets to push it), hardcode a secret anywhere, or read PORT / VITE_HTTPS from .env.

Not every config value is a secret, and they don't all live in the same place β€” sort by what the value is:

ValueWhere it livesRead with
Secret β€” API key, token, PAT, Sentry DSN / auth token.env / .env.dev (never committed)import.meta.env.* / process.env.*
Non-secret shared config β€” API base URL, feature flagmono.env.ts (committed)resolveEnv (load-time) Β· monoEnv (runtime)
Dev-server-only β€” port, local HTTPShardcoded in the build config β€” Rule V6 / N6β€”

The rest of this rule takes each row in turn.

Secrets β†’ .env (.env.dev for dev, .env for prod) ​

Real secrets β€” API keys, tokens, the Sentry DSN β€” go in an env file at the app root, never hardcoded in a page, store, or mono.config.ts. Two files, picked by what you're running:

  • .env.dev β€” development. Loaded by the dev scripts (pnpm dev).
  • .env β€” production. Loaded by build / preview (pnpm build, pnpm preview).
mono-vue-remote/
β”œβ”€β”€ .env.dev              # development secrets/config
β”œβ”€β”€ .env                  # production secrets/config
└── .mono/
    └── apps/
        └── mono-host/
            └── .env.dev  # the other app's env, layered in automatically

Syncing env between apps ​

You don't merge env files by hand. Every script already runs through mono-env (shipped with mono-utils), which loads this app's root env first, then layers each connected app's matching env file from .mono/apps/ on top β€” see Environment. So just run the normal scripts from package.json:

json
{
  "scripts": {
    "postinstall": "mono sync && mono prepare",
    "dev": "mono prepare && mono env -e .env.dev -- vite",
    "build": "mono env -e .env -- vue-tsc && mono env -e .env -- vite build --emptyOutDir",
    "preview": "mono env -e .env -- vite preview"
  }
}

postinstall leads with mono sync so a plain pnpm i recovers an empty or stale .mono/apps/ on its own β€” pnpm auto-runs install before any script, so a broken .mono/ would otherwise break every pnpm command, pnpm mono:sync included. (A Nuxt host chains && nuxt prepare after it.) See Sync.

pnpm dev loads .env.dev (root + every app); pnpm build / pnpm preview do the same with .env. The -- <command> part is the real command that runs with the merged environment.

Shared vs private: the MONO_ prefix ​

The build config sets envPrefix: ['VITE_', 'MONO_'] (Rule V6 / N6), so only those prefixes reach client code. Use them deliberately:

  • MONO_… β€” the only shared vars. Anything one app must read from another (or the Host shares with Remotes). Read with import.meta.env.MONO_*. Keep keys unique per app (MONO_HOST_API_URL, MONO_VUE_API_URL) so they don't override each other when apps are layered.
  • VITE_… β€” app-private client config (standard Vite).
  • no prefix β€” build/tooling-only secrets. Stay in process.env for the command, never shipped to the browser.

Don't commit real secrets

.env / .env.dev hold real values and must not be committed. Commit a .env.example with empty keys instead (the templates already ship one). See Environment for the full load order and flags.

Non-secret config β†’ mono.env.ts ​

A base URL isn't a secret β€” hiding it in .env just means a dev can forget to push it and the app breaks. Put non-secret, per-environment values in a committed mono.env.ts at the app root: an env object keyed by NODE_ENV, plus the resolved appEnv. It's checked in, merges across the host+remote extends chain, and any consumer can import it β€” including the Node OData codegen (odata2ts.config.ts), which can't import mono.config.ts (it would pull in the @mono-host alias, mono-devextreme, and the generated DTO).

ts
// mono.env.ts
import { resolveEnv } from 'mono-utils/config'

export const env = {
  default:    { API_BASE_URL: 'https://api.dev.example.com' },
  production: { API_BASE_URL: 'https://api.example.com' },
}
export const appEnv = resolveEnv({ env })
  • At load time (mono.config.ts, odata2ts.config.ts) import appEnv and read the value β€” e.g. fetching.api.main.url = String(appEnv.API_BASE_URL). Pass env to defineConfig too, so it merges and is exposed.
  • At runtime (component / store) use monoEnv('API_BASE_URL').
  • Selection is by NODE_ENV β€” each env file should set it (.env.dev β†’ development, .env β†’ production); env.default is the fallback.
  • Values must be scalars and non-secret β€” they ship to the browser bundle.
  • Don't call monoEnv() inside the fetching literal (it resolves before mono inits β†’ empty); use appEnv / resolveEnv at load time there.

See Environment β†’ Config env object.

Dev-only knobs β†’ the build config ​

PORT and VITE_HTTPS are dev-server-only, so they're hardcoded in the build config, not in .env. Where exactly depends on your language β€” Rule V6 (Vue: a const in vite.config.ts) or Rule N6 (Nuxt: devServer.port).

8. Unique exports & store keys β€” suffix with the app name ​

When: always β€” every store / composable you create. Do: suffix the export name and the defineStore key with the app name. Don't: ship a generic name like useAuth or a key like 'auth'.

Stores and composables are auto-imported (Rule 1), which means a Host and every Remote share one global namespace. Two apps that export the same name β€” or two Pinia stores that use the same defineStore key β€” collide silently: only one survives, and you end up calling the wrong code or reading the wrong state with no error to warn you. So make every auto-imported export name and every store key unique by suffixing it with the app name.

Pinia stores β€” unique variable and unique key ​

A store in stores/ has two identifiers, and both must be unique:

ts
// ❌ generic β€” collides with the Host or another Remote
export const useAuthStore = defineStore('use-my-fetch-auth', () => {
  const data = ref()
  return { data }
})

// βœ… both the export and the key carry the app name
export const useAuthStoreMonoVue = defineStore('use-my-fetch-auth-mono-vue', () => {
  const data = ref()
  return { data }
})
  • The export variable (useAuthStore…) is auto-imported globally. If a Remote and the Host both export useAuthStore, auto-import resolves to one of them β€” you may call a completely different store than you meant to.
  • The defineStore key ('use-my-fetch-auth…') is how Pinia stores global reactive state. Two stores sharing a key share the same state slot: if both declare const data = ref(), the data you read is whichever store registered first β€” the other is silently overridden. This is the dangerous one, because the app still runs.

Use the pattern: variable useAuthStoreAppName, key 'your-key-name-app-name'.

Composables & utils β€” unique export name too ​

The same applies to anything auto-imported from composables/ (Rule 10) β€” the top-level export must be unique, so suffix it with the app name:

ts
export const useHostHelper = () => { /* … */ }   // Host
export const useUtilsHost  = () => { /* … */ }   // Host
// in a Remote, the mirror: useHelperMonoVue, useUtilsMonoVue, …

Rule of thumb: if it's auto-imported, its name (and a store's key) must be globally unique β€” append the app name. Generic names like useAuth, useUtils, or a key like 'auth' are landmines across a Host + its Remotes.

9. Static data lives in datas/ ​

When: you have static / constant data (config, option lists, column defs, sample rows). Do: put it in datas/ (src/datas/ in Vue, app/datas/ in Nuxt), grouped, with one export default per group. Don't: inline it in a page / component / store, or scatter named exports.

Any static / constant data β€” config objects, option lists, lookup maps, column definitions, labels, sample rows β€” goes in datas/ inside your source dir (src/ in Vue, app/ in Nuxt β€” Rules V2 / N2). Never inline it in a page, component, or store. Static data tends to be large and turns into a mess when it's scattered or passed around as props, and it's almost always shared across more than one component β€” keeping it in one place keeps it reusable and out of the way.

Group it, and expose one export default per group so consumers do a single import. A long list of named imports β€” import { static1, day, am } from '...' β€” is painful to maintain. Instead, put each piece in its own leaf file, then collect them in an index.ts that default-exports one object.

src/
└── datas/
    └── table/
        β”œβ”€β”€ users.ts                  # each leaf: `export default { ... }`
        β”œβ”€β”€ products.ts
        β”œβ”€β”€ orders.ts
        β”œβ”€β”€ invoices.ts
        β”œβ”€β”€ customers.ts
        └── index.ts                  # groups the leaves + export default

src/datas/table/index.ts β€” group, then default-export:

ts
import users from './users'
import products from './products'
import orders from './orders'
import invoices from './invoices'
import customers from './customers'

const config = {
  users,
  products,
  orders,
  invoices,
  customers,
}

export default config

Now a consumer imports the whole group once and reaches into it by key:

ts
import table from '@/datas/table'

table.orders     // β†’ the orders table config
table.invoices   // β†’ the invoices table config

As with everything else, type the data (and put any reused shapes in types/, per Rule 16).

10. Shared utilities go in composables/use-utils.ts ​

When: you write a genuinely global / reusable helper (not tied to one feature). Do: put it in composables/use-utils.ts (or a split-out file re-exported from it). Don't: use it for feature-specific helpers β€” those stay in that feature's use-<name>-utils.ts (Rule 16).

When you have a reactive helper, a common utility, or a reusable bit of behavior that should be available globally (used across features, not tied to one page), put it in composables/use-utils.ts. Everything in composables is auto-imported (Rules V1 / N1), so it's usable anywhere with no import line.

  • This is for genuinely shared / global helpers. Feature-specific helpers still belong in that feature's own use-<name>-utils.ts (Rule 16).
  • If a group of utilities grows large, give it its own file in composables/ and re-export it through use-utils.ts, so consumers still reach everything from one place.

The example below shows the Vue layout (src/); a Nuxt app uses app/composables/ β€” and must have the folder opted into imports.dirs (Rule N2).

src/composables/
β”œβ”€β”€ use-utils.ts          # global helpers β€” auto-imported everywhere
└── use-date-utils.ts     # split out when a group grows; re-exported by use-utils.ts
ts
// src/composables/use-utils.ts
export * from './use-date-utils'   // pull a large, split-out group back in

// `ref` is auto-imported β€” no import needed
export function useToggle(initial = false) {
  const on = ref(initial)
  const toggle = () => (on.value = !on.value)
  return { on, toggle }
}

11. Component flavor β€” light DOM in a Vue app, shadow in a Nuxt (SSR) host ​

When: building UI with mono-* components (pairs with Rule 14). Do: in a Vue app (host or remote) use the default light-DOM build (import 'mono-helper/ui/<c>'); in a Nuxt host use the shadow-DOM build (import 'mono-helper/ui/shadow/<c>') so SSR paints the real component. Don't: ship light-DOM components as the primary UI of a Nuxt/SSR host (skeleton delay + reflow on hydration); hand-wrap shadow components when mono-helper/nuxt already auto-wraps them.

Every mono-* component ships in two builds, and which one you reach for is decided by your app's template language β€” because it comes down to whether the app server-renders:

Your appBuildImportTagRule
type: 'vue' β€” Vue host or any remotelight DOMmono-helper/ui/<c><mono-<c>>V5
type: 'nuxt' β€” the Nuxt hostshadow DOM (SSR)mono-helper/ui/shadow/<c><mono-<c>> (auto-wrapped)N5

Why the split: a Vue SPA hydrates on the client, so there is no server paint to protect and light DOM is the simplest, smallest path. A Nuxt host does server-render, and the shadow build emits Declarative Shadow DOM so the browser paints the fully-styled component on the first response β€” no skeleton delay, no unstyled flash, no reflow when Lit hydrates.

Rule of thumb: Vue β†’ light; Nuxt host β†’ shadow. Note the asymmetry that catches people out β€” a Vue remote stays light even under a Nuxt host (it is still a client-rendered Vue island); the shadow build is only for the Nuxt host's own server-rendered shell and pages. So a single running page can legitimately contain both.

Everything else about using the component (props, events, theming via CSS variables) is identical either way β€” see Rule 14. For the mechanics of each build β€” the exact import, whether you write the tag yourself, and the SSR wrapper β€” go to Rule V5 or Rule N5.

12. Data fetching β€” always use mono-utils/fetching ​

When: always β€” any data access. Do: use mono-utils/fetching (monoCreateFetcher + a DataSource for lists/tables); for one row, reuse a source you already hold via load({ take: 1 }). Don't: add an external HTTP lib; build a second fetcher (monoOdataFetch / another monoCreateFetcher) for a detail/related row you could read from a DataSource you already hold; or use dataSource.store().byKey() for a single record.

All data access goes through mono-utils/fetching. Never introduce an external HTTP library (axios, ofetch, ky, a raw fetch wrapper, …). The mono helpers already resolve the base URL, token / JWT, headers, and OData source from mono.config.ts (Rule 5) β€” bypassing them breaks auth and the shared config.

Two things to do when fetching enters the picture:

  • New base URL β†’ add it to mono.config.ts first. If the user gives a base URL that isn't already a named entry under fetching.api, add it there, then consume it by name with configBaseUrl: '<entry>'. That keeps URLs centralized and shared so both Host and Remote can use them β€” don't hardcode a one-off baseUrl for something reusable.
  • New page β†’ ask if it should be deployable. When you plan a new page (Rule 16), ask the user: should this feature be visible to the other Host/Remote β€” i.e. registered in mono.config.ts menu so it shows in the nav β€” or is it still in development and should stay out of the menu for now?

monoCreateFetcher + a DataSource is the standard ​

monoCreateFetcher is the all-in-one fetch solution β€” make it your default for list and table data. It returns a reactive DataSource, and that DataSource is how you should hold and re-query server data. Why:

  • One call gives a live handle, not a dead snapshot β€” it already knows its base URL, token, and OData source from mono.config.ts.
  • You fetch once, keep the DataSource in a ref, and re-query through that same source β€” filter, search, sort, change select. You never build a second fetcher.
ts
// (per Rule 16 this lives in the store)
const usersSource = ref<any>(null)

onMounted(async () => {
  const { dataSource } = await monoCreateFetcher({
    configBaseUrl: 'myOdata',
    url: '/Users',
  }).response({ options: { select: ['Id', 'Name'], paginate: true, pageSize: 20 } })

  usersSource.value = dataSource
})

// later β€” need a different slice? reuse the same source, no new fetcher:
await usersSource.value.store().load({ filter: ["contains(Name,'jo')"], select: ['Id', 'Name'] })

// need one row (e.g. a DETAIL view) while the source is bound to a grid/select?
// query its STORE so the bound list isn't disturbed β€” and do NOT build a 2nd fetcher:
const [detail] = await usersSource.value.store().load({
  filter: ['Id', '=', id], expand: ['Role'], select: ['Id', 'Name', 'Role'], take: 1,
})

// the source is NOT bound to a live component? dataSource.load() is fine too:
const [one] = await usersSource.value.load({ filter: ['Id', '=', id], take: 1 })

Single record β†’ reuse the source, never a 2nd fetcher or byKey()

Already holding a DataSource (a list/table you fetched)? To read one row from it β€” a detail view, a related record β€” reuse that same source; don't reach for a fresh monoOdataFetch or another monoCreateFetcher. That's the whole point of the live handle: one source, re-queried.

  • Source is bound to a live component (grid / mono-select): query its store β€” source.store().load({ filter: ['Id','=',id], take: 1, expand, select }). This runs a one-off query without disturbing the bound list's paging/filter/sort.
  • Source is not bound to anything: source.load({ filter: ['Id','=',id], take: 1 }) is fine too.
  • Never dataSource.value.store().byKey(id) β€” once the source is bound, byKey caches against the already-loaded data and can return a stale row (or nothing).

See DataSource.

Bind that DataSource straight to mono-select, mono-tag-input, or a controlMonoTable table β€” see DataSource.

One DataSource per purpose

A DataSource is a single live, shared instance. If you bind the same DataSource to two components β€” say a mono-select and a mono-tag-input β€” they share its state, so filtering, sorting, or paging from one also changes the other. Create a separate DataSource (its own monoCreateFetcher call) for each component / purpose. Reuse the same source only when you genuinely want the two views kept in sync.

OData: select only what you need ​

With OData you shape the response, so select only the fields the screen actually uses. Before adding a field, find the reason it's needed; if there isn't one, ask the user. Example: a table renders Id and Name β€” does it really need Description? If nothing displays or filters on it, leave it out (smaller payload, faster load). Work out the reason first, then ask.

What's in mono-utils/fetching ​

ExportWhat it does
monoFetchPlain REST fetch. Resolves base URL (configBaseUrl or baseUrl), token, and headers.
monoOdataFetchOData fetch β€” shape the result via options (select, filter, sort, paginate…). Returns { data, dataSource, statusCode, error }.
monoOdataFetchUniqueLike monoOdataFetch, but collapses concurrent identical requests (unique).
monoCreateFetcherAll-in-one builder β†’ a reactive OData DataSource. The default for list / table data.
monoStaticDataSourceWrap a static array as an OData-style DataSource.
monoTryCatchDatasourceSafe load wrapper around a DataSource (handles errors / notifications).
monoLoadChuckStoresLoad a store in batched chunks.
monoConfigureFetching / monoResetFetchingConfigSet / reset runtime fetch options (notif, token, base-URL overrides, and unauthCall β€” where to send the user when a token expires and the refresh can't save it; it needs the router, so it can't live in mono.config.ts).
monoFetchingRuntimeInspect the resolved runtime (base URLs, source, token) for a configBaseUrl.
monoRestBaseUrl / monoOdataBaseUrl / monoOdataSource / monoRequestTokenLow-level resolvers (REST/OData base URL, OData source ctors, request token). Rarely needed directly.

See Data Fetching for full call examples and DataSource for binding a DataSource to components.

13. OData URL β†’ generate typed classes with odata2ts ​

When: the user gives you an OData URL, or you consume an OData entity. Do: generate classes with odata2ts, then map them with MonoOdataMapTypes in types/odata.d.ts. Don't: hand-write the entity interfaces, or edit the generated odata/<Key>/ output.

When the user hands you an OData URL (or asks you to consume an OData entity), never hand-write the entity interfaces. Generate them from the service's $metadata with odata2ts, then map the output to plain types. This keeps the shapes correct and in sync with the backend.

The flow, in short:

  1. Configure the service in odata2ts.config.ts β€” each entry's key is unique and becomes the folder name under odata/<Key>. One config can hold many OData sources (one key/folder each).
  2. Generate with pnpm odata:gen β€” it writes the Q-objects, models, and services into odata/<Key>/. That output is generated β€” never hand-edit it (Rule 4).
  3. Map to usable types. The generated output is raw query-object classes, not plain shapes. In types/odata.d.ts, wrap each with MonoOdataMapTypes<typeof Q...> (from mono-utils/runtime), importing the Q-object through the app's own resolve alias β€” @mono-vue/odata/... in a Remote, @mono-host/odata/... in the Host (Rule 2). The alias already resolves src/ vs app/ for you.
  4. Use the mapped types across the app β€” in store state, fetch generics, and table columns. They're safe and shared.

If the OData base URL is new, it's also an env var (MONO_…, Rule 7) and a fetching.api entry (Rules 5 / 12).

See OData Types for the full step-by-step with real examples.

14. UI β€” reach for mono-helper/ui first ​

When: building any UI. Do: go top-down β€” <mono-*> component β†’ its .mono-* CSS β†’ UnoCSS β†’ native CSS in assets/. Don't: write custom markup/CSS when a mono-* component already covers it.

Build UI with the shared library before writing anything custom. Follow this priority top-to-bottom, and only drop to the next level when the current one genuinely can't do the job:

  1. mono-ui Lit component β€” the web components from mono-helper/ui/* (<mono-button>, <mono-input>, <mono-select>, …). Import the subpath (import 'mono-helper/ui/button') and use the tag β€” in a Nuxt host, the shadow subpath instead (Rules 11, V5, N5). This is the default for anything that has a matching component.
  2. mono-ui native (CSS only) β€” when you must hand-build the markup, reuse the component's .mono-* CSS classes and assemble the element yourself. Same look, full control over the DOM.
  3. UnoCSS utilities β€” no suitable mono-ui for a very custom layout/style? Use UnoCSS utility classes.
  4. Native CSS (last resort) β€” UnoCSS still can't express it? Write plain CSS and store it in assets/ in its own, specifically-named file (e.g. assets/brand-report.css), then import it only into the page that needs it. Don't scatter ad-hoc global <style> or dump rules into shared files.

See Mono-UI β†’ Getting started for install and usage, and the per-component pages for the available tags and their props.

15. External utilities β€” reach for useMonoUtility helpers first ​

When: you need validation, notifications, list add/update/remove, OData filters, or JSON parsing. Do: destructure from useMonoUtility() (imported from mono-utils/runtime). Don't: hand-roll these; and don't use them for fetching (that's Rule 12).

Before you hand-roll form validation, notifications, list add/update/remove, OData filter strings, or JSON parsing, stop β€” there's almost certainly a ready-made, battle-tested helper for it in useMonoUtility (the shared helper surface in mono-utils). Don't reinvent these; the shared helpers handle the awkward edge cases (nested schema errors, @odata.* metadata cleanup, double-wrapped JSON) you'd otherwise rediscover.

These helpers cover, broadly:

  • Validation (Yup) β€” validate a whole form or a single field, and clear errors.
  • Notifications β€” one unified notif() for success / info / warning / error / promise (render them with MonoNotivue in the app shell).
  • Data manipulation & filtering β€” upsert/remove rows in arrays or DataSources, build OData in filters, stringify DevExtreme filters to $filter.
  • Numeric input β€” preventMinus guards on numeric fields.
  • JSON & misc β€” safe parse, JSON / date type guards.

How to use them ​

Import useMonoUtility from mono-utils/runtime β€” the same entry as monoJwt / createMono β€” and destructure what you need:

ts
import { useMonoUtility } from 'mono-utils/runtime'
const { validateAllSchema, notif, replacerData } = useMonoUtility()

A project may still re-spread it into a local auto-imported composable (composables/use-helper.ts) if it wants a single project-wide helper.

Fetching stays on mono-utils/fetching

mono-utils also ships fetching / datasource helpers, but data access still goes through mono-utils/fetching (Rule 12) β€” that's the standard path here.

DevExtreme-grid extras via useUtils

useMonoUtility excludes numColTemplate (auto-numbering DataGrid column) + the Excel/lookup helpers. On the rare screen that needs them, use the full useUtils() β€” also re-exported from mono-utils/runtime.

See Useful Utils for the full list of helpers, signatures, and what each one does.

16. Building a new feature β€” separate the code ​

When: building a feature or page β€” the synthesis step that uses Rules 1–15. Do: split into layers β€” thin page, components, Pinia store (logic+fetching), composable helpers, .d.ts types. Don't: dump everything in one file, or put business logic / fetching in the page.

This is the last rule on purpose: by here you understand the machine, the conventions, and the capabilities. Building a feature just composes them.

When asked to build a feature or page (say a Brand / Master screen), never dump everything into one file. Split it across these layers so logic stays reusable and the page stays readable.

1. Ask for the page name first. Before creating anything, ask the user what the page/route should be named (e.g. brand). Don't assume a name.

2. Create the page as a thin entry at pages/<name>.vue. The page is presentational: markup plus wiring to the store. No business logic, no fetching, no heavy computation inline β€” it only renders UI and reads from / calls the store.

3. Extract components when the page grows. If a page gets large (~500 lines) or contains markup that repeats or could be reused, pull those chunks into their own focused components under components/<name>/. Keep each file small and single-purpose.

4. Put all logic in a Pinia store at stores/use-<name>.ts. Everything that isn't markup lives here and is globally reusable: reactive state (ref / reactive), computed, watchers, plain functions, and fetching. Pages and components only read from or call into the store. Use the project's fetch helpers from Data Fetching β€” monoFetch, monoFetchOdata, monoCreateFetcher.

5. Move generic helpers into a composable at composables/use-<name>-utils.ts. Pure, reusable functions that might be needed again later go here. These are usually consumed only by the store (use-<name>.ts).

6. Always TypeScript, with types in .d.ts. Every file is typed. Whenever the store holds typed reactive data β€” e.g. const data = ref<Brand[]>() β€” declare that type in types/<name>.d.ts and import it. Don't inline complex shapes ad-hoc.

Layout ​

The tree below is a Vue app (src/, Rule V2). In a Nuxt app the identical layers live under app/ (Rule N2) and the page is a route the same way β€” only the root folder changes.

src/
β”œβ”€β”€ pages/
β”‚   └── brand.vue                 # thin page β€” UI + store wiring only
β”œβ”€β”€ components/
β”‚   └── brand/                    # extracted, reusable pieces (when the page grows)
β”‚       └── BrandTable.vue
β”œβ”€β”€ stores/
β”‚   └── use-brand.ts              # ALL logic: state, computed, fetching, functions
β”œβ”€β”€ composables/
β”‚   └── use-brand-utils.ts        # generic helpers, usually used by the store
└── types/
    └── brand.d.ts                # types for the store's reactive data

Each layer, in short ​

src/types/brand.d.ts β€” the shape, declared once and shared.

ts
export interface Brand {
  id: string
  name: string
  active: boolean
}

src/stores/use-brand.ts β€” all state, fetching and logic; globally reusable.

ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { monoFetch } from 'mono-utils/fetching'
import type { Brand } from '@/types/brand'
import { sortByName } from '@/composables/use-brand-utils'

export const useBrand = defineStore('brand', () => {
  const data = ref<Brand[]>([])
  const loading = ref(false)

  const activeBrands = computed(() => data.value.filter((b) => b.active))

  async function load() {
    loading.value = true
    const res = await monoFetch<Brand[]>('/brands', { configBaseUrl: 'MyApi', method: 'GET' })
    data.value = sortByName(res.data ?? [])
    loading.value = false
  }

  return { data, loading, activeBrands, load }
})

src/composables/use-brand-utils.ts β€” generic, reusable helpers.

ts
import type { Brand } from '@/types/brand'

export function sortByName(rows: Brand[]): Brand[] {
  return [...rows].sort((a, b) => a.name.localeCompare(b.name))
}

src/pages/brand.vue β€” thin: read the store, render UI.

vue
<script setup lang="ts">
import { onMounted } from 'vue'
import { useBrand } from '@/stores/use-brand'

const brand = useBrand()
onMounted(() => brand.load())
</script>

<template>
  <section>
    <mono-input label="Search" />
    <!-- when this grows, move the table into components/brand/BrandTable.vue -->
    <ul>
      <li v-for="b in brand.activeBrands" :key="b.id">{{ b.name }}</li>
    </ul>
  </section>
</template>

The takeaway: pages render, stores think, composables provide reusable helpers, and types live in .d.ts. There's no rigid flow beyond that β€” build what the feature needs, but keep it in these layers.

17. MONO Skills β€” read shared knowledge, save the session ​

When: before non-trivial work (read what's already known), and after meaningful work (save what you learned). Only when MONO_SKILLS is enabled.Do: mono skills check once; read / search the app's knowledge (CLI) β€” or read the public summarized llms-skills.txt index (then llms-skills-full.txt only for full detail) when there's no token β€” before complex work; after meaningful work, stage the session and mono skills save, then report the saved path. Don't: touch GitHub directly, bypass the mono skills CLI, print the token, save under a generic app name, or claim a save succeeded unless the result is { "success": true }.

Apps live in separate repos, so what one session learns is invisible to the next β€” and to the other app. MONO Skills is the shared brain: a private repo (EJI-ICT/mono-skills) holding each app's knowledge, reusable skills, decisions, and Claude session history, reached only through the mono skills CLI. Full reference: Skills (for AI).

It's opt-in and off by default β€” it only runs when MONO_SKILLS=true (with MONO_SKILLS_GITHUB_TOKEN). When it's off or missing, do your normal work and skip the whole workflow; it must never block, delay, or change the primary task.

The loop ​

bash
mono skills check                                # confirm config once (never prints the token)
mono skills read   --app <id> --type knowledge   # ground yourself before complex work
mono skills search --app <id> --query "<topic>"  # only when relevant
# …do the user's task…
# stage the session under .mono/skills/pending/<id>/ (metadata.json, summary.md, …)
mono skills save --app <id> --dir .mono/skills/pending/<id>   # upload (--dry-run to validate)

On success the CLI returns { "success": true, "savedPath": "…" } β€” report that path. On failure it preserves the staging under .mono/skills/failed/ for mono skills retry. .mono/skills/ is local staging only (gitignored, part of the .mono/ convention β€” see Sync); the permanent store is the repo.

Two ways to read what past sessions learned ​

  • mono skills read / search β€” the full, curated app knowledge and skills folders, plus ranked search. Needs MONO_SKILLS=true + MONO_SKILLS_GITHUB_TOKEN.
  • https://mono-libs.pages.dev/llms-skills.txt β€” a public, no-token, summarized index of the session history, grouped by project (one short entry per session). Start here. The full content of every session lives at https://mono-libs.pages.dev/llms-skills-full.txt β€” read it only when the summarized index isn't enough. Both are generated at build from the same synced mono-skills data and work even when MONO_SKILLS is off β€” with zero setup.

Prefer the CLI when the feature is on (targeted + current); fall back to the summarized llms-skills.txt index (then llms-skills-full.txt) otherwise.

Rename the app first β€” generic names are skipped

While the app still uses a default template name (mono-host, mono-vue, mono-nuxt-host, mono-vue-remote), save / retry are skipped (reason: "MONO_SKILLS_GENERIC_APP_NAME") so history isn't filed under a placeholder id. Rename it first (Rule 1 β†’ Confirm the app name).

Through the CLI only β€” and never fake it

Use mono skills only; never call the GitHub API / git / gh directly for this workflow, never target another repo, and never print or pass the token. A second-pass redaction strips secrets from anything uploaded β€” but don't stage secrets / .env values in the first place. Only claim success on { "success": true }, and never hide from the user that session history was saved.

Save for meaningful work β€” code changes, bug investigations, architecture / DB / API changes, business-rule clarifications, config / security changes, reusable findings (including useful failures). Don't save greetings, trivial fixes, or cancelled work with no conclusion.

Common Rules: Vue ​

Read this section when mono.config.ts says type: 'vue'.

That covers a Vue Host and every Remote β€” a Remote is always a Vue + Vite app, even when its Host is Nuxt (see Setup). So in a Vue-Host + Vue-Remote pair, both apps read this section; in a Nuxt-Host + Vue-Remote pair, the Host reads Nuxt and the Remote reads this β€” with the extra wiring in Rule V7.

These rules sit on top of the General rules; they never replace them.

V1. vite.config.ts is the core β€” one monoRepo() call wires it ​

When: always be aware of it; act only when a change to build wiring seems needed. Do: get the mono wiring from const mono = await monoRepo(), and register mono.plugin last. Don't: hand-write resolve.alias / server.fs.allow / __MONO_CONFIG_EXPOSE__, or rewire any of it without asking (Rule 1).

A Vue app's infrastructure lives in vite.config.ts, and the whole mono side of it is a single call β€” monoRepo() from mono-utils/vite, the Vite twin of the mono-utils/nuxt module:

ts
// vite.config.ts β€” the mono core
import { monoRepo } from 'mono-utils/vite'

export default defineConfig(async () => {
  const mono = await monoRepo()

  return {
    envPrefix: ['VITE_', 'MONO_'], // Rule V6
    plugins: [
      VueRouter({ routesFolder: [{ src: 'src/pages' }, ...mono.ecosystem('pages')] }),
      AutoImport({ dirs: ['src/composables', 'src/stores', ...mono.ecosystem(['composables', 'stores'])] }),
      Components({ dirs: ['./src/components', ...mono.ecosystem('components')] }),
      Layouts({ layoutsDirs: 'src/layouts', defaultLayout: 'default' }),
      mono.plugin, // ← MUST be last
    ],
  }
})

monoRepo() loads mono.config.ts once and resolves the extends-active apps, then gives you:

From the handleWhat it does
mono.ecosystem(subs)The remote dirs to feed an ecosystem plugin β€” type-aware, so a nuxt remote resolves to app/<sub> and a vue remote to src/<sub>. Takes a string or string[]. Replaces the old monoEcosystem({ dirname, apps, subs }) at every call site.
mono.pluginRegister last. Wires resolve.alias (Rule 2), __MONO_CONFIG_EXPOSE__, server.fs.allow for .mono/apps, and dependency dedup β€” Host ↔ Remote links 2 and 3 of Rule 1, from that one config load.
mono.nuxt()Compat helpers, only when the app you extend is Nuxt β€” Rule V7.
mono.alias / mono.appsThe computed alias map and the extends-active { name, type } list, if you need to read them.

mono.plugin goes last, and your own dirs come first

Register mono.plugin as the final plugin β€” it reads what the others resolved. And keep your own folder first in each list ('src/pages', then ...mono.ecosystem('pages')) so local files win over federated ones. A host that owns / also does ...mono.ecosystem('pages').map((dir) => ({ src: dir, exclude: ['*/index.vue'] })), so each remote's root index.vue can't clobber the host's home page.

layoutsDirs is the one line that differs by role. A Host ships its own layout shell, so it keeps layoutsDirs: 'src/layouts' (as above). A Remote consumes the Host's layouts instead β€” layoutsDirs: mono.ecosystem('layouts'). Getting this backwards is why a Remote renders without the Host's shell.

This replaced a hand-written block

Older templates opened with monoAlias() + getMonoConfig() + resolveExtendsAppNames, then repeated monoEcosystem({ dirname, apps: activeApps, subs }) four times, and hand-wrote resolve.alias / define / server.fs.allow. If you're looking at a vite.config.ts shaped like that, it predates monoRepo(). Those exports still work, so don't migrate it as a side-quest β€” but write new wiring the way above. See Template Changelog β†’ Simplify Config.

What each plugin gives you (DX) ​

These plugins are why src/ code is so light β€” most of them mean you don't write imports:

PluginWhat it does
@vitejs/plugin-vueCompiles .vue SFCs. isCustomElement lets mono-, dx- (and ui5- in a Remote) tags through untouched so the browser renders the web components.
unplugin-auto-import (AutoImport)Auto-imports vue, vue-router, @vueuse/core, pinia, unhead, and everything in src/stores and src/composables. So ref, computed, useRouter, defineStore, and your own useBrand() work with no import line. Generates a .d.ts for type support (Rule V3).
unplugin-vue-components (Components)Auto-registers components from src/components β€” drop a .vue there and use it in any template without importing. directoryAsNamespace turns the folder into a prefix (e.g. components/brand/Table.vue β†’ <BrandTable>).
vue-router/vite (VueRouter)File-based routing β€” a file in src/pages becomes a route automatically. No manual route table.
vite-plugin-vue-layouts (Layouts)Wraps pages in layouts from src/layouts; defaultLayout: 'default'.
unocss/vite (UnoCSS)Atomic utility-class CSS engine. Its scan config lives in uno.config.ts and must reach the synced apps through an absolute content.filesystem base β€” see the relative-glob warning in Rule 1.

Each of those dirs / routesFolder lists gets its federated half from mono.ecosystem(...), as shown above β€” that's the one place remote dirs enter the build.

Host-only extras: vite-plugin-vue-devtools (the in-app Vue devtools) and @sentry/vite-plugin (uploads source maps when VITE_SENTRY_DSN is set). Remote-only extras: vite-plugin-mkcert (local HTTPS when VITE_HTTPS=true) and a server.proxy for /api and /odata during dev.

V2. Source lives in src/ ​

When: creating any file, or resolving where an existing one lives. Do: put source under src/; let auto-import cover stores / composables / components. Don't: create an app/ folder (that's Nuxt β€” Rule N2), or expect datas / types / odata to be auto-imported.

type: 'vue' means this app's source root is src/. The folder names the General rules refer to are all relative to it:

src/
β”œβ”€β”€ pages/        # file-based routes (VueRouter)          β€” auto-routed
β”œβ”€β”€ layouts/      # page layouts, defaultLayout: 'default' β€” auto-applied
β”œβ”€β”€ components/   # auto-registered (directoryAsNamespace)  β€” no import line
β”œβ”€β”€ composables/  # auto-imported (Rules 10, 8)             β€” no import line
β”œβ”€β”€ stores/       # auto-imported Pinia stores (Rule 8)     β€” no import line
β”œβ”€β”€ datas/        # static data (Rule 9)      β€” import via @<app-name>/datas
β”œβ”€β”€ types/        # .d.ts shapes (Rule 16)    β€” import via @<app-name>/types
β”œβ”€β”€ odata/        # generated OData (Rule 13) β€” import via @<app-name>/odata
β”œβ”€β”€ assets/       # last-resort native CSS (Rule 14)
└── main.ts       # app bootstrap (Rule V4)

The three not auto-imported (datas, types, odata) are reached through the @app-name alias β€” the reason why is Rule 2.

V3. Generated files a Vue app never edits ​

When: before touching any *.d.ts at the project root. Do: change the source (a store / composable / component / page) and let dev/build regenerate. Don't: hand-edit these β€” the edits are silently overwritten (Rule 4).

On top of the language-neutral list in Rule 4, a Vue app generates:

  • auto-imports.d.ts β€” by unplugin-auto-import
  • components.d.ts β€” by unplugin-vue-components
  • typed-router.d.ts β€” by vue-router (its header literally says "DO NOT MODIFY THIS FILE")

They're regenerated on every pnpm dev / pnpm build. Commit them, never edit them.

V4. Register mono in src/main.ts ​

When: wiring the app up, or debugging "mono state is empty". Do: app.use(createMono(monoConfig)) in src/main.ts. Don't: add a server event resolver β€” that's Nuxt-only (Rule N4).

ts
// src/main.ts
import { createMono } from 'mono-utils/runtime'
import monoConfig from '../mono.config'

app.use(createMono(monoConfig))

A Vue app is client-only, so there is no server pass to hydrate cookies on β€” createMono alone is the whole registration.

V5. Use the light-DOM mono-* build ​

When: using any mono-* component (the decision itself is Rule 11). Do: import 'mono-helper/ui/<c>' and write <mono-<c>>. Don't: import the mono-helper/ui/shadow/* subpath or write <mono-shadow-*> in a Vue app.

vue
<script setup lang="ts">
import 'mono-helper/ui/button'
import 'mono-helper/ui/input'
</script>

<template>
  <mono-input label="Search" />
  <mono-button>Save</mono-button>
</template>

The component renders into the light DOM (no shadow root) and is styled by the global mono-helper CSS the Host imports (Host template β€” a Remote never imports the theme).

A Remote stays light even when its Host is Nuxt. The Remote is a client-rendered island inside the Host's server-rendered shell, so it gains nothing from the shadow/SSR build. Don't "match the host" β€” match your own type.

V6. Dev-only knobs live in vite.config.ts ​

When: setting a port, local HTTPS, or the client env prefix. Do: hardcode them as a const / literal in vite.config.ts (Rule 7). Don't: read PORT or VITE_HTTPS from .env.

ts
const PORT = 2020
// server: { port: PORT }, preview: { port: PORT + 1 }
// local HTTPS stays off unless you flip it: `false && mkcert({ … })`
// envPrefix: ['VITE_', 'MONO_']  β€” only these reach client code (Rule 7)

V7. When the app you extend is Nuxt β€” mono.nuxt() ​

When: the app you extends (normally your Host) has type: 'nuxt'. Do: add ...mono.nuxt().hostResolver() before VueRouter, and mono.nuxt().extendRoute() inside it. Don't: add either one for a Vue federation, hardcode the host's app/pages path, or change this without asking (Rule 1).

A Nuxt app keeps its code under app/, its pages use definePageMeta, and its layouts render <slot/> β€” none of which a Vue/Vite app can execute. The same mono handle from Rule V1 carries the two compat helpers:

ts
// vite.config.ts (Vue app federating a Nuxt app)
import { monoRepo } from 'mono-utils/vite'

const mono = await monoRepo()

plugins: [
  // before VueRouter: rewrites the Nuxt-only code (see below)
  ...mono.nuxt().hostResolver(),
  VueRouter({
    // type-driven: finds the other app's app/pages, no hardcoded path
    routesFolder: [{ src: 'src/pages' }, ...mono.ecosystem('pages')],
    // injects { layout, title } for its definePageMeta pages
    extendRoute: mono.nuxt().extendRoute(),
  }),
  mono.plugin, // ← last
]
  • hostResolver() returns the Plugin[] that makes synced Nuxt code runnable: strips the definePageMeta({…}) macro, rewrites a remote layout's default <slot/> to <router-view/> (so setupLayouts' nested routes render the page), resolves Nuxt's useState() / clearNuxtState() to the mono-utils/runtime shim, rewrites <NuxtLink> to <RouterLink>, and defines import.meta.server / import.meta.client. Spread it before VueRouter() so the rewrite runs first. Each rewrite can be switched off (nuxtState: false, nuxtLink: false, defineImportMeta: false).
  • extendRoute() is the VueRouter({ extendRoute }) callback that parses { layout, title } out of a synced page's source so setupLayouts wraps it. Pages using definePage are read natively by vue-router and skipped.

Nuxt federation only

Omit mono.nuxt() entirely for a Vue federation β€” those pages use definePage natively and those layouts already render <router-view/>, so the rewrites have nothing to do.

Set the other app as apps[].type: 'nuxt' in mono.config.ts; mono prepare then points @mono-host/* at its app/* (not src/*) in the generated .mono/tsconfig.json automatically (Rule 2). Everything else in your app is unchanged β€” you still write src/, still use the light build (V5). See Setup.

(The standalone monoNuxtHost / monoExtendRoute exports from mono-utils/vite still work and take the same options; mono.nuxt() is the current surface.)

Common Rules: Nuxt ​

Read this section when mono.config.ts says type: 'nuxt'.

Today only a Host can be Nuxt β€” a Remote is always Vue. So this section describes the server-rendered shell that Remotes plug into. These rules sit on top of the General rules; they never replace them.

The headline difference: a Nuxt app server-renders. That's what drives the shadow-DOM component build (N5) and the plugin-based registration (N4).

N1. nuxt.config.ts is the core β€” there is no vite.config.ts ​

When: always be aware of it; act only when a change to build wiring seems needed. Do: treat nuxt.config.ts as read-only; let the two mono modules do the wiring. Don't: create a vite.config.ts, or hand-list aliases / server.fs.allow (they're injected).

A Nuxt Host has no vite.config.ts. The same fixed infrastructure lives in nuxt.config.ts, driven by two modules and a mono key:

ts
// nuxt.config.ts
export default defineNuxtConfig({
  ssr: true,
  mono: { utils: {}, helper: {} },
  modules: ['mono-utils/nuxt', 'mono-helper/nuxt', /* …unocss, pinia, vueuse… */],
  // nested shared/ dirs aren't scanned by default β€” opt them in (Rule N2)
  imports: { dirs: ['composables', 'composables/**', 'stores', 'stores/**'] },
  vite: { envPrefix: ['VITE_', 'MONO_'] },  // Vite-only tweaks go under `vite` (Rule N6)
})

What the two modules give you:

ModuleWhat it injects
mono-utils/nuxtthe @<app-name> aliases, server.fs.allow for .mono/apps, the __MONO_CONFIG_EXPOSE__ define, and the ecosystem merge β€” everything a Vue app wires by hand in vite.config.ts
mono-helper/nuxtthe base mono-helper CSS, the mono-* isCustomElement rule, and SSR (monoSsr) β€” including the auto-wrap behind Rule N5

So the three Host ↔ Remote links of Rule 1 map onto Nuxt like this:

Vue (vite.config.ts)Nuxt (nuxt.config.ts)
mono.ecosystem(...) + the plugins (from monoRepo())the modules array + mono key β€” they discover and wire the other app's pages/components/stores for you
mono.plugin β†’ resolve.aliasinjected by mono-utils/nuxt β€” not hand-listed
server.fs.allowinjected by mono-utils/nuxt

Treat nuxt.config.ts as read-only and ask before changing the mono wiring (Rule 1). Setup has the full walkthrough.

N2. Source lives in app/ ​

When: creating any file, or resolving where an existing one lives. Do: put source under app/; opt nested shared/ dirs into imports.dirs. Don't: use src/ (that's Vue β€” Rule V2), or assume a nested folder is auto-imported.

type: 'nuxt' means this app's source root is app/. The folder names the General rules refer to are all relative to it β€” app/pages, app/components, app/stores, app/composables, app/layouts, app/datas, app/types, app/odata, app/assets.

Nested shared/ dirs need imports.dirs

Nuxt scans only the top level of composables/ and stores/ by default, so a nested app/composables/shared/use-x.ts is not auto-imported and its call fails at runtime with an undefined function. Opt the globs in explicitly:

ts
imports: { dirs: ['composables', 'composables/**', 'stores', 'stores/**'] }

This matters here more than in Vue because the ecosystem merge pulls in the other app's stores/shared and composables/shared (Rule 1) β€” exactly the nested layout Nuxt skips.

N3. Generated files a Nuxt app never edits ​

When: before touching anything under .nuxt/. Do: change the source and let Nuxt regenerate (nuxt prepare / pnpm dev). Don't: hand-edit .nuxt/ or commit fixes into it (Rule 4).

On top of the language-neutral list in Rule 4, Nuxt generates the whole .nuxt/ directory β€” auto-import declarations, typed routes, and the generated tsconfig Nuxt extends. It's rebuilt on every nuxt prepare / dev / build, so edits there vanish. If a type is wrong, fix the source file (or nuxt.config.ts imports.dirs, Rule N2) and regenerate.

N4. Register mono in app/plugins/mono.ts, not main.ts ​

When: wiring the app up, or debugging "cookies/JWT are empty on the server". Do: register createMono from a Nuxt plugin and keep the setMonoEventResolver server guard. Don't: look for a main.ts β€” a Nuxt app has none (that's Vue, Rule V4).

Because Nuxt also runs on the server, mono needs a way to read the request's cookies during SSR. That's what the resolver does β€” drop it and monoState() hydrates empty on the server, so the first paint renders logged-out and then flips after hydration:

ts
// app/plugins/mono.ts
import { createMono, setMonoEventResolver } from 'mono-utils/runtime'
import monoConfig from '@mono-host-root/mono.config'

export default defineNuxtPlugin((nuxtApp) => {
  if (import.meta.server) setMonoEventResolver(() => useRequestEvent())
  nuxtApp.vueApp.use(createMono(monoConfig))
})

Keep the import.meta.server guard β€” useRequestEvent() only exists on the server.

N5. Use the shadow-DOM mono-* build β€” and let it auto-wrap ​

When: using any mono-* component in a Nuxt host (the decision itself is Rule 11). Do: import 'mono-helper/ui/shadow/<c>' and write the normal <mono-<c>> tag. Don't: hand-write <mono-shadow-*>, hand-wrap in an SSR wrapper, or import the light subpath for the host's own UI.

vue
<script setup lang="ts">
// the shadow subpath β€” this import is also the signal that triggers auto-wrapping
import 'mono-helper/ui/shadow/button'
</script>

<template>
  <mono-button>Save</mono-button>
</template>

mono-helper/nuxt auto-wraps a <mono-*> in its SSR wrapper when the .vue file imports the matching shadow subpath. The match is import-driven and exact β€” so importing mono-helper/ui/shadow/button wraps <mono-button> in that file, and nothing else. You write the ordinary tag; the module does the rest.

The payoff (Rule 11): the server emits Declarative Shadow DOM, so the browser paints the fully-styled component on the first response β€” no skeleton delay, no unstyled flash, no reflow when Lit hydrates. See Mono-UI β†’ Getting started.

N6. Dev-only knobs live in nuxt.config.ts ​

When: setting a port or the client env prefix. Do: use devServer.port, and put Vite-only options under the vite key (Rule 7). Don't: read PORT from .env, or create a vite.config.ts to hold them (Rule N1).

ts
export default defineNuxtConfig({
  devServer: { port: 2020 },
  vite: { envPrefix: ['VITE_', 'MONO_'] },  // only these prefixes reach client code
})

Before you finish ​

A quick self-check against the always-rules before you call a change done:

  • [ ] You read the section matching this app's type β€” and applied only that one (Vue / Nuxt).
  • [ ] The mono-* build matches the language: light in a Vue app, shadow in a Nuxt host (Rules 11, V5, N5).
  • [ ] New files landed in the right source root β€” src/ in Vue (V2), app/ in Nuxt (N2).
  • [ ] All data access goes through mono-utils/fetching β€” no external HTTP lib (Rule 12).
  • [ ] A detail / single-row read reuses an existing DataSource (source.store().load({ take: 1 }) when it's bound to a component) instead of a second fetcher; never store().byKey() (Rule 12).
  • [ ] Every new store has a unique export name and defineStore key (app-suffixed) (Rule 8).
  • [ ] Nothing under .mono/apps/ or any generated file (*.d.ts, .nuxt/, .mono/tsconfig.json, odata/DTO/) was hand-edited (Rules 3, 4, V3, N3).
  • [ ] OData types are generated + mapped with MonoOdataMapTypes, not hand-written (Rule 13).
  • [ ] Variables/functions use consistent leading prefixes and handlers take one typed object β€” openModal({ item }: { item: T }) (Rule 6).
  • [ ] Typed data has its shape in a .d.ts; static data lives in datas/ (Rules 16, 9).
  • [ ] Cross-app / datasΒ·typesΒ·odata imports use the @app-name alias (Rule 2).
  • [ ] UI starts from mono-helper/ui; secrets live in env with the right prefix (Rules 14, 7).
  • [ ] If the Host shell looks unstyled (layout/sidebar lose their utility classes while mono-* components still render), uno.config.ts scans .mono/apps via an absolute content.filesystem base, not a relative glob (Rule 1).
  • [ ] You paused to ask before touching infra (the build config, aliases, mono.config.ts extends/apps).
  • [ ] If MONO_SKILLS is on and the work was meaningful, the session was saved with mono skills save and the path reported (Rule 17).

Host template ​

When: you're working in the Host. Do: own the shared shell β€” pages (Login/Logout), layouts (Sidebar/Navbar), Middleware, and the theme (Mono/Material + color). Don't: bury a Remote's module-specific logic in the Host.

The Host owns the shell every Remote reuses β€” see Getting Started. All Common Rules above apply here too β€” General, plus Vue or Nuxt depending on this Host's type.

  • Owns: Login / Logout pages, layouts (Sidebar, Navbar), Middleware β€” the generic pieces a Remote should never rebuild.
  • Owns the theme β€” and is the only app that does. The Host is the single place that imports the theme CSS (mono-helper/index.css, plus mono-helper/ui/theme/mui.css only if Material is used) and sets the active theme (Mono / Material) and theme color via the theming API (applyTheme / applyFlavor). Theme/color apply as classes on a root element (<html> / <body>), so they cascade into every Remote automatically β€” a Remote inherits the Host's theme without importing or applying anything. See Theme.
  • Exposes to Remotes via the three Host ↔ Remote links (Rule 1) and mono.config.ts (Rule 5): its shared pages/components/stores and the merged cookie/JWT/API/menu config.
  • Keep out of the Host: feature logic that belongs to one Remote's module β€” build that in the Remote.

When the Host is a Nuxt app ​

A Host can be a Nuxt app instead of Vue + Vite (type: 'nuxt' in mono.config.ts). It owns the same shell and theme β€” only the language-level wiring differs, and that is now covered in full by Common Rules: Nuxt: nuxt.config.ts as the core (N1), source under app/ (N2), registration via app/plugins/mono.ts (N4), and the shadow-DOM component build for the server-rendered shell (N5).

Nothing about the Host's role changes: it still owns Login/Logout, layouts, Middleware, and the theme. Setup has the full config walkthrough.

Starter repos: Vue https://github.com/EJI-ICT/mono-vue-host Β· Nuxt https://github.com/EJI-ICT/mono-nuxt-host

Remote template ​

When: you're working in a Remote. Do: add your own modules under src/, and register pages in mono.config.ts menu to appear in the Host nav. Don't: re-create Login / Logout / layouts / Middleware, or manage the theme β€” the Host already provides them.

A Remote only adds its own modules and consumes the Host's shell β€” see Getting Started. All Common Rules above apply here too β€” General plus Vue, because a Remote is always a Vue app, whatever its Host is.

  • Adds: its feature pages, components, stores under its own src/ (auto-imported per Rule V1).
  • Surfaces in the Host: add a menu entry in mono.config.ts (Rule 5) so the module shows in the Host's navigation; ask the user whether a new page should be deployable yet (Rule 12).
  • Don't rebuild the shell: Login, Logout, Sidebar, Navbar, Middleware and layouts come from the Host β€” just use them.
  • Don't manage the theme. Don't import the theme CSS (mono-helper/index.css, mono-helper/ui/theme/mui.css) and don't call applyTheme / applyFlavor. The Host owns and applies the theme globally; a Remote just uses the mono-* components and inherits the active theme/color. See Theme.

When the host is a Nuxt app ​

A Vue Remote stays a Vue + Vite app even when its Host is Nuxt β€” you keep reading Common Rules: Vue, you write the same src/ modules, and your day-to-day code is unchanged. Do not switch to the Nuxt section, and do not switch to the shadow component build (Rule V5) β€” the Host's type is not yours.

Only vite.config.ts infra differs, and that is Rule V7: apps[].type: 'nuxt' makes mono prepare resolve @mono-host/* to the host's app/* (Rule 2), and the build adds the Nuxt-compat helpers mono.nuxt().hostResolver() / .extendRoute() alongside the same type-driven mono.ecosystem(...) discovery. See Setup for the exact config.

Starter repo: https://github.com/EJI-ICT/mono-vue-remote (branches example-vue-host and example-nuxt-host)