Skip to main content

Architecture Overview

This document provides a comprehensive overview of the Nekzus architecture for developers. It covers system components, data flow, extension points, and key design decisions.


System Overview

Nekzus is a secure API gateway and reverse proxy designed for local network services. It provides auto-discovery, JWT authentication, WebSocket support, and a React-based management dashboard.

High-Level Architecture

d2

Component Interaction Diagram

d2


Core Components

cmd/nekzus/ - Main Application

The main application package contains the entry point, HTTP handlers, and route configuration.

Key Files

FilePurpose
main.goApplication initialization, server lifecycle, signal handling
handlers.goCore HTTP handlers (health checks, utility functions)
auth_handlers.goAuthentication endpoints (login, logout, pairing)
device_handlers.goDevice management (list, revoke, details)
qr_handlers.goQR code pairing flow
apikey_handlers.goAPI key management
stats_handlers.goDashboard statistics
webhooks.goWebhook registration and delivery
federation_handlers.goP2P federation endpoints
static.goStatic file serving for React frontend

Application Structure

// Application holds the main application state
type Application struct {
// Configuration
config types.ServerConfig
configPath string
configWatcher *config.Watcher

// Registries
services *ServiceRegistry // Auth, Discovery, Toolbox, Certs
limiters *RateLimiterRegistry // Per-endpoint rate limiters
managers *ManagerRegistry // WebSocket, Router, Activity, Peers
handlers *HandlerRegistry // HTTP handlers
jobs *JobRegistry // Background jobs

// Core Infrastructure
storage *storage.Store
metrics *metrics.Metrics
proxyCache *proxy.Cache
dockerClient *client.Client
httpServer *http.Server
}

Registry Pattern

The application uses registries to organize related components:

d2


internal/auth/ - Authentication

The auth package handles JWT token management, bootstrap tokens, and device authentication.

Architecture

d2

Key Components

Manager (jwt.go): Core authentication manager

type Manager struct {
jwtSecret []byte // HS256 signing key
issuer string // Token issuer (default: nekzus)
audience string // Token audience (default: nekzus-mobile)
bootstrap *BootstrapStore // Short-lived pairing tokens
revocation *RevocationList // Revoked tokens/devices
}

Scopes (scopes.go): Permission system

ScopeDescription
read:catalogView app catalog
read:eventsView activity events
access:mobileMobile app access
access:*Wildcard access to proxied services
read:*Read all resources
write:*Write all resources

Bootstrap Tokens: Used for QR code pairing flow

  • 5-minute expiry by default
  • Single-use tokens
  • Rate-limited to prevent brute force

internal/config/ - Configuration

The config package handles YAML configuration loading, validation, and hot reload.

Configuration Flow

d2

Hot Reload

The config watcher monitors the configuration file for changes:

type Watcher struct {
configPath string
currentConfig types.ServerConfig
fsWatcher *fsnotify.Watcher // File system watcher
handlers []ReloadHandler // Reload callbacks
}

Reloadable Settings:

  • Routes and apps
  • Bootstrap tokens
  • Discovery intervals
  • Health check settings
  • Metrics endpoint toggle

Non-Reloadable Settings (require restart):

  • Server address (server.addr)
  • TLS certificates
  • JWT secret
  • Database path

internal/discovery/ - Service Discovery

The discovery package auto-discovers services from Docker, Kubernetes, and mDNS.

Discovery Architecture

d2

Discovery Worker Interface

type DiscoveryWorker interface {
Name() string
Start(ctx context.Context) error
Stop() error
}

Docker Discovery

Discovers containers with nekzus.enable=true label:

labels:
nekzus.enable: "true"
nekzus.app.id: "myapp"
nekzus.app.name: "My Application"
nekzus.route.path: "/apps/myapp/"

Kubernetes Discovery

Discovers pods with annotations:

annotations:
nekzus/enable: "true"
nekzus/app-id: "myapp"

