12 KiB
Contributing
This guide covers running Oxynote from a source checkout and the commands
you need day to day. Everything is driven from the repository root through
make; the component-specific docs are linked at the end.
Prerequisites
| Tool | Used for |
|---|---|
| Docker with Compose | the dev stack (Postgres, Caddy, the app containers, …) and the Go database-layer tests, which start throwaway containers |
| Go 1.27+ | server/core and datagen |
| Node.js 24+ | web, server/auth-realtime, e2e and the production launcher |
| pnpm | package manager for the Node packages. Any recent install works: pnpm switches to the version pinned in each package's packageManager field on its own |
| make | orchestrates every setup, build and run command |
| goreleaser | builds the Go binaries and their docker images — needed by every target that starts a stack |
| golangci-lint | lints the Go code (make lint, make check-lint) |
| moq | only to regenerate the Go test mocks with go generate; the generated files are committed |
First run
make setup # install dependencies + create the local env files from templates
make start # build all images + run the dev stack in the background
Then open http://localhost:8080 and sign up. make stop stops the stack.
Everyday commands
Running the dev stack
make run # foreground: streams all logs, ctrl-c stops the stack
make start # the same, in the background
make stop # stop the background stack
make dev # backend containers + the web dev server on the host
make dev is the frontend loop: the backend runs in containers and Nuxt
runs on the host with hot reload on http://localhost:3000. Ctrl-c stops the
dev server; make stop stops the containers.
All three targets rebuild the images before starting, so a code change is picked up by simply running them again.
Environment files
The dev stack reads docker/env/*.local.env, which make setup creates
from the committed *.example.env templates. The files are gitignored and
the defaults work out of the box. Integrations (GitHub app, Slack app, AI
assistant, email, changedetection.io) are optional and stay disabled until
their variables are set; the frontend hides features the deployment does
not offer. Search is built in: core keeps its index on the container's
volume and rebuilds it from Postgres when it is missing.
make check-env # report variables that drifted between the templates and your local files
make sync-env # rewrite the local files from the templates, keeping your values
make setup-force # setup, but the local files are recreated from the templates, values and all
make setup only copies a template when the local file is missing, so a
variable renamed or added in a template never reaches an existing local
file. The dev stack targets run make check-env first and refuse to start
on drift — make sync-env fixes it.
Linting
make lint # fix lint, format and type issues in every component
make check-lint # the same gates, verification only (what CI runs)
End-to-end tests
make e2e-dev # build the dev stack, run the Playwright suite, tear it down
make e2e-prod # the same suite against the production all-in-one image
Both are one-shot: every run builds the stack and drops it afterwards. To
iterate on tests, build the stack once with make e2e-dev-stack-build (or
make e2e-prod-stack-build), then run pnpm test:dev (or pnpm test:prod)
from e2e/ as often as you like, and finish with
make e2e-dev-stack-stop (or make e2e-prod-stack-stop). The e2e stacks
use their own ports and throwaway state, so they run alongside the dev stack.
Production image
make prod-build # build the all-in-one image
make prod-run # build it, then run the example deployment on :8080 (ctrl-c stops it)
make prod-stop # stop it
make prod-run also starts a Mailpit on http://localhost:8025 so the
signup verification mail is readable. Stop the dev stack first — both listen
on :8080.
Ports
| Port | Dev stack |
|---|---|
8080 |
front door: the app, with the APIs under /core and /auth-realtime |
8082 |
changedetection.io |
8083 |
Grafana |
8084 |
Mailpit (dev email inbox) |
3000 |
the web dev server, only with make dev |
The e2e dev stack listens on 18080 (Mailpit on 18025) and the e2e prod
stack on 19080 (19025).
Repository layout
web/— Nuxt 4 + Vue 3 frontend; ships as both a web app (SSR) and an Electron desktop app. See web/README.md.server/core/— Go API server: storage, search, integrations, AI pipelines. See server/README.md.server/auth-realtime/— Node service running Better Auth and a Hocuspocus (Yjs) real-time server.datagen/— demo-data generator (Go); demo/testing only.e2e/— Playwright end-to-end suite and the docker-compose stacks it drives.docker/— dev docker-compose stack, Caddyfile, env templates, demo-data configs.docker/prod/— the production all-in-one image and its example deployment. See docker/prod/README.md.scripts/— helpers the root Makefile calls.
AI coding agents
Instructions for coding agents live in AGENTS.md at the repository root,
in each component directory, and in a few core packages with rules of their
own; the root file maps the components and links their files. Every
CLAUDE.md next to one is a one-line import that points Claude Code at the
AGENTS.md beside it — edit AGENTS.md, never the shim.
Adding a data source
Decide first what you are adding. If the new type is compatible with an existing processor, as MariaDB is with the MySQL one, it needs no processor of its own, only the wiring in the steps below. Otherwise it needs its own processor, client interface, routes and assistant tools.
- Create the type constant. The type constant, the credential rules
and the runner that opens a client for a stored data source all live
in
server/core/internal/datasource/. Add the constant, then follow an existing type through the package: the credential check, the runner's per-type construction and the runner method that returns the client, which refuses types it does not accept. A type with its own processor also gets its client interface there, with a mock regenerated bygo generate(moq is in the prerequisites). - Write the processor.
server/core/internal/datasource/processor/holds one processor per kind of source. It connects, tests the connection, including that the credentials cannot write (every data source is read-only), maps errors to a connection status, runs queries and shapes their results. What a new source needs ranges from a set of macro definitions to a whole client; reuse what the existing processors share, such as time-range handling, the$__macros, SQL metadata and result transformation, and add only what the new source does differently. - Add the assistant tools. Make sure every action the data source
offers is also an assistant tool in
server/core/internal/assistant/tools/; the MCP server exposes the same tools.- If metric blocks draw from the new type, add a case to the query
handler in
server/core/internal/server/internal/datasource/and to the simulation checker inserver/core/internal/datasource/simulation/, which decides when a metric block stops showing generated data; without a case it never does.
- If metric blocks draw from the new type, add a case to the query
handler in
- Add the type to the web app. The frontend knows the types through
the enum in
web/app/utils/api/data-source/. From there, follow an existing type throughweb/app/components/: the status icon, the settings section that lists connectable sources (without it the type cannot be added) and the read-only warning in the connect dialog. Their strings live inweb/i18n/locales/. The help and warning links are runtime config, declared inweb/nuxt.config.tsandweb/index.d.tsand set indocker/env/web.example.envand the production launcher's env mapping underdocker/prod/launcher/.- If metric blocks draw from the new type, add the query editor's
language support and the query help popover under
editor/blocks/metrics/config-modal/.
- If metric blocks draw from the new type, add the query editor's
language support and the query help popover under
- Add demo data (optional).
datagen/fills the demo databases anddocker/runs them. Add a generator package and a block in the datagen command that runs it when its env variables are set, a compose service with its init script underdocker/demo/, thenmake sync-env. - Extend the tests and docs. Every place above that switches on the
type has a table-driven test with one row per type; extend the tables
in the packages you touched, and the unit test beside each web
component. The processor tests start a throwaway container per
database, so a new one needs a container in their
TestMainand Docker to run. Assistant tool descriptions are compared against a golden file; regenerate it fromserver/core/withUPDATE_GOLDEN=1 go test -run Test_Info_toEino ./internal/assistant/tools/and review the diff. Update the supported-sources line inREADME.md, and the prerequisites above if a new container became a test dependency. The e2e suite creates no data sources and needs nothing.
Adding a hook
Decide first whether the hook depends on an integration the deployment
may not have, the way GitHub tracking needs the GitHub app and the
website watcher needs changedetection.io. Such a hook also needs a
client under server/core/internal/apps/, a capability flag and the
gating in step 3.
- Create the type constant. The type constant, its validation and
its human-readable name (used in notification text), and the switch
that turns a stored hook into a running processor all live in
server/core/internal/document/hook/. Add the constant, then follow an existing type through the package. A hook with its own client gets it through the input that hands processors their clients, declared in the processor package and implemented in this one. - Write the processor.
server/core/internal/document/hook/processor/holds one processor per type. Its struct is the hook's settings, and it computes a freshness score and a state from them, resets that state, and tears down whatever it holds outside the deployment. - Gate on the integration. If the hook depends on an integration,
three places refuse or skip it when the deployment lacks it: the
manager in
server/core/internal/document/hook/manager/skips it while processing, the assistant's create refuses it, and the capabilities endpoint inserver/core/internal/server/reports whether it is configured, so the web app can hide it. - Add the assistant tools. The hook tools in
server/core/internal/assistant/tools/take settings as one object whose shape the type decides: add the type's schema variant and its decoding case. - Add the type to the web app. The frontend knows the types, their
settings and their state through
web/app/utils/api/document.ts. Add a directory with the config menu underweb/app/components/editor/hooks/, then follow an existing type through the menu content beside it (the component and props switches and the add-menu entry, gated on the capability when the hook depends on one) and through the notification box inweb/app/components/. Their strings live inweb/i18n/locales/. - Extend the tests. Every place above that switches on the type has
a table-driven test with one row per type; extend the tables in the
packages you touched, and the unit test beside each web component.
Assistant tool descriptions are compared against a golden file;
regenerate it from
server/core/withUPDATE_GOLDEN=1 go test -run Test_Info_toEino ./internal/assistant/tools/and review the diff. The e2e suite creates no hooks and needs nothing.
Before opening a pull request
Run make check-lint. CI runs the same gates per component, plus the unit
tests and both end-to-end suites.