Contributing guide
Thanks for your interest. This guide gets you from zero to a working dev environment and covers what to keep in mind when submitting changes.
Sign your commits
Section titled “Sign your commits”Meshploy takes contributions under the Developer Certificate of
Origin. It is one line in each commit
message, which git commit -s adds for you:
Signed-off-by: Your Name <[email protected]>By adding it you certify that you wrote the change, or have the right to submit it under the project’s licence. There is no separate agreement to sign, and nothing is transferred beyond that licence. CI checks that the line is there.
Prerequisites
Section titled “Prerequisites”| Tool | Version |
|---|---|
| Go | 1.25+ |
| Node.js | 20+ |
| PostgreSQL | 15+ |
| Docker or Podman | any recent version |
You only need PostgreSQL to get started. Headscale, K3s, CoreDNS, and Caddy are all optional — the API and frontend work without any of them running.
Local setup
Section titled “Local setup”git clone https://github.com/meshploy/meshploy.gitcd meshploy
# Copy the env template and fill in your local valuescp .env.example .envMinimum env vars to start locally:
DATABASE_URL=postgres://meshploy:meshploy@localhost:5432/meshploy?sslmode=disableJWT_SECRET=any-long-random-stringENCRYPTION_KEY=0123456789abcdef0123456789abcdefStart PostgreSQL
Section titled “Start PostgreSQL”docker compose -f deploy/docker-compose.dev.yml up -dThis starts only PostgreSQL on port 5432 with default credentials (meshploy/meshploy). No Headscale, no CoreDNS, no Caddy needed.
Start the API
Section titled “Start the API”cd apps/api && go run main.go# Runs on :4000. DB migrations run automatically on startup.Start the web dev server
Section titled “Start the web dev server”cd apps/web && npm install && npm run dev# Runs on :5173. Route tree is auto-generated.Build the CLI
Section titled “Build the CLI”cd apps/cli && go build -o meshploy .What works locally vs what needs a VPS
Section titled “What works locally vs what needs a VPS”The API and UI are fully usable locally without any deploy infrastructure. Mesh and cluster features degrade gracefully — they don’t crash, they just return empty data or skip the infra step.
| Area | Local (Postgres only) | Needs a VPS |
|---|---|---|
| Auth, RBAC, permissions, invitations | ✅ full | |
| Orgs, projects, services, stacks, jobs | ✅ full | |
| Secrets, variable groups, routes | ✅ full | |
| Frontend UI — all pages and flows | ✅ full | |
| CLI commands (service, stack, job, secret) | ✅ full | |
| Node list / registration API | ⚠️ API works, no real nodes | ✅ |
| Deployments | ⚠️ triggers, fails at K8s step | ✅ |
| Build jobs | ⚠️ triggers, fails at K8s step | ✅ |
| WireGuard mesh, Headscale | ❌ no-ops silently | ✅ |
Edge proxy routing (apps/proxy) | ❌ no routes to resolve | ✅ |
| CoreDNS wildcard DNS | ❌ not running | ✅ |
| Worker node install/uninstall | ❌ needs real servers | ✅ |
If your change is in the API, frontend, CLI, or service layer — local dev is all you need. Only reach for a VPS when your change touches the mesh, the proxy, node registration, or the actual build/deploy execution path.
Project layout
Section titled “Project layout”apps/api/ Thin CE entrypoint (main.go calls server.Main()). Chi + Huma REST API core lives in packages/server — business logic in service/, HTTP in handler/.apps/proxy/ Edge reverse proxy. Reads Host header → WireGuard mesh → upstream.apps/cli/ Cobra CLI binary. Wraps API calls; node install/uninstall shells out to scripts.apps/web/ Vite + React 19 + TanStack Router frontend.packages/db/ Shared GORM models imported by api and proxy.packages/server/ API core — config, service, handler, middleware, k8s, templates. Imported by apps/api.packages/client/ Typed Go REST client for the API. Imported by cli and mcpserver.packages/mcpserver/ MCP tool definitions. Imported by cli (stdio) and by packages/server (remote /mcp).Guidelines
Section titled “Guidelines”Go (api, proxy, cli)
Section titled “Go (api, proxy, cli)”- Never put business logic in handlers. Handlers call the service layer and return results. Logic belongs in
packages/server/service/. - Use GORM for all DB access. No raw SQL — use
applyConstraints()inpackages/db/db.gofor DDL. - Schema changes go on the models. Add fields to the structs in
packages/db/models.goand new models to theAutoMigratelist indb.go; indexes GORM can’t express go inapplyConstraints().db.RegisterMigration()is only for schema that lives outsidepackages/db, such as the Enterprise module. - Secrets stay encrypted. Use
db.EncryptedStringfor any sensitive column. Never store plaintext. - Error responses use
huma.Error4xx()helpers — don’t write raw JSON.
TypeScript / React (web)
Section titled “TypeScript / React (web)”- File-based routing in
src/routes/. Every route file exportsRoute = createFileRoute(...). - Use shadcn/ui components from
src/components/ui/— don’t reach for native HTML elements for UI. - shadcn/ui uses
@base-ui/react(not Radix UI). Use therenderprop instead ofasChild. - Tailwind v4 — no
tailwind.configfile. All tokens live insrc/index.css. - State via Zustand in
src/store/. API calls go throughsrc/lib/api/.
Safety rules
Section titled “Safety rules”- Never modify files inside
deploy/headscale/data/. - Never commit
.env,.db,.db-shm, or.db-walfiles. - Never expose worker container ports to public interfaces.
- Never delete a gateway node (
k3s_role=server) via the API — block at handler level.
Commit convention
Section titled “Commit convention”feat: new user-visible featurefix: bug fixrefactor: code change with no behaviour changetest: adding or updating testsdocs: documentation onlychore: build, deps, config, release toolingperf: performance improvementci: CI/CD changesOne subject line, no trailing period. Keep it short: around 100 characters is a good ceiling, but a clear subject matters more than hitting a count.
Pull requests
Section titled “Pull requests”- One concern per PR. A refactor and a bug fix are two PRs.
- Tests for service-layer changes. The
packages/server/service/package has integration tests — add coverage for new service methods. - Build must pass. Run
go build ./...before pushing. - Type-check the frontend. Run
npm run buildinapps/web/to catch TypeScript errors.
Testing on a staging VPS
Section titled “Testing on a staging VPS”If your change touches anything in the “Needs a VPS” column above, you need a real Linux server with a public IP. A $5/month VPS is enough for a single-node test setup.
Required open ports (gateway only)
Section titled “Required open ports (gateway only)”Configure your firewall or cloud security group to allow inbound traffic on:
| Port | Protocol | Purpose |
|---|---|---|
| 80 | TCP | Caddy: ACME HTTP-01 challenges + HTTP→HTTPS redirect |
| 443 | TCP | Caddy: dashboard, API, proxy routing, Headscale control plane |
| 53 | TCP + UDP | CoreDNS: authoritative DNS for the domain. NS-delegation mode only |
| 41641 | UDP | Optional. Lets nodes reach the gateway directly instead of through a relay |
Worker nodes do not need open ports. They only make outbound connections to the gateway. When two nodes cannot connect directly, WireGuard traffic is relayed through Tailscale’s public DERP servers; Meshploy’s Headscale does not run its own relay.
Keep everything else closed. The API (4000), the built-in registry (5000), the k3s API (6443), node_exporter (9100) and the kubelet (10250) listen on every interface so the mesh can reach them, and the registry has no authentication, so they rely on the firewall to stay off the internet.
Gateway setup
Section titled “Gateway setup”# Install Meshploy on the gateway serversudo bash -c "$(curl -fsSL https://meshploy.com/install.sh)"Set up DNS for the domain you will give the installer. The default mode
delegates it to the gateway with an NS record; if your provider can’t do that,
install with --dns-mode=ondemand and add wildcard A records instead. The exact
records for both modes are in the README’s DNS setup.
CoreDNS handles internal mesh DNS (*.internal.yourdomain.com) automatically
once it’s running — you don’t need to configure those records manually.
Adding a worker node
Section titled “Adding a worker node”In the dashboard, open Cluster and choose Add a worker node. It creates a single-use provisioning token and shows the command to run on the worker. Or, from any machine where the CLI is logged in, let Meshploy do it over SSH:
meshploy node add ubuntu@<worker-ip>The worker registers with Headscale, joins the WireGuard mesh and the k3s cluster, and appears in the dashboard within a few seconds. Worker nodes don’t need a domain or any open firewall ports.
Iterating without a full reinstall
Section titled “Iterating without a full reinstall”The gateway runs prebuilt images from GHCR, and /opt/meshploy is an unpacked
copy of deploy/, not a git checkout, so there is nothing to git pull or
rebuild there. Once a change is on main, CI publishes :main images; move the
gateway onto them with:
sudo meshploy update --edgesudo meshploy server-upgrade --edgeTo try an API change before merging, push your own image and set
MESHPLOY_API_IMAGE in /opt/meshploy/.env to its repository (the tag still
comes from MESHPLOY_CHANNEL), then run docker compose up -d api in
/opt/meshploy.
The database, certificates and Headscale state are preserved across upgrades.
Reporting bugs
Section titled “Reporting bugs”Open a GitHub Issue. Include the Meshploy version (meshploy version), OS, and steps to reproduce.
For security vulnerabilities, do not open a public issue — see SECURITY.md.