Thank you for your interest in contributing to NyxID. This guide covers the development workflow, coding conventions, and quality expectations for the project.
- Code of Conduct
- Getting Started
- Development Workflow
- Architecture Rules
- Backend Conventions
- Frontend Conventions
- Security Requirements
- Testing
- Commit Messages
- Pull Requests
- Documentation
- Resources
Be respectful, constructive, and inclusive. Harassment, discrimination, and bad-faith behavior are not tolerated. We are all here to build something useful together.
| Tool | Version | Purpose |
|---|---|---|
| Rust | 1.85+ | Backend and node-agent compiler |
| Node.js | 20+ | Frontend build tooling |
| MongoDB | 8.0 | Primary database |
| Docker | 24+ | Run MongoDB and Mailpit via Compose |
# Clone the repository
git clone https://github.com/ChronoAIProject/NyxID.git
cd NyxID
# Copy environment template and generate an encryption key
cp .env.example .env
openssl rand -hex 32 # Paste output as ENCRYPTION_KEY in .env
# Start infrastructure (MongoDB on 27018, Mailpit on 8025)
docker compose up -d
# Start the backend (auto-generates RSA keys in dev mode)
cargo run
# In a separate terminal, start the frontend
cd frontend && npm install && npm run devThe backend starts on http://localhost:3001, frontend on http://localhost:3000, and Mailpit web UI on http://localhost:8025.
Install the pre-commit hook to catch formatting and lint issues before they reach CI:
ln -sf ../../scripts/pre-commit .git/hooks/pre-commitThe hook runs cargo fmt --check, cargo clippy, and eslint on staged files. It only checks the relevant tool when files of that type are staged.
curl http://localhost:3001/health
# {"status":"ok","version":"0.2.0"}- Fork and clone the repository
- Create a feature branch from
dev:git checkout -b feature/your-feature dev - Make your changes, following the conventions below
- Run tests and lint before committing (see Testing)
- Commit using conventional commit messages
- Push your branch and open a pull request against
dev
| Prefix | Use Case |
|---|---|
feature/ |
New features |
fix/ |
Bug fixes |
refactor/ |
Code restructuring |
docs/ |
Documentation only |
test/ |
Adding or fixing tests |
chore/ |
Tooling, CI, dependencies |
NyxID follows a strict layered architecture. Understand these boundaries before contributing.
handlers/ --> services/ --> models/
- models/ -- Plain structs with serde derive macros and a
COLLECTION_NAMEconstant. No business logic. - services/ -- Business logic. Takes
&mongodb::Databaseand&strfor IDs. No HTTP types. - handlers/ -- HTTP layer only. Converts
AuthUser.user_id(Uuid) toStringbefore calling services. Uses dedicated response structs -- never serialize model structs directly to API responses. - crypto/ -- Cryptographic operations (JWT, AES, password hashing, token generation).
- mw/ -- Axum middleware (auth extraction, rate limiting, security headers).
- errors/ -- Centralized error types (
AppErrorenum withthiserror).
- Define request/response types in
handlers/<module>.rs - Implement business logic in
services/<module>.rs - Register the route in
routes.rs - Add audit logging where appropriate
These are critical -- incorrect serialization silently breaks data persistence:
- NEVER use
#[serde(skip_serializing)]on model fields -- it preventsinsert_one(&struct)from storing them - ALWAYS use
#[serde(with = "bson::serde_helpers::chrono_datetime_as_bson_datetime")]onDateTime<Utc>fields - For
Option<DateTime<Utc>>, use the custombson_datetime::optionalhelper (inmodels/bson_datetime.rs) - IDs are UUID v4 stored as strings in MongoDB
_idfields - Each model must have a
const COLLECTION_NAME: &strconstant
Use the AppError enum and AppResult<T> type alias:
fn my_handler() -> AppResult<Json<MyResponse>> {
// AppResult<T> = Result<T, AppError>
}Error variants map to HTTP status codes and numeric error codes (1000-3002, 7000, 8000-8003). Internal and database errors must never leak details to clients.
- Use
thiserrorfor error types - Prefer
&stroverStringin service function parameters - Keep handler functions thin -- delegate logic to services
- All key material must use
Zeroizingwrappers - All
Debugimpls on sensitive types must redact secrets
- Validation: Zod schemas in
schemas/directory, one file per domain - Forms: React Hook Form with
@hookform/resolvers/zod - Server state: TanStack Query hooks in
hooks/, one file per domain - Client state: Zustand store (
stores/auth-store.ts) - UI components: Radix UI + shadcn/ui pattern (
components/ui/) - Routing: TanStack Router with type-safe route params
- No
console.login production code
- Many small files over few large files (200-400 lines typical, 800 max)
- Organize by feature/domain, not by type
- Extract reusable components into
components/shared/
The CLI embeds a React-based wizard UI (cli/src/wizard/assets/index.html) into its binary via rust_embed, so cargo build -p nyxid-cli does not need a Node toolchain. The bundle is a committed prebuilt artifact — if you edit any file the wizard pulls in (frontend/src/components/cli-wizard/**, any components/ui/ primitive or hooks/, lib/, schemas/, stores/, pages/cli-pair/ file the wizard imports, frontend/wizard.html, frontend/vite.wizard.config.ts, or frontend/package-lock.json), rebuild the bundle and commit the result in the same PR:
npm --prefix frontend run build:wizard
git add cli/src/wizard/build:wizard now copies the built bundle into cli/src/wizard/assets/index.html, writes the module-graph manifest to cli/src/wizard/bundle-meta/index.manifest, and a source-closure hash to cli/src/wizard/bundle-meta/index.hash. All three files must be committed.
Freshness is enforced by the wizard_bundle_is_fresh integration test in cli/tests/wizard_bundle_freshness.rs, which runs as the dedicated CLI Wizard Bundle Freshness CI job and on cargo test -p nyxid-cli --test wizard_bundle_freshness locally. The test re-hashes the current source closure from the committed manifest and compares against the committed hash — no npm install or vite rebuild is needed. The hash covers every file Vite traversed at build time (so transitive imports like components/ui/button.tsx are caught automatically) plus frontend/package-lock.json, frontend/vite.wizard.config.ts, frontend/wizard.html, frontend/vite-plugins/wizard-manifest.ts, and .node-version.
All contributions must pass these checks before merge:
- No hardcoded secrets (API keys, passwords, tokens) -- use environment variables
- All user inputs validated (Zod on frontend, service-level validation on backend)
- No SQL/NoSQL injection vectors (use parameterized queries)
- No XSS vectors (sanitized HTML, proper escaping)
- Authentication and authorization verified on all protected endpoints
- Error messages do not leak internal details (database errors, stack traces)
- Sensitive data encrypted at rest (use the existing
crypto/aes.rsenvelope encryption) - Rate limiting applied to new endpoints where appropriate
If your change touches authentication, encryption, or credential handling, it will receive extra scrutiny during review.
The full backend suite needs a dedicated, ephemeral MongoDB 8 replica set. It
creates and drops a database per DB-backed test, so start that test-only mongod
with minSnapshotHistoryWindowInSeconds=0; MongoDB's 300-second default retains
dropped WiredTiger idents and file descriptors long enough to exhaust a full
run. This is a server-wide parameter: do not set it on a shared development,
staging, or production MongoDB.
docker run --rm --detach --name nyxid-test-mongodb --publish 27017:27017 \
--ulimit nofile=65536:65536 \
mongo:8.0 --replSet rs0 --bind_ip_all \
--setParameter minSnapshotHistoryWindowInSeconds=0
docker exec nyxid-test-mongodb mongosh --quiet --eval \
'rs.initiate({_id:"rs0",members:[{_id:0,host:"127.0.0.1:27017"}]})'
export NYXID_TEST_DATABASE_URL='mongodb://127.0.0.1:27017/?replicaSet=rs0&directConnection=true'Wait until db.hello().isWritablePrimary is true before starting the suite.
The CI workflow uses the same startup contract and verifies the effective
parameter with getParameter before running tests.
Database setup shares one 90-second readiness deadline across ping, a unique probe write, and initial run-lease publication. A busy shared mongod can recover after the former five-second probe limit; the original operation waits within this budget without retrying the test. Errors still fail immediately, a server that remains unavailable fails at the deadline, and an explicit URI never falls back to another server. Heartbeat, stale-sweep, and cleanup fencing retain their separate short deadlines.
If readiness fails, inspect the reported phase and elapsed time alongside the
resources-backend-test artifact and the job's MongoDB diagnostics. Clustered
setup failures alone do not establish the cause of a server stall. Preserve
these diagnostics and fix recurring setup failures instead of repeatedly
rerunning the full workflow or adding test retries.
# Backend unit tests
cargo test
# Backend tests with all features (including KMS providers)
cargo test --all-features
# Node agent tests
cargo test -p nyxid-cli
# Frontend tests
cd frontend && npm run test
# Frontend lint
cd frontend && npm run lint
# Frontend type check + build
cd frontend && npm run build- All new backend logic should have corresponding unit tests
- All new frontend components should have tests where behavior is non-trivial
- Existing tests must not break -- if a test fails, fix the implementation, not the test (unless the test itself is wrong)
- Frontend linting must pass with zero errors
Use Conventional Commits:
<type>: <description>
<optional body>
| Type | Use Case |
|---|---|
feat |
New feature |
fix |
Bug fix |
refactor |
Code restructuring (no behavior change) |
docs |
Documentation only |
test |
Adding or fixing tests |
chore |
Tooling, CI, dependencies |
perf |
Performance improvement |
Keep the subject line under 72 characters. Use the body for context on why the change was made, not what was changed (the diff shows that).
- All tests pass locally (
cargo test,npm run test,npm run lint) - No new compiler warnings (
cargo buildis clean) - Your branch is rebased on the latest
dev - Commit history is clean (squash fixup commits)
Include:
- Summary: What changed and why (1-3 bullet points)
- Test plan: How you verified the changes work
Every pull request runs the following checks automatically via GitHub Actions. Jobs are gated on path filters so a PR only runs the suites whose files actually changed -- e.g. an SDK-only PR skips backend, CLI, and frontend jobs. CI Pipeline aggregates the required jobs and treats success and skipped as passing. Branch rules must require this status check to enforce the gate at merge time.
| Job | What it does | Triggers on |
|---|---|---|
| Rust Format | cargo fmt --all -- --check -- ensures consistent formatting |
any Rust change (backend, cli, mcp-demo, root Cargo files) |
| Rust Clippy | cargo clippy --workspace --all-targets -- -D warnings -- catches common mistakes and enforces idioms |
any Rust change |
| Backend Test | cargo nextest run -p nyxid -p nyxid-service-adapters against a MongoDB 8.0 replica set -- handler, service, and integration tests |
backend/** or shared workspace files |
| Backend Billing Smoke | Runs the standalone billing route smoke test with its original Cargo feature set | backend changes |
| Backend Image Inputs | Builds the production backend binary and checks that Docker stages every embedded input | backend changes |
| Coverage (Backend) | Measures PR-head coverage and enforces the line threshold | backend changes |
| Coverage (Backend Base) | Reuses an exact-commit report or measures the base branch for the informational PR delta | backend PRs |
| CLI Test | cargo nextest run -p nyxid-cli -- CLI + node-agent unit and integration tests |
cli/** or shared workspace files |
| Rust Features | Builds with --features aws-kms, gcp-kms, and both -- ensures KMS providers compile |
backend/** or shared workspace files |
| Frontend | npm run lint, npm run test, npm run build -- lint, test, and type-check |
frontend/** or .node-version |
| CLI Wizard Bundle Freshness | Re-hashes the wizard's source closure (from cli/src/wizard/bundle-meta/index.manifest) and fails if it no longer matches the committed hash. No npm install or vite rebuild in CI. See CLI Wizard Bundle. |
frontend/**, cli/src/wizard/**, or .node-version |
| SDK Build | npm run build across all SDK packages -- ensures TypeScript compiles |
sdk/** or .node-version |
| CI Pipeline | Aggregator for all correctness checks above; the base coverage comparison is informational. Treats skipped as success. |
always |
A change under Cargo.toml / Cargo.lock re-runs both Backend Test and CLI Test because they share the workspace lock file. A change under .github/workflows/** or .github/scripts/** forces every job to run.
Wait for a green CI Pipeline before merging. If a check fails, fix the issue locally and push -- the workflow re-runs automatically.
Backend test and coverage builds use CARGO_PROFILE_TEST_DEBUG=line-tables-only
to keep the large test binary within hosted-runner memory. Each mode compiles
one archive, then runs four isolated shards with one MongoDB per runner.
Inventory verification checks that every expected test ran exactly once across
the shards; coverage profiles are merged before enforcing the existing threshold.
When a runner exits with code 143 during compilation, inspect the last completed
step before treating it as a test failure. Repeated shutdowns at the same compile
stage need a build-resource correction rather than repeated full-workflow retries.
The CI profile schedules the capacity-wait heartbeat, trigger grace deadline/recovery,
lost-session recovery, stream-bound, and bounded analytics admission tests alone
within each shard to keep concurrent database creation and cleanup out of their
timing windows. The profile and reusable workflow are included in the coverage
fingerprint. Base coverage reuses an exact-commit published report when available;
it is informational and does not rerun the base suite.
The reusable backend workflow sets RUST_MIN_STACK=8388608 (8 MiB) for ordinary
and instrumented native journeys. Each build and execution shard has a 60-minute
budget. Tests retain the CI profile's five-minute deadline and one retry; every
retried test is named in the job summary, and a second failure fails the run.
For failed backend runs, open the verification job summary or download
backend-test-diagnostics / backend-coverage-diagnostics (retained for 14 days).
These contain available shard JUnit reports, failures/retries/skips, missing
reports and the slowest cases; completeness and coverage remain separate gates.
See the CI stability follow-up
for focused reproduction commands and cache changes, and
service history
for the credential usage/edit timestamp contract.
Rollup-health watchdog issues are incident-routing records, not a second source of CI truth. The rollup PR checks, annotations, and uploaded artifacts remain authoritative for whether integration is blocked.
When a rollup-health issue is opened, keep the repair scoped to the failing check evidence:
- Record the rollup PR, head SHA, failed check names, red-start time, and any immutable artifact or log link used for diagnosis.
- Reproduce the failing job locally with the same package-level commands where possible, then fix the smallest contract break that restores the check.
- Open or keep a separate issue only when it adds ownership or durable work that the rollup PR status cannot carry: cross-package coordination, runbook updates, owner routing, bisect/revert tracking, or automation follow-up.
- If the same rollup-health class recurs for a third time in a quarter, the next repair must include either a class-level guardrail/runbook update or an explicit recurrence waiver explaining why a one-off fix is still the right boundary.
- PRs target the
mainbranch by default - At least one approval is required before merge
- All CI checks must pass (see above)
- Security-sensitive changes require explicit security review
- Merge branch-sync PRs with Create a merge commit. Squashing drops the merged branch's ancestry, so GitHub can still report the same conflicts.
- Update
docs/API.mdif you add or change API endpoints - Update
docs/ARCHITECTURE.mdif you add new collections, services, or architectural patterns - Update
README.mdif you add new features visible to users - Do not create new documentation files unless the feature is significant enough to warrant a standalone guide
- Add comments only where the logic is non-obvious
- Do not add boilerplate docstrings to every function
- Do not add
// removedcomments for deleted code -- just delete it
Releases are triggered by pushing a semver tag to main:
git tag v0.2.0
git push origin v0.2.0The release workflow:
- Runs the full CI suite as a gate
- Builds and pushes Docker images to GitHub Container Registry (
ghcr.io) - Creates a GitHub Release with an auto-generated changelog
Only maintainers should create release tags. Contributors do not need to worry about releases -- just open PRs against dev.
| Document | Description |
|---|---|
| README.md | Project overview, quick start, features |
| docs/API.md | Full API reference with request/response schemas |
| docs/ARCHITECTURE.md | System architecture, data flows, database schema |
| docs/DEVELOPER_GUIDE.md | In-depth development patterns and conventions |
| docs/SECURITY.md | Security architecture and threat model |
| docs/DEPLOYMENT.md | Production deployment guide |
| docs/ENCRYPTION_ARCHITECTURE.md | Envelope encryption and KMS integration |
| docs/NODE_PROXY.md | Credential node setup and usage |
| docs/NYXID_NODE.md | Node agent CLI reference |
By contributing to NyxID, you agree that your contributions will be licensed under the MIT License.