mDNS Discovery

Scans for services advertising via mDNS/Bonjour (e.g., _http._tcp).


internal/proxy/ - Reverse Proxy

The proxy package handles HTTP and WebSocket proxying with caching.

Proxy Architecture

d2

Proxy Cache

Caches httputil.ReverseProxy instances per route:

type Cache struct {
proxies sync.Map // map[string]*httputil.ReverseProxy
}

WebSocket Proxy

RFC 6455 compliant WebSocket proxying:

type WebSocketProxy struct {
Target string
BufferSize int // Default: 32KB
DialTimeout time.Duration // Default: 10s
InsecureSkipVerify bool
}

Features:

  • Bidirectional tunneling via connection hijacking
  • TLS support (ws:// and wss://)
  • Header forwarding (Origin, Cookie, X-Forwarded-*)
  • Buffer pooling to reduce GC pressure

internal/storage/ - SQLite Persistence

The storage package provides SQLite-based persistence with WAL mode for better concurrency.

Database Schema

+------------------+       +------------------+
| apps | | routes |
+------------------+ +------------------+
| id (PK) |<------| route_id (PK) |
| name | | app_id (FK) |
| icon | | path_base |
| tags (JSON) | | target_url |
| endpoints (JSON) | | scopes (JSON) |
+------------------+ | websocket |
| strip_prefix |
+------------------+

+------------------+ +------------------+
| devices | | proposals |
+------------------+ +------------------+
| device_id (PK) | | id (PK) |
| device_name | | source |
| platform | | detected_host |
| scopes (JSON) | | detected_port |
| last_seen | | suggested_app |
+------------------+ | suggested_route |
+------------------+

+------------------+ +------------------+
| certificates | | toolbox_deploy |
+------------------+ +------------------+
| id (PK) | | id (PK) |
| domain | | service_id |
| certificate_pem | | container_id |
| private_key_pem | | status |
| not_after | | env_vars (JSON) |
+------------------+ +------------------+

Repository Interfaces

type DeviceRepository interface {
SaveDevice(deviceID, deviceName, platform, platformVersion string, scopes []string) error
GetDevice(deviceID string) (*DeviceInfo, error)
ListDevices() ([]DeviceInfo, error)
DeleteDevice(deviceID string) error
UpdateDeviceLastSeen(deviceID string) error
}

type RouteRepository interface {
SaveRoute(route types.Route) error
GetRoute(routeID string) (*types.Route, error)
ListRoutes() ([]types.Route, error)
DeleteRoute(routeID string) error
}

SQLite Configuration

// WAL mode for better concurrency
db.Exec("PRAGMA journal_mode=WAL")
db.Exec("PRAGMA foreign_keys=ON")
db.Exec("PRAGMA busy_timeout=5000")

// Connection pool
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)

internal/toolbox/ - Docker Compose Deployment

The toolbox package manages one-click service deployments using Docker Compose.

Toolbox Architecture

d2

Service Template Structure

Service templates are loaded from Docker Compose files with special labels:

services:
myservice:
image: vendor/myservice:latest
labels:
nekzus.toolbox.name: "My Service"
nekzus.toolbox.category: "productivity"
nekzus.toolbox.description: "Service description"
nekzus.toolbox.icon: "Package"

Environment Variable Extraction

Variables are automatically extracted from Compose files:

// Pattern: ${VAR:-default} or ${VAR:?error}
varPattern := regexp.MustCompile(`\$\{([A-Z_][A-Z0-9_]*)(:-([^}]*))?\}`)

internal/middleware/ - HTTP Middleware

The middleware package provides HTTP middleware for authentication, rate limiting, and more.

Middleware Chain

d2

Rate Limiter

Uses token bucket algorithm with RFC 6585 headers:

