Architecture
Numa is structured as a monorepo managed by Turborepo and pnpm workspaces. Applications are independently deployable Next.js apps that share common packages.
Directory structure
numa/
├── apps/
│ ├── energy/ → energy.numalogistics.com (port 3001)
│ ├── gateway/ → gateway.numalogistics.com (port 3000)
│ └── docs/ → Internal documentation (port 3005)
│
├── packages/
│ ├── config/ → Constants, types, platform config, route utilities
│ ├── i18n/ → next-intl server config, translation messages
│ ├── utils/ → Utility functions, extensions, cn() helper
│ ├── api/ → Axios client, TanStack Query helpers, service hooks
│ ├── ui/ → Shared UI components (shadcn, G* components, layouts)
│ ├── ui-error/ → Lightweight error/not-found components
│ └── providers/ → React providers, context, shared hooks
│
└── tooling/
├── eslint/ → Shared ESLint configuration
├── prettier/ → Shared Prettier configuration
└── typescript/ → Shared TypeScript base and Next.js configsApps
| App | Package Name | Port | Description |
|---|---|---|---|
| Gateway | @numa/gateway | 3000 | Login, SSO, profile |
| Energy | @numa/energy | 3001 | Energy management dashboard |
Each app is a Next.js 16 application using the App Router with Turbopack for development.
Packages
Packages are internal libraries that apps import. They are not published to npm — they use workspace:* protocol for linking.
| Package | Purpose |
|---|---|
@numa/config | Constants, types, platform config, route utilities, i18n locales, UI theme |
@numa/i18n | next-intl server config, translation messages |
@numa/utils | Utility functions, extensions, cn() helper, hooks |
@numa/api | Axios client, TanStack Query hook factories, service hooks |
@numa/ui | Shared UI components: shadcn primitives, G-prefixed components, layouts |
@numa/ui-error | Lightweight error page and not-found components |
@numa/providers | React providers (QueryClient, OIDC, essentials), shared hooks |
Tooling
Shared configurations consumed by all apps and packages:
| Package | Purpose |
|---|---|
@numa/eslint-config | ESLint 9 flat config with Next.js, TypeScript, Prettier |
@numa/prettier-config | Prettier formatting rules |
@numa/typescript-config | TypeScript base and Next.js configs |
Workspace configuration
pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "tooling/*"turbo.json
Turborepo orchestrates tasks across the monorepo:
build— depends on upstream packages (^build), outputs.next/**dev— persistent, no cachelintandtype:check— cascade through dependenciesclean— no cache
Package linking
Apps declare workspace dependencies in their package.json:
{
"dependencies": {
"@numa/config": "workspace:*",
"@numa/i18n": "workspace:*",
"@numa/api": "workspace:*",
"@numa/ui": "workspace:*",
"@numa/providers": "workspace:*",
"@numa/utils": "workspace:*"
}
}Apps must also list shared packages in next.config.ts under transpilePackages so Next.js compiles them:
transpilePackages: [
"@numa/config",
"@numa/i18n",
"@numa/utils",
"@numa/api",
"@numa/ui",
"@numa/providers",
],Dependency flow
App (energy, gateway)
├── @numa/providers → React providers, OIDC, essentials
│ ├── @numa/api → Axios, TanStack Query
│ └── @numa/config
├── @numa/ui → Components, layouts
│ ├── @numa/config
│ └── @numa/utils
├── @numa/i18n → Translation loading
├── @numa/config → Types, constants, routes
└── @numa/utils → Helpers, extensionsPackages should never create circular dependencies. If package A needs something from package B and B already depends on A, use dependency inversion (accept the dependency as a prop or parameter).