Contributing
Run Voysse locally, run its tests and follow its conventions.
Run Voysse locally, run its tests and follow its conventions. For the services and data model see Architecture; for deploying an install see Self-hosting.
Contents
Local development
Docker Compose (make up, see Self-hosting) is
the fastest way to run the whole stack. To iterate on one service with hot
reload, run it outside Docker instead — each service still needs the others
reachable (PostgreSQL at minimum; the backend for the frontend and the
WhatsApp bridge).
Requirements: Python 3.12+, Node.js 20+, PostgreSQL 14+ running locally.
# 1) databases — a main DB and a separate one for tests
psql -d postgres -c "CREATE ROLE openvoiss LOGIN PASSWORD 'openvoiss';"
createdb -O openvoiss openvoiss
createdb -O openvoiss openvoiss_test
# 2) env
cp .env.example .env # then set SECRET_KEY and ENCRYPTION_KEY
# 3) backend — http://localhost:8000, docs at /docs
python3 -m venv .venv && source .venv/bin/activate
pip install -r apps/api/requirements.txt
cd apps/api && alembic upgrade head && uvicorn app.main:app --reload --port 8000
# 4) frontend (new terminal) — http://localhost:3000
cd apps/web && npm install && npm run dev
# 5) WhatsApp bridge (new terminal) — listens on 127.0.0.1:3101
cd apps/whatsapp && npm install && npm run devMigrations must run (alembic upgrade head) before the backend starts — the
app does not apply them automatically outside Docker. See .env.example for
the full local variable list (DATABASE_URL, BACKEND_URL,
WHATSAPP_BRIDGE_URL, WHATSAPP_BRIDGE_PORT, …); ⚠️ ENCRYPTION_KEY must
never change once you have real secrets/sessions stored under it.
apps/whatsapp/package.json also exposes npm run start (runs once, no
watch) — used by the self-hosting run-without-Docker
flow for a plain long-running process;
npm run dev (tsx watch) is the one you want while actively editing bridge
code.
Tests
# backend — needs the separate openvoiss_test DB from step 1 above; tables
# are dropped and recreated per test (see apps/api/tests/conftest.py), so
# never point TEST_DATABASE_URL at a database you care about
cd apps/api
pytest -q
pytest tests/test_flows.py::test_register_login_logout_and_me -v # single test
# frontend
cd apps/web
npm run lint # eslint .
npm run build # next build
# WhatsApp bridge
cd apps/whatsapp
npm test # tsx --test tests/**/*.test.ts (node's built-in test runner)
npm run build # tsc -p tsconfig.json (typecheck only, no emitted output used)apps/api/tests/conftest.py points DATABASE_URL at
postgresql+psycopg://openvoiss:openvoiss@localhost:5432/openvoiss_test by
default; override it with TEST_DATABASE_URL (e.g. for the Dockerized
POSTGRES_TEST_DB). It also forces RATE_LIMIT_ENABLED=false for the test
process, so the login/widget limiters never interfere with a test run. Test
config lives in apps/api/pyproject.toml
([tool.pytest.ini_options]: testpaths = ["tests"]).
Inside Docker, make test runs the backend pytest suite and rebuilds the
web/whatsapp images' validation stages (lint/build/typecheck), matching
the commands above without needing a local Python/Node toolchain. See
Tests in the self-hosting guide for that path,
including how to re-test migrations from scratch
(alembic downgrade base && alembic upgrade head).
Conventions
- English only in code, routes, identifiers, comments, commit messages and
docs — always. The single deliberate exception is the LLM system prompt
assembled in
apps/api/app/services/knowledge.py(build_system_prompt()), which is kept in the customer's own language on purpose — it's what the agent says to end users, not developer-facing text. Everything user-facing in the UI goes behind the typed i18n system instead of being hardcoded in another language: seeapps/web/lib/i18n(en.tsis the default locale,es.tsis Spanish;index.tsxwires the provider). - Conventional commits.
git log --onelineshows a consistenttype: description/type(scope): descriptionpattern — e.g.feat: update marketing landing page content,feat(web): adopt Base Rhea dashboard and brand system,chore: adopt FSL-1.1-MIT license and refresh READMEs,refactor: migrate dashboard forms and CTAs to shadcn components. Match that style (feat,fix,chore,refactor,docs,test, …; scope in parentheses is optional and usually an app name likewebormarketing). - Repo hygiene hook. Enable it once per clone:
git config core.hooksPath .githooks.githooks/pre-commitblocks committing local-only paths (work/,internal/,*.local.md) and any staged content matching a term in the gitignoredwork/forbidden-words.txt, or containing aDO-NOT-COMMIT/INTERNAL-ONLYmarker. Keep internal notes and roadmap items inwork/(gitignored) — never in the tracked repo. - Tenant isolation is a review-blocking concern. Any new or changed
backend query that reads/writes agency-scoped data must filter by the
authenticated user's
agency_id— see Tenant isolation. There's no database constraint enforcing this; it's app-level discipline, checked in review.
Migrations
apps/api/migrations/versions/ uses sequential, zero-padded, hand-authored
revision IDs — 0001_initial.py, 0002_white_label_portal.py, …,
0017_whatsapp_cloud_channel.py — each file's revision string matches its
filename, and down_revision points at the previous one in the sequence.
There's no autogenerate-and-rename script in the repo; the practical workflow
observed from the existing files is:
cd apps/api
alembic revision -m "short description" # creates a new revision filethen rename the generated file (and set revision/down_revision inside it)
to continue the existing NNNN_ numbering, and write upgrade()/downgrade()
by hand against sqlalchemy as sa / alembic.op (see any file under
migrations/versions/ for the established style — explicit
create_table/add_column/create_index/create_foreign_key calls, matching
downgrade() that undoes them in reverse order). env.py points
target_metadata at Base.metadata from apps/api/app/models.py, so
alembic revision --autogenerate will also pick up model changes if you
prefer to start from a diff and then tidy the generated file.
Docker runs alembic upgrade head automatically on backend container start
(see Upgrade); locally you run it yourself
(cd apps/api && alembic upgrade head, per Local
development above).
Opening a PR
Branches and commits in this repo follow plain conventional-commit style (see
Conventions) rather than an enforced branch-naming scheme —
name your branch after the change (feat/..., fix/..., or similar) and keep
commits scoped to one logical change each.
Before a non-trivial change:
- Read Architecture if it touches the data model, a router, a service, or the WhatsApp bridge — it explains the tenant-isolation rule and where each concern lives.
- Read Self-hosting if it touches Docker Compose, the gateway, environment variables, or anything operators depend on.
- Run the relevant test suite from Tests before pushing;
apps/webandapps/whatsapptypecheck as part of theirbuildscript, so a broken type is a broken build, not just a broken test.
Voysse