func RateLimit(limiter *ratelimit.Limiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
clientIP := httputil.ExtractClientIP(r)
state := limiter.GetState(clientIP)

// RFC 6585 headers
w.Header().Set("RateLimit-Limit", strconv.Itoa(state.Limit))
w.Header().Set("RateLimit-Remaining", strconv.Itoa(state.Remaining))
w.Header().Set("RateLimit-Reset", strconv.FormatInt(state.ResetAt, 10))

if !limiter.Allow(clientIP) {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}

internal/types/ - Shared Types

The types package contains shared type definitions used across packages.

Key Types

// ServerConfig holds the complete application configuration
type ServerConfig struct {
Server ServerSettings
Auth AuthSettings
Bootstrap BootstrapSettings
Storage StorageSettings
Discovery DiscoverySettings
HealthChecks HealthChecksConfig
Metrics MetricsConfig
Toolbox ToolboxConfig
Routes []Route
Apps []App
}

// App represents a discoverable application
type App struct {
ID string
Name string
Icon string
Tags []string
Endpoints map[string]string
HealthStatus string
}

// Route defines a reverse proxy route
type Route struct {
RouteID string
AppID string
PathBase string
To string
Scopes []string
Websocket bool
StripPrefix bool
}

// Proposal represents a discovered service awaiting approval
type Proposal struct {
ID string
Source string // docker, kubernetes, mdns
DetectedScheme string
DetectedHost string
DetectedPort int
Confidence float64
SuggestedApp App
SuggestedRoute Route
}

web/ - React Frontend

The React frontend provides a terminal-themed dashboard for managing Nekzus.

Frontend Architecture

d2

Context Providers

d2

ThemeProvider (ThemeContext.jsx): Manages 8 visual themes

SettingsProvider: Application settings persistence

AuthProvider (AuthContext.jsx): JWT authentication state

const AuthContext = {
user: Object, // Current user
token: String, // JWT token
isAuthenticated: Bool, // Auth status
isLoading: Bool, // Loading state
login: Function, // Login handler
logout: Function, // Logout handler
checkAuth: Function, // Validate token
}

DataProvider (DataContext.jsx): Central data management

const DataContext = {
// State
routes: Array,
discoveries: Array,
devices: Array,
activities: Array,
containers: Array,
stats: Object,
wsConnected: Boolean,

// CRUD Operations
updateRoute: Function,
deleteRoute: Function,
approveDiscovery: Function,
rejectDiscovery: Function,
revokeDevice: Function,
}

Component Structure

web/src/
+-- components/
| +-- buttons/ # Button, ButtonGroup
| +-- cards/ # DeviceCard, ServiceCard, ContainerCard
| +-- charts/ # ResourceLineChart, ServerResourcesPanel
| +-- data-display/ # Badge, HealthItem, ActivityList
| +-- forms/ # Input, Select, Checkbox, ToggleSwitch
| +-- layout/ # Container, TerminalHeader, TerminalFooter
| +-- modals/ # Modal, PairingModal, ConfirmationModal
| +-- navigation/ # Tabs, TabItem, TabContent
| +-- notifications/ # ToastContainer, NotificationBell
| +-- utility/ # ThemeSwitcher, ASCIILogo
+-- contexts/ # React contexts
+-- pages/ # Page components
+-- services/ # API and WebSocket services
+-- styles/ # CSS (base, themes, app)

Request Flow

HTTP Request Flow

d2

Proxy Request Flow

1. Request: GET /apps/grafana/api/dashboards

2. Route Lookup (Radix Tree)
- Match: /apps/grafana/ -> http://grafana:3000

3. Path Processing
- Original: /apps/grafana/api/dashboards
- Strip prefix: /api/dashboards
- Upstream: http://grafana:3000/api/dashboards

4. Header Processing
- Add X-Forwarded-For, X-Real-IP
- Remove hop-by-hop headers
- Strip Authorization header

5. Proxy Request
- Forward to upstream
- Stream response

6. Response Processing
- Rewrite HTML (if enabled)
- Rewrite cookie paths

Discovery Architecture

Discovery Flow

d2

Proposal Lifecycle

d2


Authentication Flow

QR Code Pairing Flow (2-Step)

d2

JWT Token Structure

{
"iss": "nekzus",
"aud": "nekzus-mobile",
"sub": "device_abc123",
"scopes": ["read:catalog", "read:events", "access:*"],
"iat": 1703520000,
"exp": 1706112000
}

Token Validation Flow

1. Extract token from Authorization header
2. Verify signature (HS256)
3. Check expiration
4. Verify issuer and audience
5. Check revocation list
6. Check device revocation
7. Extract scopes for authorization

Data Storage

Database Migrations

Migrations run automatically on startup:

func (s *Store) migrate() error {
migrations := []string{
// Apps table
`CREATE TABLE IF NOT EXISTS apps (...)`,
// Routes table
`CREATE TABLE IF NOT EXISTS routes (...)`,
// Devices table
`CREATE TABLE IF NOT EXISTS devices (...)`,
// ... more tables
}

for _, migration := range migrations {
if _, err := s.db.Exec(migration); err != nil {
return err
}
}
return nil
}

Schema Overview

TablePurposeKey Fields
appsApplication catalogid, name, icon, tags
routesProxy routesroute_id, app_id, path_base, to
devicesPaired devicesdevice_id, name, platform, scopes
proposalsDiscovery proposalsid, source, suggested_app, suggested_route
certificatesTLS certificatesdomain, certificate_pem, not_after
api_keysAPI key storageid, key_hash, scopes, expires_at
toolbox_deploymentsDeployed servicesid, service_id, container_id, status
service_healthHealth check stateapp_id, status, consecutive_failures
activity_eventsActivity feedevent_id, type, message, timestamp
audit_logsSecurity audit trailaction, actor_id, target_id, success

Frontend Architecture

State Management

d2

WebSocket Integration

// WebSocket message types
const WS_MSG_TYPES = {
DISCOVERY: 'discovery',
CONFIG_RELOAD: 'config_reload',
DEVICE_PAIRED: 'device_paired',
DEVICE_REVOKED: 'device_revoked',
HEALTH_CHANGE: 'health_change',
WEBHOOK: 'webhook',
}

// Connection with auto-reconnect
websocketService.connect();
websocketService.on(WS_MSG_TYPES.DISCOVERY, () => {
refreshDiscoveries();
});

CSS Architecture

Three-layer CSS architecture:

  1. base.css: Design tokens (colors, spacing, typography)
  2. themes.css: Theme-specific overrides
  3. app.css: Component styles
/* Design tokens in base.css */
:root {
--bg-primary: #0a0c10;
--text-primary: #f8fafc;
--accent-primary: #00ff88;
--space-4: 16px;
}

/* Component using tokens */
.card {
background: var(--bg-primary);
color: var(--text-primary);
padding: var(--space-4);
}

Extension Points

Adding a New Discovery Source

  1. Implement the DiscoveryWorker interface:
// internal/discovery/myprotocol.go
type MyProtocolWorker struct {
manager DiscoverySubmitter
interval time.Duration
ctx context.Context
cancel context.CancelFunc
}

func (w *MyProtocolWorker) Name() string {
return "myprotocol"
}

func (w *MyProtocolWorker) Start(ctx context.Context) error {
w.ctx, w.cancel = context.WithCancel(ctx)

ticker := time.NewTicker(w.interval)
for {
select {
case <-ctx.Done():
return nil
case <-ticker.C:
w.scan()
}
}
}

func (w *MyProtocolWorker) scan() {
// Discover services
services := discoverServices()

for _, svc := range services {
proposal := &types.Proposal{
ID: generateProposalID("myprotocol", svc.Host, svc.Port),
Source: "myprotocol",
DetectedScheme: "http",
DetectedHost: svc.Host,
DetectedPort: svc.Port,
Confidence: 0.8,
SuggestedApp: buildApp(svc),
SuggestedRoute: buildRoute(svc),
}
w.manager.SubmitProposal(proposal)
}
}

func (w *MyProtocolWorker) Stop() error {
w.cancel()
return nil
}
  1. Register the worker in main.go:
func (app *Application) setupDiscovery() error {
// ... existing workers ...

if app.config.Discovery.MyProtocol.Enabled {
worker := discovery.NewMyProtocolWorker(
app.services.Discovery,
app.config.Discovery.MyProtocol.Interval,
)
app.services.Discovery.RegisterWorker(worker)
}
}

Adding New Middleware

  1. Create the middleware:
// internal/middleware/mymiddleware.go
func MyMiddleware(config MyConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Pre-processing
ctx := context.WithValue(r.Context(), myKey, myValue)
r = r.WithContext(ctx)

// Call next handler
next.ServeHTTP(w, r)

// Post-processing (if needed)
})
}
}
  1. Add to the middleware chain in the route builder.

