A monorepo template for building full-stack JavaScript applications with Node.js, Express, and Vue. It combines backend and frontend apps in apps/, and shared reusable code in common/. Designed as an updateable template — custom code is isolated so upstream template updates can be merged cleanly.
- Node.js 24+ required, npm 11+ required
- Fully ES Modules — no CommonJS
- No TypeScript — JSDoc used for typing and IDE autocomplete
apps/ # backend and frontend apps (npm workspace)
sample-api/ # sample backend app — copy and rename, do not develop here directly
sample-mcp/ # MCP server example
sample-vue-full/ # full-featured sample Vue app (port 8081)
sample-vue-minimal/ # minimal Vue app (port 8080)
common/ # shared reusable code (npm workspaces)
iso/ # isomorphic utilities (runs in Node and browser)
node/ # Node.js modules, Express middleware and services
schemas/ # shared zod schemas
web/ # browser-only utilities and web components
vue/ # Vue-specific shared modules
docs/ # project documentation
scripts/ # DB deployment, OpenAPI generation tooling
dbdeploy/ # database migration and seed scripts
.github/ # GitHub Actions workflows and CONTRIBUTING.md
.githooks/ # native git hooks (pre-commit, pre-push)
# 1. run once after clone/fork/git init to enable upstream template merges
./setup-upstream.sh
# 2. install all workspace dependencies
npm i
# 3. configure git hooks (also runs automatically via npm prepare)
chmod +x .githooks/setup.sh && ./.githooks/setup.sh
# or manually:
# git config core.hooksPath .githooks
# 4. run the sample backend
cd apps/sample-api && npm run start
# 5. run the minimal Vue frontend
cd apps/sample-vue-minimal && npm run dev# linting and formatting (biome)
npm run lint # lint all workspaces
npm run lint:fix # auto-fix unsafe lint rules
npm run format # format all workspaces
npm run check # biome check with auto-fix (lint + format)
npm run ci # biome ci (used in CI/CD)
# testing
npm run test:workspace # run tests in apps/sample-api
npm run test:workspaces # run tests in all workspaces
# openapi docs
npm run docs:generate # generate merged openapi yaml
npm run docs:validate # validate openapi docs
# workspace management
npm ls -ws # list all workspaces
npm i <pkg> --workspace=<path> # install into a specific workspace
npm outdated -ws # check outdated packages across all workspaces| URL | Purpose |
|---|---|
http://127.0.0.1:3000/api/healthcheck |
Health check |
http://127.0.0.1:3000 |
Express app with samples |
http://127.0.0.1:3000/native/index.html |
Unbundled Vue sample |
- Copy
apps/sample-apito a new folder inapps/using kebab-case naming - Edit
.envand.env.jsonin the new folder as needed - Inject secrets from environment variables or a secret manager — never commit secrets
- Do not develop directly in
sample-api
- Copy
apps/sample-vue-fullto a new folder inapps/using kebab-case naming - Edit
.envand.env.developmentas needed - Routes use kebab-case and support up to 1 submenu level
Read docs/conventions.md before making code changes.
- Formatter and linter:
biome— runnpm run checkto auto-fix - No
console.*in backend — import and usecommon/node/loggeras globallogger - No
console.*in frontend production — errors go to Sentry - Config loading: use
common/node/configfor app config .env.json: non-sensitive structured settings, exposed viaglobalThis.__config.env: sensitive values loaded intoprocess.env- Exports: named exports preferred; default exports only for a single class, config, or plugin
- No barrel
index.jsfiles - No default and named exports in the same file
- Use native: test runner, datetime, fetch, npm, git hooks — avoid heavy libraries where possible
- Validation: use
zodfor all input validation and OpenAPI schema generation - Globals pattern (when needed):
globalThis.__myApp = globalThis.__myApp || {} const _key = Symbol('key') globalThis.__myApp[_key] = value
- Mark incomplete or planned work with
TODO
Commit messages must follow Conventional Commits.
Only three types are allowed:
| Type | Use for |
|---|---|
feat |
new behaviour the user/consumer sees |
fix |
corrects broken behaviour |
chore |
everything else — build, ci, docs, style, refactor, perf, test, revert |
Format: type(scope): short description
Breaking changes: add ! before the colon — feat(auth)!: replace token format
Use czg for interactive or AI-assisted commit messages:
npx czg # interactive prompt
npx czg --ai # AI-generated message (requires API key)Hooks live in .githooks/ and are activated by npm install (via npm prepare) or manually via git config core.hooksPath .githooks.
Pre-commit (runs on git commit):
- Biome format and lint on affected directories
- Schema validation tests for affected schema directories
Pre-push (runs on git push):
- Unit tests (
npm run test:workspace) - Schema validation tests
Skip hooks temporarily:
git commit --no-verify
git push --no-verify| Branch | Branch from | Merge to | Notes |
|---|---|---|---|
rel/1.0 |
main |
main when production ready |
Active dev branch |
feat/scope/name |
rel/1.0 |
rel/1.0 via PR |
New features |
fix/scope/name |
rel/1.0 |
rel/1.0 via PR |
Bug fixes |
chore/scope/name |
rel/1.0 |
rel/1.0 via PR |
Maintenance |
hotfix/scope/name |
main |
main + rel/1.0 + rel/2.0 |
Emergency only |
tag: v1.0.0 |
rel/1.0 after merge to main |
— | Full release tag |
tag: v1.0.1 |
rel/1.0 after hotfix merges in |
— | Patch tag only, no merge back to main |
rel/2.0 |
main after v1.0.0 tag |
main when ready |
Next dev cycle |
Key rules:
- Tags always come from
rel/*, never directly frommain mainonly receives merges fromrel/*orhotfix/*- Hotfix backport to next release uses
git cherry-pick, reviewed as a separate PR - PRs use squash merge to keep history clean
- PR titles follow Conventional Commits style:
fix(auth): handle expired token
# commit and push your changes first, then:
git fetch upstream # includes tags
git pull upstream <branch or tag> --no-rebase
# resolve any template-related merge conflictsdocker build -t express-template \
--target production \
--build-arg APP_NAME=sample-api \
--build-arg API_PORT=3000 .
docker run -p 3000:3000 express-template| Workflow | Purpose |
|---|---|
.github/workflows/deploy-cr.yml |
Build and push image to container registry |
.github/workflows/deploy-npm.yml |
Publish a package to npm |
.github/workflows/deploy-bucket.yml |
Deploy Vue frontend to object store |
Required GitHub Secrets:
CR_USERNAME— container registry usernameCR_PASSWORD— container registry password
Required GitHub Variables:
CR_HOST— container registry hostCR_NS— container registry namespaceCR_IMAGENAME— image name (defaults to repo name)
Secrets must never be stored in the repo — use a secrets vault for CI/CD.
The auth module (common/node/auth) supports three authorization layers, all optional and composable:
| Layer | Module | JWT field populated |
|---|---|---|
| RBAC | rbac.js — tenant-scoped roles + permissions |
active_tenant, tenants |
| FGA | openfga.js — fine-grained per-object checks |
roles (flat list) |
| Legacy | DB users.roles column |
roles (flat list, fallback) |
Route middleware available after authUser:
| Middleware | Source | Checks |
|---|---|---|
requireRole(...roles) |
@common/node/auth |
flat req.user.roles — works with all three layers |
requireFga(relation, object) |
@common/node/auth |
OpenFGA tuple lookup |
req.rbac.hasRole(...roles) |
attached by authUser |
tenant-scoped roles in active tenant |
req.rbac.hasPermission(...perms) |
attached by authUser |
tenant-scoped permissions in active tenant |
req.fga.check(relation, object) |
attached by authUser |
ad-hoc FGA check inside a handler |
| File | Purpose |
|---|---|
.github/CONTRIBUTING.md |
Contributor workflow, hooks, issue reporting, PR rules |
docs/conventions.md |
Coding, tooling, commit, and runtime standards |
docs/git.md |
Git workflow, branch/tag patterns, merge strategy |
docs/install-apps.md |
Backend and frontend setup and development guide |
docs/install-common.md |
Shared code and workspace reference |
docs/authz.md |
Authorization — RBAC and FGA: setup, JWT payload, roles fallback chain, usage |
docs/NOTES.md |
Design decisions, caveats, open questions, TODOs |