Pull requests are welcome 🎉. Read README.md for what docs.plus is and how to run it. Read this file for how to change it, and how to get a change merged.
CODE_OF_CONDUCT.md is the Contributor Covenant. It binds every contributor and every maintainer. Read it before you take part.
Install Docker, Bun and Node.js first. README Prerequisites lists the versions, the macOS and Windows notes, and the Bun-only rule.
- Git - Install
-
Fork the repository on GitHub
-
Clone your fork:
git clone https://github.com/YOUR_USERNAME/docs.plus.git cd docs.plus -
Add upstream remote:
git remote add upstream https://github.com/docs-plus/docs.plus.git
-
Start the local stack:
make dev-local
URLs, sign-in notes, stop commands and the full-Docker alternative live in the README Quick Start.
-
Check the environment when something is wrong:
bun run doctor
It checks Webapp 3000, Admin Dashboard 3100, REST API 4000, WebSocket Server 4001, Worker 4002, PostgreSQL 5432, Redis 6379, Supabase API 54321 and Supabase Studio 54323.
The port block never fails. It reports a warning when only some ports are in use, and a warning still exits 0. When it warns, read the
In useline and start any service you still need.An idle 3100 is normal.
make dev-localstarts no Admin Dashboard. Start it withbun run dev:adminfrom the README Command Reference.bun run doctorexits 1 only for a real failure. The failing checks are a Bun version below the floor, a missingdockerordocker compose, a stopped Docker daemon, a missing.env.development, and a missingnode_modules.
Pick from any of these:
- 🐛 Bug Fixes: Fix issues reported in GitHub Issues
- ✨ Features: Implement new features (check Issues for ideas)
- 📚 Documentation: Improve docs, add examples, fix typos
- 🧪 Tests: Pin a reported regression with a Cypress E2E test
- 🎨 UI/UX: Improve design, accessibility, user experience
- ⚡ Performance: Optimize code, reduce bundle size
- 🔒 Security: Report or fix security issues
- 🔌 Extensions: Work on one of the five
@docs.plus/extension-*packages
Extension work has its own per-package scoping. That scoping lives in extensions/README.md. Testing below states what the extension gates cost to run.
The repository layout lives in README Project Structure.
The repository's durable conventions live in AGENTS.md — package manager, commit policy, code quality, test policy. Rules that apply to one area live in that area's CLAUDE.md: apps/webapp/, apps/hocuspocus.server/, apps/admin-dashboard/, packages/supabase/, extensions/, plus the two nested files apps/webapp/src/components/TipTap/CLAUDE.md and apps/webapp/src/components/chatroom/CLAUDE.md. AGENTS.md §Filed by directory maps every section to its file.
Two more rule files are outside that set. CONTEXT.md at the repository root is the domain glossary. extensions/extension-hyperlink/AGENTS.md is package-local, and you read it in addition to the root file when you work inside that package.
Claude Code loads a directory's CLAUDE.md when it reads a file under that path. Cursor does not read CLAUDE.md at all, so Cursor gets the same rules through .cursor/rules/agent-rules-and-memory.mdc. Skim the files that cover the area you change.
bun run check:agent-docs verifies that every cross-reference between those files still resolves. It runs as part of bun run check.
Create a new branch for your changes:
git checkout -b type/descriptionBranch naming conventions:
feature/- New featuresfix/- Bug fixesrefactor/- Code refactoringdocs/- Documentation updatestest/- Test additions/updateschore/- Maintenance tasks
Examples:
feature/add-dark-modefix/resolve-memory-leakdocs/update-api-docs
Branch names are a convention. No hook and no workflow check them.
This repository follows the Conventional Commits format:
<type>(<scope>): <description>
[optional body]
[optional footer]
Types: The commit-msg hook accepts these eleven types:
feat: New featurefix: Bug fixdocs: Documentation changesstyle: Code style changes (formatting, etc.)refactor: Code refactoringperf: Performance worktest: Adding or updating testsbuild: Build system or dependency changesci: CI configuration changeschore: Maintenance tasksrevert: Revert of an earlier commit
Examples:
feat(webapp): add dark mode toggle
fix(api): resolve memory leak in document sync
docs(readme): update installation instructions
refactor(editor): simplify toolbar component
(build): … is a separate form. It is a maintainer-only production deploy trigger, parsed by .github/scripts/parse-build-trigger.sh, and a contributor never uses it.
Prettier formats the code. Check formatting:
bun run formatOr auto-fix formatting:
bun run format:fixESLint checks code quality. Fix the findings:
bun run lint:fixHusky runs local quality gates before code reaches a remote branch.
.husky/*contains lightweight wrapper scripts.scripts/hooks/*.shcontains the actual hook logic.- Active hooks:
pre-commit: runsbun run lint-staged(staged-file lint and format checks, Prettier included)commit-msg: validates commit message formatpre-push: runs selective build checks, then always runsbun run check:push(lint + lint:styles + typecheck)post-merge: runsbun installwhenpackage.jsonorbun.lockchanges
Run the push gate without pushing:
bun run pre-pushThat script runs bun run check:push and no build. The git hook itself also runs the selective extension, admin-dashboard and webapp builds, so the hook takes minutes.
For the same quality gates GitHub Actions runs, call this before you push (opt-in; not the hook):
bun run check:ciTrigger the other two hooks by hand. Run both from inside the repository, because each wrapper resolves the repository root with git rev-parse --show-toplevel and exits 1 when it cannot.
# pre-commit (staged file checks)
sh .husky/pre-commit
# commit message validation
echo "feat(webapp): verify hook docs" > /tmp/commit-msg.txt
sh .husky/commit-msg /tmp/commit-msg.txt- Use TypeScript for all new code
- Avoid
anytypes - use proper types orunknown - Enable strict mode in your IDE
- Run type checking:
bun run typecheck
Full naming convention: .cursor/docs/scripts-naming-convention.md. Quick reference:
| Command | Use case |
|---|---|
bun run check |
Full local report: lint + lint:styles + format + typecheck + check:agent-docs |
bun run check:ci |
Local replica of the prod quality gates. Run this before you push. |
bun run check:push |
Pre-push hook: lint + lint:styles + typecheck (no format) |
bun run check:fix |
Auto-fix all: ESLint + Stylelint + Prettier (in that order) |
bun run lint |
ESLint report |
bun run lint:fix |
ESLint --fix |
bun run lint:styles |
Stylelint report |
bun run lint:styles:fix |
Stylelint --fix |
bun run format |
Prettier --check |
bun run format:fix |
Prettier --write |
bun run typecheck |
tsc --noEmit (report only — no fix) |
bun run lint-staged |
Staged-file lint/format, run by the pre-commit hook |
bun run check:agent-docs |
Cross-reference check across AGENTS.md, CONTEXT.md and every CLAUDE.md |
bun run test # extension gates + webapp Jest + E2E, report saved to Notes/
bun run test:unit # extension gates + webapp Jest
bun run test:e2e # E2E only (4 workers) — needs BASE_URL, see E2E Tests belowbun run test:unit is not a unit-only run. It runs the five @docs.plus/extension-* extension gates. Each gate builds its package, then runs a clean-room Cypress suite in a browser. bun run test:unit then runs the webapp Jest suite. Budget about 45 minutes. bun run test inherits the same cost.
bun run test and bun run test:e2e both need a running local stack, and both need a BASE_URL override. See E2E Tests below before you run either one. An unreachable BASE_URL makes the script print Continue anyway? [y/N], and it aborts on any answer other than y or Y.
When one extension gate fails, run that package's gate on its own. The EXT_ONLY scoping lives in extensions/README.md. The clean-room port for each package lives in the package table at the top of that same file.
Run the webapp Jest suite on its own:
bun run --filter @docs.plus/webapp testThat skips the five extension gates.
bun run test:e2e runs every Cypress spec under apps/webapp/cypress/e2e/, which today means editor/, chatroom/ and document/. The run fails when the number of specs the workers ran does not match the number of spec files on disk. Those specs run in parallel across multiple workers using cypress-split. cypress-split splits spec files across N Cypress instances for faster feedback.
# Interactive mode (single instance)
bun run --filter @docs.plus/webapp cypress:open
# Interactive against a port-3000 stack, from the repository root
bunx cypress open --project apps/webapp --config baseUrl=http://localhost:3000
# Headless — parallel (default 4 workers)
BASE_URL=http://localhost:3000 bun run test:e2e
# Custom worker count via env var
CYPRESS_PARALLEL=2 bun run test:e2e
CYPRESS_PARALLEL=6 bun run test:e2e
CYPRESS_PARALLEL=8 bun run test:e2eBefore you run E2E: Start the local stack.
make dev-localserves the webapp on port 3000. Read theWEBAPPline in themake dev-localoutput to confirm the real port, because Next moves to the next free port when 3000 is busy.scripts/run-tests.shdefaultsBASE_URLtohttp://localhost:3001, so pass the port yourself:BASE_URL=http://localhost:3000 bun run test:e2e. One spec,apps/webapp/cypress/e2e/document/draft-first-edit-anchor.cy.ts, hardcodeshttp://localhost:3001and ignoresBASE_URL, so it fails against a port-3000 stack until a maintainer fixes it.
BASE_URL reaches bun run test and bun run test:e2e only. cypress:open is a bare bunx cypress open, and apps/webapp/cypress.config.ts sets no baseUrl. Specs that call visitEditor still reach http://localhost:3001, through the fallback in apps/webapp/cypress/support/commands.ts. A relative cy.visit has no base URL to resolve against, so pass --config baseUrl=http://localhost:3000, the same flag scripts/run-tests.sh uses.
Choosing a worker count:
| Workers | RAM needed | Best for |
|---|---|---|
| 2 | ~1 GB | Low-resource machines, CI containers |
| 4 | ~2 GB | Default — good balance on most laptops |
| 8 | ~4 GB | 16 GB+ RAM, 8+ cores |
After a run you get an aggregated report with per-worker stats, timing breakdown, and parallelism factor. scripts/run-tests.sh saves that report to Notes/test-results-*.txt.
Default: do not add a test. Add one only in these three cases.
- A maintainer asks for it.
- The change pins a regression that shipped or was reported.
- The failure mode is a real branching, ordering, race, parsing or projection bug.
Name that failure mode in one sentence in your pull request. If you cannot name it, leave the test out.
When your change adds no test, bun run check plus a manual check of the changed surface is the bar. State in the pull request which check you ran.
Prefer Cypress E2E over unit tests. Write a unit test only for parsers, projections, schema validators and pure utilities. Run any test you add and watch it pass.
When a test is warranted:
- Test behavior, not implementation
- Use descriptive names that describe behavior, never a ticket ID
- Keep tests focused
AGENTS.md §Test Policy is the authority here. It also lists the test shapes that maintainers always delete.
After a SQL change, regenerate the Supabase types:
bun run --filter @docs.plus/supabase_back typesapps/webapp/src/types/supabase.ts is generated output. Never hand-edit it. Include the regenerated file in the same change.
Run the other scripts in the same package with bun --filter @docs.plus/supabase_back <script>: start, stop, status, reset and seed. Both bun --filter and bun run --filter work, so the README form and this form match.
The SQL, migration and RLS rules live in packages/supabase/CLAUDE.md. Read it before you write SQL.
When you add or change a Dockerfile, for a new service or a new image:
- Monorepo, no flatten: Keep the workspace layout (
apps/<name>,extensions/<name>,packages/<name>) in the image; do not copy a single package to/appand discard the rest. - One
bun installper stage; do not copynode_modulesbetween stages (Bun symlinks break). - Minimal copy set: Root workspace files + full copies only of packages the service needs; use stub
package.jsonfor other workspaces so the lockfile resolves.
Copy a new workspace member's package.json into all three Dockerfiles — apps/webapp/docker/Dockerfile.bun, apps/hocuspocus.server/docker/Dockerfile.bun and apps/admin-dashboard/docker/Dockerfile.bun — before any --frozen-lockfile install.
A merged contribution ships under the repository license, MIT. Read LICENSE before you submit.
-
Update your fork:
git fetch upstream git checkout main git merge upstream/main
-
Rebase your branch (if needed):
git checkout your-branch git rebase main
-
Run checks before opening a pull request:
bun run check:ci
That command is the local replica of the prod quality gates. It prints pass / fail / skip for each gate.
bun run checkstill coverscheck:agent-docs, which CI does not run. Pre-push staysbun run check:push(lint + lint:styles + typecheck; no full-repo Prettier). CI runsbun run lint,bun run formatandbun run lint:stylesin one job, andbun run typecheckin a second job. -
Test locally:
- Start the local stack
- Test your changes manually
- Verify no regressions
-
Push your branch:
git push origin your-branch # first push git push --force-with-lease origin your-branch # after a rebase
A rebase rewrites the branch history, so the remote rejects a plain push to a branch you already pushed. Never use
--force. -
Create a pull request on GitHub:
- Use a clear, descriptive title
- Fill out the pull request template at
.github/PULL_REQUEST_TEMPLATE.md - Reference any related issues
- Add screenshots/GIFs for UI changes
-
Pull request checklist:
- Code follows style guidelines
-
bun run check:cipasses - Every test I added passes
- Documentation updated (if needed)
- No console errors/warnings
- Changes are backward compatible (if applicable)
- A maintainer reviews your pull request
- Address feedback promptly
- Keep each pull request focused and reasonably sized
- Be open to suggestions and improvements
- 💬 Discord: Join our server for real-time help
- 🐛 Issues: GitHub Issues for bug reports
- 🔒 Security: See SECURITY.md for reporting vulnerabilities
- 📧 Email: contact@newspeak.house
Ask in Discord before you start a large change. A maintainer can tell you early whether the idea fits.
Your contributions make docs.plus better for everyone. Thank you for taking the time to contribute! ❤️