Adding API Endpoints

  1. Create the handler:
// internal/handlers/myhandler.go
type MyHandler struct {
store *storage.Store
metrics *metrics.Metrics
}

func NewMyHandler(store *storage.Store, metrics *metrics.Metrics) *MyHandler {
return &MyHandler{store: store, metrics: metrics}
}

func (h *MyHandler) HandleList(w http.ResponseWriter, r *http.Request) {
items, err := h.store.ListItems()
if err != nil {
apperrors.WriteJSON(w, apperrors.Wrap(err, "LIST_FAILED", "Failed to list items", 500))
return
}
json.NewEncoder(w).Encode(items)
}
  1. Register routes in the route builder.

Adding Frontend Features

  1. Create the API service:
// web/src/services/api/myResource.js
export const myResourceAPI = {
list: async () => {
const response = await fetch('/api/v1/my-resource', {
headers: getAuthHeaders(),
});
return response.json();
},
create: async (data) => {
const response = await fetch('/api/v1/my-resource', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(data),
});
return response.json();
},
};
  1. Add to DataContext:
// web/src/contexts/DataContext.jsx
export function DataProvider({ children }) {
const [myResources, setMyResources] = useState([]);

const refreshMyResources = useCallback(async () => {
const data = await myResourceAPI.list();
setMyResources(data);
}, []);

// Add to WebSocket listeners
websocketService.on('my_resource_updated', refreshMyResources);

const value = {
myResources,
refreshMyResources,
// ... other values
};
}
  1. Create the component:
