app:create scaffolds the whole structure, and app:init lays down the workspace root that holds it together. This page explains how that monorepo is wired and how to work inside it.
Why a monorepo
An Ooneex app grows by adding modules —app, shared, then one per business domain (movie, billing, user). A monorepo keeps all of them in one place while letting each evolve independently:
- One install, one lockfile —
bun installresolves every module’s dependencies into a singlenode_modulesat the root, so versions stay consistent across the whole app. - Shared tooling — formatter, linter, TypeScript config, commit rules, and git hooks are defined once at the root and apply to every module.
- Cross-module imports without publishing — modules reference each other through path aliases and Bun workspaces, with no build-and-publish step in between.
- Cached, incremental tasks — Nx runs
lint,test, andbuildacross every module, only re-running what changed. - Independent slices — microservices and SPAs live as modules in the same repo yet build and deploy on their own.
Anatomy of the workspace
The monorepo is a Bun workspace. The rootpackage.json declares the workspace, the scripts, and the shared lint-staged config; everything under modules/* is a workspace member.
package.json is the heart of the workspace:
package.json
workspaces: ["modules/*"] glob is what makes this a monorepo: Bun treats every folder under modules/ as a package, hoists their dependencies into the root node_modules, and links them together.
Modules as workspace packages
Each module undermodules/<name>/ is its own package with its own package.json and tsconfig.json, but it shares the root’s install, tooling, and TypeScript config. A module owns a complete vertical slice of your domain — controllers, services, repositories, entities, migrations, and seeds — so all the code for one feature lives in one folder rather than being scattered across global controllers/ and services/ directories.
Modules come in a few flavors, all of them members of the same workspace:
| Type | Runs as | Registered into AppModule / SharedModule | Created with |
|---|---|---|---|
api | Part of the main app process | Yes | module:create (the app module) |
module | Part of the main app process | Yes | module:create |
microservice | Standalone HTTP service (own entrypoint + Dockerfile) | Yes | microservice:create |
spa | Standalone Vite front-end (own dev server) | No | spa:create |
microservice and spa modules live in the same monorepo and reuse the same
install, aliases, and tooling, but they build and deploy independently. See
Microservices and Single Page App.Cross-module imports with path aliases
The roottsconfig.json maps each module to a @module/<name> alias, so modules import each other by name instead of brittle relative paths:
tsconfig.json
module:create or microservice:create, the generator adds the new alias to tsconfig.json automatically (and registers the module in AppModule, its entities in SharedModule, and its scope in the commitlint config). SPAs are deliberately left out of the path aliases and module registries so they never pull server-side wiring into the front-end.
Task orchestration with Nx
Tooling tasks run across the whole monorepo through Nx. Thelint and test scripts use nx run-many to execute the matching target in every module at once, while nx.json configures dependency order and caching:
nx.json
dependsOn: ["^build"]ensures a module’s dependencies are built before it is linted, tested, or built.cache: truelets Nx skip any task whose inputs haven’t changed, so repeated runs only touch what you actually modified.
Shared root tooling
Because it’s a monorepo, configuration is defined once at the root and inherited by every module:biome.jsonc— a single formatter and linter for all modules.bun run fmtapplies it everywhere.tsconfig.json— the strict, shared TypeScript baseline plus the module path aliases. Each module’stsconfig.jsonextends from this root..commitlintrc.ts— conventional-commit rules. Module names become valid commit scopes, kept in sync as you add and remove modules..husky/— git hooks installed at the root:pre-commitrunslint-staged(Biome on staged files) andcommit-msgruns commitlint.bunfig.toml— Bun config; enables test coverage across the workspace.
app:init writes — see below.
How the monorepo is generated
Two commands build the monorepo, one layered on the other.app:init lays down the workspace root
app:init writes the root tooling — package.json (with the modules/* workspace), tsconfig.json, biome.jsonc, nx.json, bunfig.toml, .commitlintrc.ts, .gitignore, .zed/settings.json, and an .env.yml — installs the shared dev dependencies (Biome, commitlint, husky, lint-staged, nx, TypeScript), initializes the git repository, and configures the husky hooks. It can optionally add the Claude and Codex skills.Run it on its own to turn an existing folder into an Ooneex workspace:app:create builds the full app on top of it
app:create creates the app and shared modules, the entrypoint, the shared database, roles.yml, and Docker files, then calls app:init internally for the workspace root, installs the runtime dependencies, and offers CI/CD files. This is the one command you normally run:Working in the monorepo day to day
A single
bun install at the root installs dependencies for every module.
You almost never run bun install inside a module folder — the workspace
hoists and links everything from the root.Use with Claude and Codex
Initialize the AI skills, then ask your agent to work across the monorepo in natural language — it uses the project’s real module structure, aliases, and tooling.- Claude
- Codex
Prompt
External resources
Bun Workspaces
How Bun resolves and links the
modules/* members.Nx
Task orchestration and caching for
lint, test, and build.TypeScript Paths
How the
@module/* aliases map to module source.Conventional Commits
The commit format enforced across the monorepo.
Next steps
Create your app
Scaffold the monorepo and build your first module.
Configuration
The
.env.yml and roles.yml files in the shared module.Module overview
What a module is and how it slices your domain.
app:create
The command that generates the whole monorepo.