Contributing to Nekzus
Thank you for your interest in contributing to Nekzus! This guide covers everything you need to know to contribute effectively to the project.
Table of Contents
- Getting Started
- Development Workflow
- Code Standards
- Test-Driven Development
- Testing
- Pull Request Process
- Documentation
- Issue Guidelines
Getting Started
Prerequisites
Before you begin, ensure you have the following installed:
| Tool | Version | Purpose |
|---|---|---|
| Go | 1.22+ | Backend development |
| Node.js | 20+ | Frontend development |
| Docker | Latest | Container testing |
| Make | Any | Build automation |
| Git | Latest | Version control |
Fork and Clone
-
Fork the repository on GitHub
-
Clone your fork:
git clone https://github.com/YOUR_USERNAME/nekzus.git
cd nekzus -
Add the upstream remote:
git remote add upstream https://github.com/nstalgic/nekzus.git -
Verify remotes:
git remote -v
# origin https://github.com/YOUR_USERNAME/nekzus.git (fetch)
# origin https://github.com/YOUR_USERNAME/nekzus.git (push)
# upstream https://github.com/nstalgic/nekzus.git (fetch)
# upstream https://github.com/nstalgic/nekzus.git (push)
Setting Up the Development Environment
- macOS
- Linux
- Windows
# Install dependencies with Homebrew
brew install go node docker docker-compose
# For Docker on Apple Silicon
brew install colima
colima start --cpu 6 --memory 8 --disk 60 --vm-type=vz
docker context use colima
# Or use the make target
make first-setup
# Install Go (Ubuntu/Debian)
sudo apt update
sudo apt install golang-go nodejs npm docker.io docker-compose
# Add user to docker group
sudo usermod -aG docker $USER
newgrp docker
# Install with winget
winget install GoLang.Go
winget install OpenJS.NodeJS.LTS
winget install Docker.DockerDesktop
# Or use WSL2 with Linux instructions
Install Dependencies
# Install Go dependencies
go mod download
# Install frontend dependencies
cd web && npm install && cd ..
# Verify installation
go version
node --version
docker --version
Build and Run
# Build everything (web UI + Go binary)
make build-all
# Run in development mode (no TLS)
make run-insecure
# Or start the demo environment
make demo
Access the web dashboard at http://localhost:8080
Development Workflow
Branch Naming Convention
Use descriptive branch names with the following prefixes:
| Prefix | Purpose | Example |
|---|---|---|
feature/ | New features | feature/websocket-compression |
fix/ | Bug fixes | fix/proxy-timeout-handling |
refactor/ | Code refactoring | refactor/discovery-manager |
docs/ | Documentation updates | docs/api-reference |
test/ | Test additions/fixes | test/federation-e2e |
chore/ | Maintenance tasks | chore/update-dependencies |
Creating a Branch
# Sync with upstream
git fetch upstream
git checkout main
git merge upstream/main
# Create your feature branch
git checkout -b feature/your-feature-name
Commit Message Format
Follow the Conventional Commits specification:
<type>(<scope>): <description>
[optional body]
[optional footer]
Types:
feat: New featurefix: Bug fixdocs: Documentation changesstyle: Code style changes (formatting, semicolons)refactor: Code refactoringtest: Adding or updating testschore: Maintenance tasks
Examples:
# Feature
git commit -m "feat(proxy): add WebSocket compression support"
# Bug fix
git commit -m "fix(discovery): handle Docker socket timeout"
# Documentation
git commit -m "docs(api): update authentication endpoints"
# With body
git commit -m "fix(auth): validate JWT audience claim
The audience claim was not being validated, allowing tokens
issued for other services to be accepted.
Closes #123"
Keep Your Branch Updated
# Fetch latest changes
git fetch upstream
# Rebase your branch
git rebase upstream/main
# Force push if needed (only for your own branches)
git push origin feature/your-feature --force-with-lease
Code Standards
Go Code Style
Formatting
All Go code must be formatted with gofmt:
# Format all Go files
go fmt ./...
# Or use make target
make fmt
Linting
Run the linter before committing:
# Run golangci-lint
make lint
# Or directly
golangci-lint run
Error Handling
Use the structured error package from internal/errors:
import apperrors "github.com/nstalgic/nekzus/internal/errors"
// Create a new error
func doSomething() error {
if somethingFailed {
return apperrors.New(
"OPERATION_FAILED",
"Operation failed due to invalid input",
http.StatusBadRequest,
)
}
return nil
}
// Wrap an existing error
func processData(data []byte) error {
result, err := parseData(data)
if err != nil {
return apperrors.Wrap(
err,
"PARSE_ERROR",
"Failed to parse input data",
http.StatusBadRequest,
)
}
return nil
}
// Write error response
func handleRequest(w http.ResponseWriter, r *http.Request) {
err := doSomething()
if err != nil {
apperrors.WriteJSON(w, err)
return
}
}
Storage Operations
Always check for nil storage and handle gracefully:
// Check if storage is available
if app.storage != nil {
device, err := app.storage.GetDevice(deviceID)
if err != nil {
// Handle error
}
}
// Async updates for non-critical operations
go func() {
if app.storage != nil {
if err := app.storage.UpdateDeviceLastSeen(deviceID); err != nil {
log.Printf("Warning: failed to update last seen: %v", err)
}
}
}()
Metrics Recording
Record metrics for observability:
// HTTP requests
app.metrics.RecordHTTPRequest(method, path, status, duration, reqSize, respSize)
// Authentication
app.metrics.RecordAuthPairing("success", platform, duration)
// Proxy requests
app.metrics.RecordProxyRequest(appID, status, duration)
React/JavaScript Code Style
File Organization
web/src/
├── components/ # Reusable UI components
│ ├── Button.jsx
│ └── Card.jsx
├── contexts/ # React contexts
│ ├── AuthContext.jsx
│ └── SettingsContext.jsx
├── pages/ # Page components
│ ├── Dashboard.jsx
│ └── Settings.jsx
├── hooks/ # Custom hooks
│ └── useApi.js
└── styles/ # CSS files
├── base.css
├── themes.css
└── app.css
Component Structure
import { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
export function MyComponent({ title, onAction }) {
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
// Effect logic
}, []);
const handleClick = () => {
setIsLoading(true);
onAction();
};
return (
<div className="my-component">
<h2>{title}</h2>
<button onClick={handleClick} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Click Me'}
</button>
</div>
);
}
MyComponent.propTypes = {
title: PropTypes.string.isRequired,
onAction: PropTypes.func.isRequired,
};
CSS Conventions
Nekzus uses a design token system with CSS custom properties. Never use hardcoded values.
Design Token Architecture
base.css - Design tokens and base styles
themes.css - Theme-specific overrides (8 themes)
app.css - Application component styles
Correct Usage
/* CORRECT - Use CSS variables */
.card {
padding: var(--space-4);
background: var(--bg-secondary);
color: var(--text-primary);
border: var(--border-width) solid var(--border-color);
border-radius: var(--radius-md);
}
.button-primary {
background: var(--accent-primary);
color: var(--text-white);
font-weight: var(--font-weight-bold);
}
/* INCORRECT - Hardcoded values */
.card {
padding: 16px; /* Use var(--space-4) */
background: #1a1f2e; /* Use var(--bg-secondary) */
color: #f8fafc; /* Use var(--text-primary) */
border-radius: 8px; /* Use var(--radius-md) */
}
Available Design Tokens
| Category | Examples |
|---|---|
| Spacing | --space-1 through --space-12 (8px scale) |
| Colors | --bg-primary, --text-primary, --accent-primary |
| Typography | --font-mono, --font-weight-bold |
| Borders | --border-width, --border-color, --radius-md |
Test-Driven Development
Nekzus practices Test-Driven Development. This is NOT optional. All contributions must follow the TDD workflow.
The TDD Cycle
- Red - Write a failing test that defines the expected behavior
- Green - Write the minimum code necessary to pass the test
- Refactor - Clean up the code while keeping tests green
- Document - Update documentation to reflect changes
TDD Rules
| Rule | Description |
|---|---|
| Tests First | Write tests BEFORE implementation code |
| Behavior-Driven | Tests define expected behavior and API contracts |
| Complete Coverage | All new features REQUIRE test coverage FIRST |
| Coverage Targets | Maintain 80%+ coverage for critical packages |
Example: TDD Workflow
Step 1: Write the failing test first
// internal/proxy/cache_test.go
func TestCache_GetOrCreate(t *testing.T) {
cache := NewCache()
target, _ := url.Parse("http://localhost:8080")
// First call should create proxy
proxy1 := cache.GetOrCreate(target)
if proxy1 == nil {
t.Fatal("expected non-nil proxy")
}
// Second call should return same proxy (cached)
proxy2 := cache.GetOrCreate(target)
if proxy1 != proxy2 {
t.Error("expected same proxy instance from cache")
}
}
Step 2: Run the test (it fails)
go test -v ./internal/proxy/...
# --- FAIL: TestCache_GetOrCreate
Step 3: Implement the minimum code to pass
// internal/proxy/cache.go
func (c *Cache) GetOrCreate(target *url.URL) *httputil.ReverseProxy {
c.mu.Lock()
defer c.mu.Unlock()
key := target.String()
if proxy, exists := c.proxies[key]; exists {
return proxy
}
proxy := httputil.NewSingleHostReverseProxy(target)
c.proxies[key] = proxy
return proxy
}
Step 4: Run the test (it passes)
go test -v ./internal/proxy/...
# --- PASS: TestCache_GetOrCreate
Step 5: Refactor if needed, keeping tests green
Testing
Running Tests
# All tests with race detector (recommended)
go test -race ./...
# Unit tests only (fast, skip E2E)
go test -race -short ./...
# Specific package with verbose output
go test -race -v ./internal/proxy/...
# With coverage report
go test -race -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
Make Targets for Testing
| Command | Description |
|---|---|
make test | Run all tests with race detector |
make test-short | Run unit tests only (skip E2E) |
make test-fast | Fast E2E with persistent infrastructure |
make e2e | Start E2E test environment |
make e2e-test | Run E2E test battery |
Test Organization
project/
├── cmd/nekzus/
│ ├── main_test.go # Integration tests
│ ├── e2e_test.go # E2E tests
│ └── *_test.go # Handler tests
├── internal/
│ ├── proxy/
│ │ ├── proxy.go
│ │ └── proxy_test.go # Unit tests
│ └── auth/
│ ├── jwt.go