// web/src/components/MyResourceList.jsx
import { useData } from '../contexts';

export function MyResourceList() {
const { myResources, refreshMyResources } = useData();

return (
<div className="my-resource-list">
{myResources.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}

Best Practices

Error Handling

Use the structured errors package:

import apperrors "github.com/nstalgic/nekzus/internal/errors"

// Create structured error
return apperrors.New("ERROR_CODE", "User message", http.StatusBadRequest)

// Wrap existing error
return apperrors.Wrap(err, "ERROR_CODE", "User message", http.StatusInternalServerError)

// Write JSON error response
apperrors.WriteJSON(w, err)

Metrics Recording

Record operations for observability:

// HTTP requests
app.metrics.RecordHTTPRequest(method, path, status, duration, reqSize, respSize)

// Authentication events
app.metrics.RecordAuthPairing("success", platform, duration)

// Proxy requests
app.metrics.RecordProxyRequest(appID, status, duration)

Storage Operations

Handle optional storage gracefully:

// Always 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 err := app.storage.UpdateDeviceLastSeen(deviceID); err != nil {
log.Printf("Warning: %v", err)
}
}()

Testing

Follow TDD practices:

# Run all tests with race detector
go test -race ./...

# Run short tests only
go test -race ./... -short

# Run specific package
go test -race ./internal/proxy/... -v