template-nx is an Nx monorepo template for building modern TypeScript products with a clean separation between apps, shared packages, core platform modules, and repo-level tooling.
It ships with a web app, a Storybook workspace, a shared UI library, a type-safe API layer, database and auth foundations, and operational modules for analytics, logging, Redis, AI workflows, and security.
- Nx for monorepo orchestration, task pipelines, and caching
- Bun as the package manager and script runner
- React 19 + TanStack Start for the main app
- Tailwind CSS v4 for styling
- Base UI-powered shared components in
packages/ui - oRPC + Zod for end-to-end type-safe APIs
- Drizzle + PostgreSQL for the data layer
- better-auth for authentication
- Storybook for component development
- Oxlint + Oxfmt for linting and formatting
template-nx/
├── apps/
│ ├── web/ # Main product app
│ └── storybook/ # UI development and component showcase
├── packages/
│ ├── ui/ # Shared UI primitives and app-facing components
│ └── api/ # Type-safe API layer built around oRPC
├── core/
│ ├── env/ # Environment validation and config loading
│ ├── auth/ # Authentication services and integration
│ ├── db/ # Database client, schema, and Drizzle workflows
│ ├── logging/ # Logging and OpenTelemetry integration
│ ├── posthog/ # Analytics clients
│ ├── redis/ # Redis connection utilities
│ ├── arcjet/ # Security and request protection helpers
│ └── mastra/ # AI and agent workflow integration
└── tooling/
├── css/ # Shared CSS and styling utilities
├── oxc/ # Lint and format configuration
├── boundary/ # Workspace boundary enforcement
└── config/ # Shared configuration package
The main application is built with React, Vite, and TanStack Start. It consumes shared UI from packages/ui, the API layer from packages/api, and platform modules from core/*.
Storybook is included as a dedicated app so you can develop and document the shared UI system in isolation.
The workspace logging and OpenTelemetry setup exports logs to these targets by default:
- console
- PostHog
Mastra observability is also configured to export traces to PostHog by default.
To run services that use @core/logging or @core/mastra, define these server environment variables:
POSTHOG_API_KEYPOSTHOG_HOSTOTEL_SERVICE_NAME
This is the shared component library for the workspace. It contains reusable UI building blocks, hooks, and utilities that can be consumed by apps across the monorepo.
- Built around Base UI primitives
- Styled with Tailwind CSS
- Exported through typed workspace entrypoints such as
@packages/ui/components/* - Backed by Storybook for faster design system development
This package centralizes the application's type-safe API surface.
- Uses oRPC and Zod
- Connects the app to
core/auth,core/db,core/env, andcore/arcjet - Keeps backend contracts reusable and composable across the workspace
The core directory contains platform-level modules that hold infrastructure and business-critical foundations.
@core/env: validates and exposes environment variables@core/auth: wraps authentication withbetter-auth@core/db: manages Drizzle, PostgreSQL, and local database workflows@core/logging: provides logging and OpenTelemetry support@core/posthog: separates analytics for client and server usage@core/redis: provides Redis access utilities@core/arcjet: holds request protection and security helpers@core/mastra: supports AI and agent-related workflows
This structure makes it easier to keep product code, infrastructure concerns, and shared platform capabilities clearly separated.
The tooling layer keeps repo-wide conventions centralized.
@tooling/css: shared global styles and styling dependencies@tooling/oxc: linting and formatting scripts powered by Oxlint and Oxfmt@tooling/boundary: workspace boundary checks to keep architecture clean@tooling/config: shared config package consumed across apps and libraries
Together, these packages make the template easier to scale without scattering config across the repository.
Install dependencies:
bun installIf you are using the database features, configure your environment in apps/web/.env and then apply the schema:
bun run db:pushStart development:
bun run devStart only the web app:
bun run dev:webStart Storybook:
bun run dev:storybookThis workspace already includes a full Paraglide setup for the apps/web application. The goal of the integration is to keep localization type-safe, easy to evolve, and tightly aligned with routing and server rendering.
At a high level, the stack is split across three places:
project.inlang/settings.jsondefines the localization source of truthmessages/pt-br.jsonstores the message catalog for the current localeapps/web/src/paraglide/*is generated by the Paraglide Vite plugin and consumed by the app at runtime
The current configuration uses pt-br as both the base locale and the only active locale:
{
"baseLocale": "pt-br",
"locales": ["pt-br"]
}Even with a single locale, this integration is valuable because it establishes the full i18n pipeline now instead of forcing a rewrite later when additional languages are introduced.
Paraglide is registered in apps/web/vite.config.ts through paraglideVitePlugin(...).
That plugin is responsible for:
- reading the inlang project from
../../project.inlang - compiling message files into generated runtime modules
- writing generated output to
apps/web/src/paraglide - configuring locale detection through URL, cookie, browser preference, and base locale fallback
- localizing routes using the configured URL patterns
The current Vite config uses this strategy order:
urlcookiepreferredLanguagebaseLocale
That means the app resolves locale using the request URL first, then the Paraglide cookie, then the browser language, and finally falls back to the configured base locale if nothing else matches.
The web app is configured to localize all routes with this pattern:
- default route form:
/:path(.*)? - localized
pt-brroute form:/pt-br/:path(.*)?
In practice, Paraglide gives the app a consistent way to move between localized and de-localized paths.
Inside apps/web/src/router.tsx:
deLocalizeUrl(url)normalizes incoming URLs for TanStack Router matchinglocalizeUrl(url)rewrites outgoing URLs back into the localized format
This is important because it lets the router work with a single route tree while still presenting locale-aware URLs externally.
The web server entrypoint wraps the TanStack Start handler with Paraglide middleware:
return paraglideMiddleware(req, () => handler.fetch(req));This happens in apps/web/src/server.ts and is what allows locale resolution to happen at the request layer instead of only in the client. It ensures the locale is available during server rendering, routing, and HTML generation.
Application code imports Paraglide messages from the generated module:
import { m } from '@/paraglide/messages';Message access is function-based, so UI code calls translation keys like:
m.sign_in();
m.dashboard();
m.not_found();This gives a few concrete benefits:
- translation keys are referenced through generated, typed functions
- missing or renamed keys are easier to catch during development
- interpolation stays explicit at the callsite
- UI code remains readable without hand-written translation wrappers
You can see this pattern throughout the app, including:
apps/web/src/routes/__root.tsxfor document metadata and<html lang>apps/web/src/routes/index.tsxfor page contentapps/web/src/components/header.tsxfor navigation labelsapps/web/src/components/sign-in-form.tsxandsign-up-form.tsxfor form labels, validation, and toastsapps/web/src/components/user-menu.tsxfor authenticated navigation
The root route uses Paraglide runtime helpers directly:
getLocale()sets the<html lang>attributem.app_title()sets the document title
That means the rendered document language and metadata are driven by the same locale resolution flow as the rest of the app.
Message files live in the repo root messages/ directory, not inside apps/web.
Right now the active catalog is:
messages/pt-br.json
This location matters because the inlang project is repository-level configuration, not app-local configuration. Keeping messages near project.inlang makes it easier to:
- share locale configuration across future apps
- centralize translation management
- avoid duplicating i18n setup if more frontends are added later
Paraglide writes generated runtime files into:
apps/web/src/paraglide/
Those files are intentionally ignored by git:
- root
.gitignoreignores**/paraglide/ apps/web/.gitignoreignoressrc/paraglide/
Do not hand-edit generated Paraglide output. Treat project.inlang/settings.json and messages/*.json as the editable inputs, and let the plugin regenerate the runtime layer.
When you need to add a new translated string:
- Add the message key to
messages/pt-br.json - Use the generated function in app code via
m.your_key() - Run the web app so Vite regenerates the Paraglide output
- Verify the string appears in the correct route or component
For example, adding:
{
"profile_title": "Perfil"
}would allow usage like:
m.profile_title();If you later introduce interpolation, Paraglide keeps that usage explicit. A message such as:
{
"welcome_user": "Bem-vindo {username}"
}is consumed as:
m.welcome_user({ username: session.user.name });The current setup is already structured for expansion. To add another locale in the future, the typical workflow is:
- Add the locale to
project.inlang/settings.json - Create a new message file in
messages/, such asmessages/en.json - Add localized route mappings in
apps/web/vite.config.tsif needed - Start the app and verify generated Paraglide output updates correctly
- Test localized URLs, locale detection, and document metadata
Because the router, middleware, and runtime hooks are already integrated, adding languages should mostly be a content and configuration task rather than an architectural rewrite.
Paraglide fits this workspace especially well because it keeps concerns separated cleanly:
- repo-level locale configuration lives in
project.inlang - human-edited translations live in
messages/ - generated runtime artifacts stay inside the consuming app
- app code only imports typed runtime helpers
That separation matches the broader Nx monorepo structure used throughout this repository: source inputs stay explicit, generated output stays disposable, and application code depends on stable typed interfaces instead of ad hoc string lookups.
bun run dev
bun run build
bun run check-types
bun run lint
bun run lint:fix
bun run format
bun run checkDatabase commands:
bun run db:start
bun run db:push
bun run db:generate
bun run db:migrate
bun run db:studio
bun run db:stop
bun run db:down- Use Bun from the repo root for workspace commands
- Use Nx targets for app and package workflows
- Shared product code belongs in
packages/* - Platform and infrastructure code belongs in
core/* - Repo-wide config and guardrails belong in
tooling/* - This project uses Base UI, not Radix UI; use the
renderprop instead ofasChild - Do not edit generated Paraglide files in
apps/web/src/paraglide
See CONTRIBUTING.md for contributor setup, workflow, validation expectations, and guidance for working with the Paraglide localization pipeline.
template-nx is designed for teams that want more than a starter app. It gives you a structured foundation for shipping production software with clear module boundaries, reusable packages, and tooling that scales with the codebase.