Thank you for your interest in contributing to Port Daddy! This guide covers the v3 architecture, development workflow, and conventions you need to know.
- Node.js 18+
- npm 9+
- TypeScript 5.x (included in devDependencies)
- macOS or Linux (Windows support via WSL)
# Clone the repo
git clone https://github.com/curiositech/port-daddy.git
cd port-daddy
# Install dependencies
npm install
# Start the daemon in development mode
npm run dev
# Run the full test suite (1255 tests across 21 suites)
npm testport-daddy/
├── server.ts # Express daemon (main entry)
├── config.json # Daemon configuration
├── package.json # ESM project, "type": "module"
├── bin/
│ └── port-daddy-cli.ts # Unified CLI (subcommands, no separate scripts)
├── lib/
│ ├── services.ts # Port assignment module
│ ├── locks.ts # Distributed locks
│ ├── messaging.ts # Pub/sub messaging
│ ├── agents.ts # Agent registry
│ ├── activity.ts # Activity logging
│ ├── webhooks.ts # Webhook subscriptions
│ ├── identity.ts # Semantic ID parsing (project:stack:context)
│ ├── detect.ts # Framework detection (60+ frameworks)
│ ├── config.ts # .portdaddyrc handling
│ ├── health.ts # Health check utilities
│ ├── utils.ts # Common utilities
│ ├── client.ts # JavaScript SDK (PortDaddy class)
│ ├── orchestrator.ts # Service orchestrator (topological sort, spawn, health)
│ ├── discover.ts # Monorepo/workspace service discovery
│ └── log-prefix.ts # Colored multiplexed log output
├── routes/
│ ├── index.ts # Route aggregator
│ ├── services.ts # /claim, /release, /services
│ ├── messaging.ts # /msg, /subscribe, /channels
│ ├── locks.ts # /locks
│ ├── agents.ts # /agents
│ ├── info.ts # /health, /version, /metrics
│ ├── webhooks.ts # /webhooks
│ ├── activity.ts # /activity
│ └── detect-config.ts # /detect, /init, /config
├── shared/
│ └── types.ts # Input validation
├── public/
│ └── index.html # Dashboard UI
├── tests/
│ ├── integration/ # API tests against live daemon
│ │ ├── api.test.js
│ │ ├── cli.test.js
│ │ ├── security.test.js
│ │ └── up-down.test.js
│ ├── unit/ # Unit tests (17 suites, 1042+ tests)
│ └── setup-unit.js # Unit test setup
├── completions/
│ ├── port-daddy.bash # Bash completions
│ ├── port-daddy.zsh # Zsh completions
│ └── port-daddy.fish # Fish completions
└── install-daemon.ts # Daemon installer/manager
| Area | v1 | v3 |
|---|---|---|
| CLI | Separate shell scripts (get-port, release-port, list-ports) |
Unified port-daddy command with subcommands |
| Naming | Flat project names | Semantic identities: project:stack:context |
| Routes | All handlers in server.js |
Modular routes/ directory |
| Validation | Inline in route handlers | Centralized in shared/validation.js |
| SDK | None | lib/client.js (PortDaddy class) |
| Config | Just config.json |
Per-project .portdaddyrc with auto-detection |
| Coordination | Port assignment only | Pub/sub, distributed locks, agent registry, webhooks |
| Tests | Integration only | Unit + integration (1255 tests, 21 suites) |
| Completions | Bash only | Bash, zsh, and fish |
| Module system | CommonJS | ESM throughout (import/export) |
| Language | JavaScript | TypeScript (strict mode) |
| Frameworks | 17 detected | 60+ detected |
1. Express Daemon (server.ts)
- HTTP API for all port and coordination services
- SQLite database (WAL mode for concurrency)
- Process tracking for automatic cleanup
- Rate limiting (100 req/min per IP, 10 concurrent SSE connections)
2. Unified CLI (bin/port-daddy-cli.ts)
- Single entry point:
port-daddy <subcommand> - Subcommands:
claim,release,list,dev,start,restart,status, etc. - Shell completions for bash and zsh
3. Modular Routes (routes/)
- Each domain gets its own route file
routes/index.jsaggregates all routes and mounts them on the Express app- Route handlers are thin: validate input, call lib module, return response
4. Library Modules (lib/)
- Each module exports a factory function that accepts dependencies (for testability)
- All state backed by SQLite with parameterized queries
- Modules: services, locks, messaging, agents, activity, webhooks, identity, detect, config, health, utils, client
5. JavaScript SDK (lib/client.ts)
PortDaddyclass for programmatic usage- Wraps HTTP API with a clean interface
- Importable:
import { PortDaddy } from 'port-daddy/client'
6. Shared Validation (shared/types.ts)
- Input validation rules used across routes
- Semantic identity format validation
- Port range and parameter validation
Why SQLite?
- Atomic transactions (no race conditions between agents)
- Single file (easy backup/migration)
- No separate service to manage
- Fast (<10ms queries)
- ACID guarantees
Why Semantic Identities?
project:stack:contextgives structure to service names- Enables pattern queries (all services for a project, all frontends, etc.)
- Human-readable and machine-parseable
- Example:
myapp:api:main,myapp:frontend:feature-auth
Why localhost-only?
- Port assignment is inherently local
- No remote access needed
- Simpler security model
- No authentication required
- Create a feature branch from
main - Make your changes (follow the code style below)
- Write tests -- both unit and integration
- Run
npm testand confirm all 1255+ tests pass - Commit with clear, descriptive messages
- Push and open a pull request against
main
When adding a new capability to Port Daddy, follow this sequence:
- Add module to
lib/-- export a factory function that takes dependencies - Import and wire up the module in
server.js - Add routes in the
routes/directory -- create a new file or extend an existing one - Code hash is automatic --
server.jsuses dynamicreaddirSyncto hash all source files, so newlib/androutes/files are included automatically - Add shared validation in
shared/validation.jsif the feature takes user input - Update the dashboard in
public/index.html - Write unit tests in
tests/unit/(mock dependencies, no daemon needed) - Write integration tests in
tests/integration/(test against live daemon) - Update README.md with API docs and usage examples
- Add SDK methods to
lib/client.jsso programmatic users get the feature too
Port Daddy has 1255 tests across 21 suites, split into two projects configured in jest.config.js.
# Run everything (unit + integration)
npm test
# Watch mode for rapid iteration
npm run test:watch
# Generate coverage report (target: 90%+)
npm run test:coverage
# CI gate (verifies daemon health, then runs tests)
npm run test:ci- 12 test files, one per
lib/module - All dependencies are mocked -- no daemon, no database, no network
- Fast execution
- Great for TDD: write the test first, then implement
# Run only unit tests
npm test -- --selectProjects unit- 4 test files:
api.test.js,cli.test.js,security.test.js,up-down.test.js - Run against an ephemeral daemon (auto-started by Jest globalSetup/globalTeardown)
- Verify real HTTP contracts, CLI behavior, and security controls
- The test harness restarts the daemon with fresh code and verifies the code hash
# Run only integration tests
npm test -- --selectProjects integration- Unit tests: Place in
tests/unit/<module-name>.test.js. Mock the module's dependencies using the factory function pattern. No network calls, no file I/O. - Integration tests: Place in
tests/integration/. Use real HTTP requests againstlocalhost:9876. Clean up any resources you create (ports, locks, agents, etc.) inafterEachorafterAll.
- ES Modules --
import/export, notrequire/module.exports. The project has"type": "module"inpackage.json. - No semicolons -- rely on ASI. Be consistent with the existing codebase.
- 2-space indentation -- no tabs.
- Descriptive names --
assignPortToServicenotassign,validateSemanticIdnotvalidate. - Comments for non-obvious logic -- especially around SQLite transaction boundaries and race condition handling.
- Express routes in
routes/directory -- keepserver.jsfocused on wiring, not business logic. - Factory functions with dependency injection -- every
lib/module exports a function that takes its dependencies, making testing straightforward.
Example module pattern:
// lib/example.js
export function createExample({ db, logger }) {
function doSomething(input) {
logger.info('Doing something', { input })
// ... use db, return result
}
return { doSomething }
}The entire codebase is TypeScript with strict mode enabled. Key conventions:
- All source files use
.tsextension - Imports use
.jsextension (NodeNext module resolution) - Core types are defined in
shared/types.ts - Run
npm run typecheckto verify types without building - Run
npm run buildto compile todist/ - Development uses
tsxfor direct TypeScript execution (no build step needed)
- SSRF Protection: Webhook URLs are validated against private IP ranges
- Input Validation: All user input validated through
shared/types.ts - SQL Injection Prevention: Parameterized queries throughout -- never interpolate user input into SQL
- Localhost Binding: Daemon only listens on
127.0.0.1 - Rate Limiting: 100 requests/min per IP, 10 concurrent SSE connections per IP
- HMAC Signing: Webhook payloads are signed so receivers can verify authenticity
- Process Arguments: Use
spawnSyncwith array args, never shell strings
Port Daddy is published as an npm package. To cut a release:
- Update the version in
package.jsonfollowing semver - Update CHANGELOG.md with a summary of changes
- Run the full test suite:
npm test-- all 1255+ tests must pass - Commit the version bump:
git commit -am "Bump to vX.Y.Z" - Tag the release:
git tag vX.Y.Z - Push:
git push origin main --tags - Publish to npm:
npm publish - Create a GitHub release at https://github.com/curiositech/port-daddy/releases with release notes
- Issues: https://github.com/curiositech/port-daddy/issues
- Discussions: https://github.com/curiositech/port-daddy/discussions
- README: https://github.com/curiositech/port-daddy#readme
By contributing to Port Daddy, you agree that your contributions will be licensed under the MIT License, the same license that covers the project.
Thank you for helping make Port Daddy better!