Contributing
Contributions are welcome! This guide covers everything you need to know as a contributor or developer.
Development Setup
bash
git clone https://github.com/nubisco/verba.git
cd verba
pnpm install
# Initialise the database
cd apps/api
cp .env.example .env
npx prisma migrate dev
cd ../..
# Start all services
pnpm dev| Service | URL |
|---|---|
| Web app | http://localhost:5173 |
| API | http://localhost:4000 |
| Docs | http://localhost:4173 (run pnpm docs:dev) |
Repository Layout
verba/
├── apps/
│ ├── api/ # Fastify + Prisma backend
│ │ ├── src/
│ │ │ ├── routes/ # Thin HTTP handlers, delegate to services
│ │ │ ├── services/ # Business logic, ACL, workflow
│ │ │ ├── schemas/ # Zod request/response schemas
│ │ │ └── __tests__/ # Vitest unit tests
│ │ └── prisma/ # Schema + migrations
│ └── web/ # Vue 3 + Vite frontend
│ └── src/
│ ├── views/ # Page-level Vue components
│ ├── stores/ # Pinia state
│ └── router/ # Vue Router routes
├── packages/
│ ├── shared/ # Shared TypeScript types
│ └── cli/ # verba setup / verba migrate CLI
└── docs/ # VitePress documentation siteArchitecture Rules (Hard)
- Routes call services, never Prisma directly. Controllers are thin.
- ACL is enforced in services, using
requireProjectRolefromacl.service.ts. - Zod validates all incoming data. Schemas live in
src/schemas/. - All mutations emit a domain event via
bus.emit(payload)inevents.ts. - REST only. No GraphQL, no tRPC.
- SQLite for dev, Postgres for production. Use string constants instead of Prisma enums (SQLite compatibility).
- JWT in httpOnly cookie named
token. Never use theAuthorizationheader. - Tests required for ACL and workflow on every new feature.
Role Hierarchy
ADMIN (4) > MAINTAINER (3) > TRANSLATOR (2) > READER (1)Global admin (isGlobalAdmin: true on User) bypasses project-level membership checks.
Workflow State Machine
| From | To | Who |
|---|---|---|
TODO | IN_PROGRESS | TRANSLATOR+ (automatic on first save) |
IN_PROGRESS | SUBMITTED | TRANSLATOR+ |
SUBMITTED | APPROVED | MAINTAINER+ |
SUBMITTED | IN_PROGRESS | MAINTAINER+ (reject) |
APPROVED | IN_PROGRESS | MAINTAINER+ (reopen) |
Running Tests
bash
cd apps/api
npx vitest run # single run
npx vitest # watch modeAll tests must pass before opening a PR. The test suite covers:
- Service unit tests (mocked Prisma)
- ACL and workflow edge cases
- Event emission and plugin loader
Code Style
- TypeScript strict mode everywhere.
- No
any: useunknownand narrow. - Zod for runtime validation; do not cast request bodies.
- Vue Composition API only (
<script setup>). - Pinia stores for cross-component state.
- Comments only where non-obvious logic needs clarification.
Commit Messages
Follow Conventional Commits:
feat: add namespace-scoped export endpoint
fix: reject invalid workflow transitions in import
docs: update production deployment guide
chore: upgrade vitepress to 1.7
test: add missing ACL tests for READER roleBranch Naming
feat/kanban-board
fix/otp-expiry-edge-case
docs/production-deploymentPull Request Guidelines
- Fork and create a branch from
main. - Ensure all existing tests pass (
npx vitest runinapps/api). - Add tests for new behaviour (ACL coverage required).
- Run
pnpm lintbefore pushing. - Write a clear PR description: what changed, why, and what was tested.
New Feature Checklist
- [ ] Prisma schema updated (if new model/field) + migration created
- [ ] Zod schema in
src/schemas/ - [ ] Service function with
requireProjectRoleguard - [ ] Route registered in
src/routes/ - [ ] Domain event emitted and audit log written
- [ ] Tests: happy path + ACL edge cases
- [ ] Frontend: Vue component +
apiFetchcalls - [ ] Docs updated if user-facing
Open-Core: CE vs. EE
Verba follows an open-core model:
- Community Edition (CE): everything outside
packages/ee/, licensed AGPL-3.0. - Enterprise Edition (EE):
packages/ee/only, proprietary Nubisco license.
Import boundary: CE code must never import from @nubisco/verba-ee. This is enforced by ESLint and a CI grep backstop. EE code may import from @nubisco/verba-shared.
Running in OSS-only mode
By default, the API runs in CE mode. EE features are only loaded when ENABLE_EE=true:
bash
# Pure CE (default)
pnpm dev
# With EE features (requires packages/ee to be built)
ENABLE_EE=true pnpm dev
# Explicitly force OSS-only (ignores ENABLE_EE even if set)
OSS_ONLY=true pnpm devAdding EE features
- All EE code goes in
packages/ee/src/. - EE modules register themselves at boot via the dynamic loader in
apps/api/src/index.ts. - Never import EE code from CE services, routes, or components.
- See
architecture/open-core/for full ADR and boundary documentation.
Plugin System
Verba supports server plugins. A plugin is a file that exports a default VerbaPlugin object:
ts
import type { VerbaPlugin } from '@nubisco/verba-api/plugin-types'
export default {
name: 'my-plugin',
setup(bus) {
bus.on((event) => {
if (event.type === 'translation.approved') {
/* ... */
}
})
},
} satisfies VerbaPluginDrop the file into apps/api/plugins/ and it will be auto-loaded on startup.