Thank you for your interest in contributing to Scanopy! We welcome contributions of all kinds, from bug reports and documentation improvements to new features and service definitions.
- Getting Started
- Ways to Contribute
- Development Environment Setup
- Development Workflow
- Adding Service Definitions
- Testing
- Submitting Your Contribution
- Licensing
The easiest way to contribute is by adding service definitions! Service definitions help Scanopy identify and categorize network services during discovery. This is a great first contribution that doesn't require deep knowledge of the codebase. Given the wide variety of services that folks run across their networks, this is inherently best handled as a community-driven effort.
If you're interested in adding a service definition, jump to the Adding Service Definitions section.
Service definitions are small, focused additions that help Scanopy discover and identify specific services on your network. Examples include:
- Home automation platforms (Home Assistant, OpenHAB)
- Media servers (Plex, Jellyfin, Emby)
- Infrastructure services (Pi-hole, AdGuard, Traefik)
- Development tools (Portainer, Grafana, Jenkins)
Found a bug? Please open an issue!
Help improve our documentation:
- Fix typos or clarify existing docs
- Add examples or tutorials for specific setups
- Improve installation instructions
- Document troubleshooting steps
For larger features or bug fixes:
- Discuss your idea in an issue first
- Follow the development workflow below
- Write tests for new functionality
- Update documentation as needed
Help make Scanopy accessible to users worldwide by contributing translations:
- Weblate: hosted.weblate.org/engage/scanopy
- No coding required - translate directly in your browser
- Review and improve existing translations
- Suggest new languages
This is a great way to contribute without needing to set up a development environment!
For Daemon Development:
- Linux / WSL2: Docker with host networking support, OR binary installation
- macOS: Binary installation only (Docker Desktop does not support host networking)
- Windows: Native development supported
For Server Development:
- Rust 1.90 or later
- Node.js 20 or later
- PostgreSQL 17
- Docker and Docker Compose (optional, for containerized development)
-
Clone the repository
git clone https://github.com/scanopy/scanopy.git cd scanopy -
Install development dependencies
On Ubuntu/Debian:
-
Install NVM and Node.js 20
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash nvm install 20 nvm use 20 -
Install postgresql-17
sudo apt install curl ca-certificates gnupg2 wget vim -y sudo install -d /usr/share/postgresql-common/pgdg sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc sudo sh -c 'echo "deb [arch=amd64 signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' sudo apt update sudo apt -y install postgresql-17 -
Install project dependencies
make install-dev-linux
On macOS:
-
Install Homebrew if not already installed
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -
Install Rust, Node.js 20, and PostgreSQL 17
brew install rust node@20 postgresql@17
-
Install project dependencies
make install-dev-mac
This installs:
- Rust toolchain with rustfmt and clippy
- Node.js dependencies
On Windows:
-
Install Rust (download and run
rustup-init.exe) -
Install Node.js 20 (LTS installer) or via nvm-windows
-
Install Docker Desktop (for the PostgreSQL dev database)
-
Install
makevia one of:- Chocolatey:
choco install make - Scoop:
scoop install make - Or use Git Bash (included with Git for Windows)
- Chocolatey:
-
Clone and install:
git clone https://github.com/scanopy/scanopy.git cd scanopy make install-dev-windows
-
-
Set up the database
make setup-db
This starts a PostgreSQL container on port 5432.
You have two options for development:
Run components individually with hot reload:
# Terminal 1 - Start the server
make dev-server
# Terminal 2 - Start the UI
make dev-ui
# Terminal 3 - Start the daemon (if needed)
make dev-daemonAdvantages:
- Faster iteration with hot reload
- Easier debugging
- More control over individual components
Run everything in Docker containers:
# Start all services
make dev-container
# Rebuild containers
make dev-container-rebuild
# Clean rebuild (no cache)
make dev-container-rebuild-clean
# Stop all services
make dev-downUse this when:
- Testing the full stack together
- You want a production-like environment
- You're having dependency issues locally
Once running:
- UI: http://localhost:5173 (with hot reload)
- Server API: http://localhost:60072
- Daemon API: http://localhost:60073
Multi-tab SSE hang (local dev only): Opening the same page in multiple browser tabs may cause the second tab to hang. This is caused by the browser's HTTP/1.1 connection limit (~6 per domain). Each page opens persistent SSE connections for real-time updates, which consume connection slots across all tabs. This does not affect production deployments (HTTP/2 via reverse proxy multiplexes all streams over one connection). Workaround: use separate browser profiles, or use a local reverse proxy with HTTP/2 support.
-
Create a new branch for your work:
git checkout -b feature/your-feature-name # or git checkout -b fix/your-bug-fix -
If working on the server/daemon, ensure fresh start:
make clean-daemon # Clear daemon config make clean-db # Stop and remove database make setup-db # Create fresh database
-
Write your code
- Follow existing code patterns
- Add comments for complex logic
- Keep changes focused and atomic
-
Test your changes
make testNote - this will tear down all containers, including the PostgreSql container; you'll need to recreate that after running.
You can dump the DB if you want to hold on to the data and reload the container from the dump.
make dump-db
-
Format your code
make format
-
Lint your code
make lint
Always run these commands before creating a PR:
make format # Format all code
make lint # Check for issues
make test # Run all testsAll three commands must pass without errors before submitting your PR.
Service definitions are the best place to start contributing! They help Scanopy identify and categorize services during network discovery.
Service definitions are located in:
backend/src/server/services/definitions/
├── mod.rs # Module registry
├── home_assistant.rs # Example service definition
├── plex.rs # Example service definition
└── your_service.rs # Your new service definition
Create a new file in backend/src/server/services/definitions/ named after your service (e.g., grafana.rs):
use crate::server::hosts::types::ports::PortBase;
use crate::server::services::definitions::{create_service, ServiceDefinitionFactory};
use crate::server::services::types::categories::ServiceCategory;
use crate::server::services::types::definitions::ServiceDefinition;
use crate::server::services::types::patterns::Pattern;
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Grafana;
impl ServiceDefinition for Grafana {
fn name(&self) -> &'static str {
"Grafana"
}
fn description(&self) -> &'static str {
"Metrics dashboard and visualization platform"
}
fn category(&self) -> ServiceCategory {
ServiceCategory::Monitoring
}
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::Endpoint(
PortBase::Http,
"/api/health",
"grafana"
)
}
fn logo_url(&self) -> &'static str {
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/service-logo.svg"
}
}
// This macro registers your service for automatic discovery
inventory::submit!(ServiceDefinitionFactory::new(create_service::<Grafana>));Add your module to backend/src/server/services/definitions/mod.rs:
pub mod grafana; // Add this lineThat's it! Your service will now be automatically discovered during network scans.
Patterns define how Scanopy identifies your service.
Here are the available pattern types:
This is the preferred match type, as the existence of the name of the service in a response is a strong signal that it is in fact the service in question.
That said, some services will contain the unique name of a service in circumstances like:
- Dashboards will contain multiple service names depending on the service being displayed
- Service names that are short or parts of common words can be contained in other words (ie "Plex" is part of the word "Complex", so if a service has the word "Complex" on the endpoint being checked it will cause a false positive)
So, it's best to include another pattern alongside a Pattern::Endpoint just to be sure, or use a very specific string match (ie a phrase rather than a word).
Pattern::Endpoint Check if an endpoint returns expected content:
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::Endpoint(
PortBase::Http, // Port to check
"/api/service", // Path
"service_name" // Expected text in response
)
}This pattern is acceptable if there are no usable endpoints (ie they require authentication, SSL, or otherwise don't provide service-identifying information), but try to create a pattern with multiple unique ports or combine ports with other information to make the match more precise.
Pattern::Port Match a specific port:
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::Port(PortBase::Http) // Port 80
}Common PortBase values:
PortBase::Http(80)PortBase::Https(443)PortBase::HttpAlt(8080)PortBase::Ssh(22)PortBase::DnsUdp(53)- For custom ports:
PortBase::new_tcp(8000)orPortBase::new_udp(1900)
Note UDP pattern matching is barely supported outside of DNS and a few others. Please don't rely heavily on UDP ports.
Pattern::AnyOf Match if ANY pattern succeeds:
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::AnyOf(vec![
Pattern::Port(PortBase::new_tcp(32400)),
Pattern::Endpoint(PortBase::Http, "/web", "Plex", None)
])
}Pattern::AllOf Match ONLY if ALL patterns succeed:
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::AllOf(vec![
Pattern::Port(PortBase::Http),
Pattern::Port(PortBase::new_tcp(8443))
])
}Pattern::Not Inverse of a pattern:
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::Not(&Pattern::IsGateway)
}Pattern::IsGateway Matches if the host is in the routing table as a gateway:
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::IsGateway
}Pattern::MacVendor Match based on MAC address vendor:
use crate::server::services::types::patterns::Vendor;
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::MacVendor(Vendor::EERO)
}To add new Vendor:: values:
- Go to
backend/src/server/services/types/patterns.rsand ctrl+f "pub struct Vendor;" - Use
https://gist.github.com/aallan/b4bb86db86079509e6159810ae9bd3e4to identify the string used by a vendor for their MAC address patterns. - Add your new Vendor value:
pub const NEWVENDOR: &'static str = "Acme, Inc"
```;
**Pattern::SubnetIsType**
Match based on subnet type:
```rust
use crate::server::subnets::types::base::SubnetType;
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::SubnetIsType(SubnetType::Guest)
}For a list of subnet types and information on how they are derived, check out backend/src/server/subnets/types/base.rs.
pub enum SubnetType has the list, and the method from_interface_name has specifics on how they are matched.
Pattern::None For services that aren't auto-discovered (manual only):
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::None
}Choose the most appropriate category. If the service you want to add doesn't fit the category, you can add one at backend/src/server/services/types/categories.rs.
ServiceCategory::NetworkCore- Switches, core infrastructureServiceCategory::NetworkAccess- Routers, access pointsServiceCategory::NetworkSecurity- Firewalls, security appliancesServiceCategory::DNS- DNS serversServiceCategory::VPN- VPN serversServiceCategory::ReverseProxy- Nginx, Traefik, HAProxy, etc
ServiceCategory::Storage- NAS, file serversServiceCategory::Media- Plex, Jellyfin, EmbyServiceCategory::HomeAutomation- Home Assistant, OpenHABServiceCategory::Virtualization- Proxmox, VMware, DockerServiceCategory::Backup- Backup services
ServiceCategory::Web- Web servers and applicationsServiceCategory::Database- Database serversServiceCategory::Development- Development toolsServiceCategory::Dashboard- Dashboards, admin panelsServiceCategory::Monitoring- Monitoring and metrics
ServiceCategory::Workstation- Desktop computersServiceCategory::Mobile- Mobile devicesServiceCategory::IoT- IoT devicesServiceCategory::Printer- Printers
ServiceCategory::AdBlock- Pi-hole, AdGuardServiceCategory::Custom- Custom servicesServiceCategory::Unknown- When unclear
Mark services not tied to a specific brand.
fn is_generic(&self) -> bool {
true
}Scanopy supports icons from three sources.
Dashboard Icons (Recommended - has the most service icons):
https://dashboardicons.com/icons/home-assistant
Search for the service and press the link button to get a URL like
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/home-assistant.svg"
Simple Icons:
simpleicons.org/icons/
Search for the service and right click an image to open in a new tab and get the URL like:
https://simpleicons.org/icons/homeassistant.svg
Vector Logo Zone:
vectorlogo.zone/logos/
Search for the service then press the clipboard button to get a URL like:
https://www.vectorlogo.zone/logos/akamai/akamai-icon.svg
White Background (for dark logos):
fn logo_needs_white_background(&self) -> bool {
true
}Browse available icons:
- Dashboard Icons: https://dashboardicons.com/
- Simple Icons: https://simpleicons.org/icons/
- Vector Logo Zone: https://www.vectorlogo.zone/
use crate::server::hosts::types::ports::PortBase;
use crate::server::services::definitions::{create_service, ServiceDefinitionFactory};
use crate::server::services::types::categories::ServiceCategory;
use crate::server::services::types::definitions::ServiceDefinition;
use crate::server::services::types::patterns::Pattern;
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Redis;
impl ServiceDefinition for Redis {
fn name(&self) -> &'static str { "Redis" }
fn description(&self) -> &'static str { "In-memory data structure store" }
fn category(&self) -> ServiceCategory { ServiceCategory::Database }
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::Port(PortBase::new_tcp(6379))
}
fn simple_icons_path(&self) -> &'static str { "redis" }
}
inventory::submit!(ServiceDefinitionFactory::new(create_service::<Redis>));use crate::server::hosts::types::ports::PortBase;
use crate::server::services::definitions::{create_service, ServiceDefinitionFactory};
use crate::server::services::types::categories::ServiceCategory;
use crate::server::services::types::definitions::ServiceDefinition;
use crate::server::services::types::patterns::Pattern;
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Portainer;
impl ServiceDefinition for Portainer {
fn name(&self) -> &'static str { "Portainer" }
fn description(&self) -> &'static str { "Docker container management interface" }
fn category(&self) -> ServiceCategory { ServiceCategory::Virtualization }
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::Endpoint(
PortBase::HttpAlt,
"/api/status",
"Portainer"
)
}
fn dashboard_icons_path(&self) -> &'static str { "portainer" }
}
inventory::submit!(ServiceDefinitionFactory::new(create_service::<Portainer>));use crate::server::hosts::types::ports::PortBase;
use crate::server::services::definitions::{create_service, ServiceDefinitionFactory};
use crate::server::services::types::categories::ServiceCategory;
use crate::server::services::types::definitions::ServiceDefinition;
use crate::server::services::types::patterns::Pattern;
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct HomeAssistant;
impl ServiceDefinition for HomeAssistant {
fn name(&self) -> &'static str { "Home Assistant" }
fn description(&self) -> &'static str { "Open-source home automation platform" }
fn category(&self) -> ServiceCategory { ServiceCategory::HomeAutomation }
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::AnyOf(vec![
// Check API endpoint
Pattern::Endpoint(
PortBase::HttpAlt,
"/api/",
"Home Assistant"
),
// Or check default port with web response
Pattern::AllOf(vec![
Pattern::Port(PortBase::new_tcp(8123)),
Pattern::Endpoint(PortBase::Http, "/", "homeassistant", None)
])
])
}
fn dashboard_icons_path(&self) -> &'static str { "home-assistant" }
}
inventory::submit!(ServiceDefinitionFactory::new(create_service::<HomeAssistant>));use crate::server::services::definitions::{create_service, ServiceDefinitionFactory};
use crate::server::services::types::categories::ServiceCategory;
use crate::server::services::types::definitions::ServiceDefinition;
use crate::server::services::types::patterns::{Pattern, Vendor};
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct EeroGateway;
impl ServiceDefinition for EeroGateway {
fn name(&self) -> &'static str { "Eero Gateway" }
fn description(&self) -> &'static str { "Eero mesh WiFi router" }
fn category(&self) -> ServiceCategory { ServiceCategory::NetworkAccess }
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::AllOf(vec![
Pattern::MacVendor(Vendor::EERO),
Pattern::IsGateway
])
}
fn vector_logo_zone_icons_path(&self) -> &'static str { "eero/eero-icon" }
fn logo_needs_white_background(&self) -> bool { true }
}
inventory::submit!(ServiceDefinitionFactory::new(create_service::<EeroGateway>));Before submitting any PR, you must run all tests:
make testThis will:
- Stop any running dev containers
- Clean daemon config
- Run all backend and integration tests
make dev-serverCheck the server logs for any compilation errors.
If you have the actual service running on your network:
- Start Scanopy with your changes
- Navigate to the discovery page in the UI
- Run a network scan
- Verify your service is detected and correctly categorized
- Check that the icon displays correctly
Even if you don't have the service running, you should verify:
- The service compiles without errors
- The pattern logic makes sense
- The category is appropriate
- The icon loads correctly
If you're adding complex logic, consider adding unit tests:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_properties() {
let service = YourService;
assert_eq!(service.name(), "Your Service");
assert_eq!(service.category(), ServiceCategory::Web);
}
}Scanopy uses pre-commit hooks to ensure code quality. These hooks run automatically:
- On commit: Format and lint checks
- On push: Full test suite
The hooks are installed automatically when you run make install-dev-mac or make install-dev-linux.
To skip hooks when needed (not recommended):
git commit --no-verify # Skip commit hooks
git push --no-verify # Skip push hooksPre-submission checklist:
- Created a descriptive branch name
- Code follows existing patterns and conventions
- Ran
make formatto format all code - Ran
make lintwith no errors - Ran
make testwith all tests passing - Tested your changes (if possible)
- Updated documentation (if needed)
- Committed with clear, descriptive messages
-
One change per PR: Keep PRs focused
- One service definition per PR
- One bug fix per PR
- Related changes can be grouped
-
Clear title: Use descriptive titles
Add service definition for GrafanaFix port scanning timeout issueUpdate installation documentation
-
Good description: Include context and details
- What problem does this solve?
- How did you test it?
- Any breaking changes?
- Screenshots (for UI changes)
## Add service definition for [Service Name]
**Description**: [Brief description of what this service does]
**Official Website**: [URL]
**Default Ports**: [List the ports this service typically uses]
**Discovery Method**: [Explain the pattern used and why]
- Pattern type: [Port/Endpoint/Other]
- Reasoning: [Why this pattern is appropriate]
**Icon Source**: [Dashboard Icons/Simple Icons/Vector Logo Zone]
**Testing**:
- [ ] Compiles successfully
- [ ] Tested against real instance (describe setup below)
- [ ] Unable to test (explain why below)
**Testing Details**:
[Describe how you tested this, or why you couldn't test it]
**Additional Notes**:
[Any special considerations, edge cases, or future improvements]- Make requested changes in new commits (don't force-push)
- Be open to feedback and suggestions
- Questions? Open a discussion on GitHub
- Stuck? Comment on your PR or issue
- Be respectful and professional
- Provide constructive feedback
- Help others learn and grow
- Follow the project's coding standards
By submitting a contribution to Scanopy, you agree to the following terms:
-
You grant the Scanopy project maintainers a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute your contributions and such derivative works under any license (including commercial licenses).
-
You grant the Scanopy project maintainers a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer your contributions.
-
You represent that you are legally entitled to grant the above licenses. If your employer has rights to intellectual property that you create, you represent that you have received permission to make the contributions on behalf of that employer, or that your employer has waived such rights for your contributions.
-
You represent that your contribution is your original creation and that you have not copied it from another source.
-
Your contributions will also be licensed to the public under the GNU Affero General Public License v3.0 (AGPL-3.0).
Thank you for contributing to Scanopy! Every contribution, no matter how small, helps make network discovery and documentation better for everyone.