Skip to content

API Reference

The Meshploy REST API. Built with Go, Chi router, and Huma for automatic OpenAPI 3.1 spec generation.


LanguageGo 1.25+
RouterChi
OpenAPIHuma v2 (OpenAPI 3.1, automatic schema + docs)
DatabaseGORM + PostgreSQL (via packages/db)
AuthJWT (HS256, 24h expiry) + TOTP 2FA; magt- agent tokens for automation

apps/api/ is a thin entrypoint — its main.go just calls server.Main(). The actual API core (config, HTTP handlers, business logic) lives in packages/server, which apps/api imports via a go.work replace directive. This split lets the API core be reused (e.g. embedded as the in-gateway /mcp server) without depending on the apps/api binary.

apps/api/
├── main.go # Calls server.Main() — no business logic here
├── Dockerfile
├── go.mod
└── tools/
packages/server/
├── server.go # HTTP server setup, route registration
├── entrypoint.go # Main() — config load, DB connect, server start
├── config/ # Typed env config — Load() from environment
├── middleware/ # Auth() resolves a JWT or magt- agent token; RequireAuth() 401s anything off the public allowlist
├── handler/ # HTTP layer only — thin, delegates to service
│ ├── handler.go # Handler struct, Register(), RegisterRaw()
│ ├── access.go # checkAccess(), checkOrgAdminAccess(), checkOrgMemberAccess() helpers
│ ├── auth.go # /auth/register, /auth/login, /me, TOTP, 2FA
│ ├── agent.go # Agent principals: create, list, token mint/rotate/revoke, delete
│ ├── mcp.go # Remote MCP (Streamable HTTP) at /mcp — agent-token authed, permission-scoped
│ ├── org.go # Org CRUD, member management, invitations
│ ├── project.go # Project CRUD
│ ├── environment.go # Environment levels: order, create, rename (hostnames re-derived)
│ ├── promotion.go # Groups, lineages across levels, promotion, board, own databases
│ ├── environment.go # Environment levels: list, create above/below, rename
│ ├── promotion.go # Promotion groups, promote, board, own database, copy down, bring down
│ ├── permission.go # Per-resource permission grants
│ ├── node.go # Node CRUD, self-register, self-deregister, metrics
│ ├── workload.go # Service CRUD, env vars, build/db config, pods
│ ├── stack.go # Stack CRUD, apply, sync
│ ├── job.go # Job CRUD, trigger, run history
│ ├── volume.go # Volume CRUD, mounts, backup config
│ ├── route.go # Route CRUD, targets, hostname verify
│ ├── domain.go # Domain CRUD + DNS verification
│ ├── deployment.go # Deployment list, trigger, rollback, SSE logs
│ ├── backup.go # Service backups + system backup
│ ├── notification.go # Notification channels
│ ├── email_config.go # Org SMTP config
│ ├── variable_group.go # Variable group CRUD + service attach/detach
│ ├── git_integration.go # Git provider integrations + OAuth callbacks
│ ├── registry.go # Registry integration CRUD
│ ├── storage.go # Storage integration CRUD
│ ├── terminal.go # WebSocket: node terminal + pod terminal
│ ├── webhook.go # Inbound webhooks (GitHub push, deploy token)
│ ├── webhook_git.go # Inbound push webhooks for GitLab, Gitea/Forgejo, Bitbucket
│ ├── template.go # One-click template catalog
│ ├── config_file.go # Config file CRUD + attach/detach
│ ├── entitlement.go # Licence status + activation
│ ├── brand.go # An org's brand: by host (public, the sign-in page) or by org
│ ├── ondemand_tls.go # Caddy ask endpoint for on-demand TLS
│ ├── extension.go # Extension point: extra routes (EE)
│ ├── system.go # Version, exposure notice, install/uninstall scripts
│ └── health.go # GET /health
├── service/ # Business logic — one file per domain
│ ├── service.go # Services aggregate struct + New()
│ ├── auth.go # Register (user + default org in tx), Login, TOTP
│ ├── agent.go # Agent principals + agent_tokens; ResolveToken() for the auth middleware
│ ├── org.go # Org CRUD, member management, invitations
│ ├── project.go # Project CRUD
│ ├── permission.go # Resource permission grants
│ ├── node.go # Node CRUD, registration/provisioning tokens, monitor
│ ├── node_exporter.go # Live metrics scraping from node_exporter
│ ├── workload.go # Service CRUD, env vars, build/db config
│ ├── stack.go # Stack parse, apply, sync
│ ├── job.go # Job CRUD, trigger, reconciler goroutine
│ ├── volume.go # Volume CRUD, mounts, K8s PVC lifecycle
│ ├── route.go # Route + target CRUD
│ ├── domain.go # Domain CRUD + DNS verification
│ ├── deployment.go # Deployment trigger, rollback, K8s Job lifecycle
│ ├── backup.go # Backup schedule, trigger, restore, retention
│ ├── backup_executor.go # Backup/restore K8s Job execution
│ ├── notification.go # Notification dispatch (Slack, Discord, email, webhook)
│ ├── email_config.go # Org SMTP config
│ ├── variable_group.go # Variable group CRUD + service attachment
│ ├── git_integration.go # Git provider connections + OAuth flows
│ ├── registry.go # Registry integration CRUD
│ ├── storage.go # Storage integration CRUD
│ ├── db_explorer.go # Live DB query + schema via K8s exec
│ ├── system.go # Version info, install/uninstall script serving
│ ├── template.go # Template catalog fetch/cache + deploy
│ ├── config_file.go # Config files projected into workloads via Secrets
│ ├── exposure.go # Host-firewall exposure notice + dismissed notices
│ ├── entitlement.go # Licence verification + entitlements
│ ├── brand.go # An org's brand; Meshploy's until an edition registers a source
│ ├── extension.go # Extension point: per-org quotas (EE)
│ ├── orphans.go # Cluster workloads that no service owns
│ ├── workload_status.go # Reconciles stored service status with the cluster
│ ├── volume_status.go # Reconciles stored volume status with its claim
│ ├── wsticket.go # Single-use tickets for WebSocket auth
│ └── headscale.go # Headscale API client (list, get, delete, rename nodes)
├── k8s/ # Kubernetes client helpers
├── templates/ # Built-in template assets
└── version/ # Build/version metadata

Routes are under /api/v1 unless listed under Outside /api/v1. Most need Authorization: Bearer <token>, where the token is a user’s JWT (24h) or an agent’s magt- token. The server is fail-closed: a route that is not on its public allowlist returns 401 without a token.

This lists every route registered in packages/server/handler. Auth: ✓ needs a bearer token, public needs none, otherwise the credential the route checks instead.

The OpenAPI spec is at /openapi.json and Huma’s docs page at /docs. Both need the same Authorization header, so fetch the spec with a token:

Terminal window
curl -H "Authorization: Bearer <token>" https://api.<your-domain>/openapi.json
MethodPathAuthDescription
POST/auth/loginpublicLogin and receive a JWT naming a console session (sid), good while that session is live
POST/auth/logout✓End the console session making the request
POST/auth/recoverypublicComplete login with a one-time recovery code
POST/auth/registerpublicRegister a new user
GET/auth/statuspublicCheck whether registration is open (no users exist yet)
POST/auth/totppublicComplete login with TOTP code
GET/me✓Get current user
PATCH/me/password✓Change current user password
POST/me/recovery-codes/regenerate✓Regenerate 2FA recovery codes (requires current TOTP code)
DELETE/me/totp✓Disable 2FA (requires current TOTP code)
POST/me/totp/enable✓Verify TOTP code and enable 2FA
POST/me/totp/setup✓Generate a new TOTP secret (not yet enabled)
GET/me/cli-sessions✓Your logged-in CLIs, the most recently used first; current marks the one asking
DELETE/me/cli-sessions/{id}✓Log one of your CLIs out
GET/me/sessions✓Where you are signed in to the console: browser, address, last seen; current marks the one asking
DELETE/me/sessions/{id}✓Sign one of your console sessions out
DELETE/me/sessions✓Sign out everywhere else: every console session but the one asking

A CLI logs in through a browser, as OAuth’s device flow does: it starts a login and polls; a person signed in to the console sees the same code the terminal shows and approves; the CLI collects an mcli- token that acts as them, once. The token lapses after 90 days unused.

MethodPathAuthDescription
POST/cli/loginspublicStart a login (host, optional door: the console to approve at). Returns the device code (the CLI’s secret), the user code, the approval link, the API address to save, the polling interval and expiry
POST/cli/logins/tokenpublicPoll with the device code: pending, denied, expired, approved with the token once, then collected
GET/cli/logins/{code}✓A login waiting for approval, by its user code (any case, dash optional)
POST/cli/logins/{code}/approve✓Approve it. A browser session only: an agent or CLI token is refused
POST/cli/logins/{code}/deny✓Deny it. A browser session only
DELETE/cli/session✓Log out the CLI making the request (an mcli- token only)

An installation that sets SIGN_IN_LINK_KEY lets whoever holds the key make a one-time link that signs a person in to the console, skipping their password and second factor: an operator opening a console for someone, as Cloud’s Open button does for a workspace’s owner. The key is as strong as the database, so it lives beside the database’s password. The link is the console’s /link, its mlnk- token in the fragment so no server on the way logs it; it is spent once, within five minutes. Unset, making one answers 404.

MethodPathAuthDescription
POST/system/sign-in-linkskeyMake a link for the person signing in with email (an agent has none), landing on next, a path on the console. Takes the key in X-Sign-In-Link-Key. Returns the url and its expiry
POST/auth/sign-in-linkpublicSpend a link’s token, starting a console session: returns the session’s token

Meshploy is the OAuth authorization server for its own /mcp, so a client that can only connect by signing in (Claude on the web and the desktop) can connect. /mcp answers a request without a working credential with 401 and WWW-Authenticate: Bearer resource_metadata=...; the client reads the metadata, registers itself, sends a person to the console’s /oauth/authorize page to approve, and trades the code (PKCE S256) for an moat- access token (an hour) and an mort- refresh token (rotated on every use; a copied one presented again ends the connection). A connection acts as the person who approved it or, chosen by an owner or admin, as one of the organisation’s agents. Its access token opens /mcp and nothing else: the API accepts it only on /mcp’s own calls back to it.

MethodPathAuthDescription
GET/.well-known/oauth-protected-resourcepublicRFC 9728 metadata for /mcp (also at .../oauth-protected-resource/mcp)
GET/.well-known/oauth-authorization-serverpublicRFC 8414 metadata: endpoints, S256, grant types
POST/oauth/registerpublicRFC 7591 dynamic client registration. Redirect URIs must be https, or http to loopback
POST/oauth/tokenpublicauthorization_code with code_verifier, or refresh_token (form-encoded)
GET/oauth/authorize✓What a client asking to connect is, for the approval page. A browser session only
POST/oauth/authorize✓Approve (in org_id, as yourself or as agent_id) or deny; answers where to send the browser. A browser session only
GET/orgs/{orgId}/oauth/connections✓Your connections; ?user_id=, ?agent_id= or ?all=true for an owner or admin. Disconnected ones stay listed
GET/orgs/{orgId}/cli-sessions✓ adminCLIs signed in as the organisation’s members (?user_id= for one); elsewhere marks one whose person also belongs to another organisation
DELETE/orgs/{orgId}/cli-sessions/{id}✓ adminSign out a member’s CLI. Refused (409) while they also belong to another organisation, where it acts as them too
GET/orgs/{orgId}/console-sessions✓ adminConsole sign-ins of the organisation’s members (?user_id= for one), with elsewhere as for CLIs
DELETE/orgs/{orgId}/console-sessions/{id}✓ adminSign a member out of the console. Refused (409) while they also belong to another organisation
GET/orgs/{orgId}/session-counts✓ adminConnected sessions per member: assistants connected here, and signed-in CLIs
DELETE/orgs/{orgId}/oauth/connections/{id}✓Disconnect: your own, or any for an owner or admin
MethodPathAuthDescription
GET/invitations/{token}invite tokenGet invitation info by token (public); account_exists says the address already has an account
POST/invitations/{token}/acceptinvite tokenAccept an invitation and create an account (public). 409 when the address already has one
POST/invitations/{token}/join✓Accept an invitation with the account you are signed in as; refused (403) for another address
GET/orgs✓List organizations for the authenticated user
POST/orgs✓Create an organization, only on a server that has none: Community runs one per server (409 otherwise)
GET/orgs/{orgId}✓Get an organization
PATCH/orgs/{orgId}✓Update an organization
PUT/orgs/{orgId}/network-isolation✓ admin{"enabled": true} closes each of the organization’s projects to the rest of the cluster: its pods take traffic only from the project’s own pods and levels, from projects that use one of its services’ published variables, and from the nodes (the proxy, published ports). Outgoing traffic is unchanged. Off (the default) removes every policy it made. Enforced by k3s’s own NetworkPolicy controller
GET/orgs/{orgId}/sandbox✓The nodes prepared to run sandboxed pods (labelled meshploy.com/sandbox=true by meshploy install sandbox), the sandboxed projects and levels the caller can see, with required where a rule makes it so, and new_projects: whether a project created now starts sandboxed (new_projects_required and new_projects_why when a rule makes it so)
PUT/orgs/{orgId}/sandbox✓ admin{"new_projects": true} starts the organization’s new projects sandboxed when their creator does not say; existing projects keep their own. Refused (409) while no node can run sandboxed pods
DELETE/orgs/{orgId}✓Delete an organization (owner only)
GET/orgs/{orgId}/invitations✓List pending invitations
POST/orgs/{orgId}/invitations✓Create an invite link for a new member
GET/orgs/{orgId}/members✓List organization members
POST/orgs/{orgId}/members✓Add a member to an organization
PATCH/orgs/{orgId}/members/{userId}✓Update a member’s role
DELETE/orgs/{orgId}/members/{userId}✓Remove a member from an organization
MethodPathAuthDescription
GET/orgs/{orgId}/jobs/{resourceId}/permissions✓List permissions on a job
GET/orgs/{orgId}/members/{userId}/permissions✓List all permission grants for a member
POST/orgs/{orgId}/members/{userId}/permissions✓Grant a permission to a member
DELETE/orgs/{orgId}/members/{userId}/permissions✓Revoke a permission from a member
GET/orgs/{orgId}/projects/{resourceId}/permissions✓List permissions on a project
GET/orgs/{orgId}/services/{resourceId}/permissions✓List permissions on a service
GET/orgs/{orgId}/stacks/{resourceId}/permissions✓List permissions on a stack
MethodPathAuthDescription
GET/orgs/{orgId}/agents✓List agent principals in an org
POST/orgs/{orgId}/agents✓Create an agent principal and mint its first token
DELETE/orgs/{orgId}/agents/{agentId}✓Delete an agent principal and all its tokens/grants
POST/orgs/{orgId}/agents/{agentId}/tokens✓Mint an additional token for an agent (rotation)
DELETE/orgs/{orgId}/agents/{agentId}/tokens/{tokenId}✓Revoke an agent token
MethodPathAuthDescription
GET/entitlements✓Current license entitlements for this install
POST/entitlements/license✓Install a license token
MethodPathAuthDescription
GET/brandpublicThe brand of the organisation the request’s host belongs to, for the sign-in page; Meshploy’s on any other host
GET/orgs/{orgId}/brand✓An organisation’s brand (members)
MethodPathAuthDescription
GET/orgs/{orgId}/projects✓List projects in an organization
POST/orgs/{orgId}/projects✓Create a project; sandboxed (optional) runs its apps and jobs on gVisor, and left out follows the organization’s new_projects. Refused (409) asking for no sandbox where a rule requires one, or for one no node can run
GET/orgs/{orgId}/projects/{projectId}✓Get a project
PATCH/orgs/{orgId}/projects/{projectId}✓Update a project
PUT/orgs/{orgId}/projects/{projectId}/network-open✓ admin{"open": true} exempts the project, or the level, from the organization’s network isolation: any pod in the cluster may reach it again
GET/orgs/{orgId}/projects/{projectId}/sandbox✓Whether the project’s, or the level’s, apps and jobs run on gVisor: on, and required with why when a rule makes it so
PUT/orgs/{orgId}/projects/{projectId}/sandbox✓ admin{"on": true} runs its apps and jobs on gVisor instead of the host’s kernel and moves the running ones at once (services roll); its managed databases stay on the host’s kernel. Refused (409) while no node can run sandboxed pods, while an app or job is pinned to one that cannot, or, switching off, while it is required. Builds are never sandboxed
DELETE/orgs/{orgId}/projects/{projectId}✓Delete a project
DELETE/orgs/{orgId}/projects/{projectId}/build-cache✓Clear a project’s build cache: its claim, every builder’s folder in it

A project is its own production level; each level below it is a project of its own (parent_project_id), with its own namespace, so every route above works inside a level. {projectId} may be any level.

MethodPathAuthDescription
GET/orgs/{orgId}/projects/{projectId}/environments✓A project’s levels, production first
POST/orgs/{orgId}/projects/{projectId}/environments✓Add a level above or below an existing one; starts empty
PATCH/orgs/{orgId}/projects/{projectId}/environment✓Rename a level: its hostnames are re-derived, its namespace stays
GET/orgs/{orgId}/projects/{projectId}/board✓Levels, groups, and what each service runs at each level
GET/orgs/{orgId}/projects/{projectId}/promotion-groups✓List promotion groups
POST/orgs/{orgId}/projects/{projectId}/promotion-groups✓Create a group; its services are copied into the level it enters at, the only one that builds
PATCH/orgs/{orgId}/projects/{projectId}/promotion-groups/{groupId}✓Rename a group
DELETE/orgs/{orgId}/projects/{projectId}/promotion-groups/{groupId}✓Delete a group; its services stay where they are
POST/orgs/{orgId}/projects/{projectId}/promotion-groups/{groupId}/services✓Add services, merging away any single-service group they were in
DELETE/orgs/{orgId}/projects/{projectId}/promotion-groups/{groupId}/services/{lineageId}✓Take a service out of a group; its copies keep running
POST/orgs/{orgId}/projects/{projectId}/promotion-groups/{groupId}/promote✓Promote from this level to the next on the group’s path, as the same images; needs update on the target level. ?overwrite=true also replaces a newer image built there (a hotfix)
GET/orgs/{orgId}/projects/{projectId}/promotion-groups/{groupId}/preflight✓Per service: new to the target, own variables it brings, and what would block it (variables from below)
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/copy-to-level✓Copy a service into a lower level as a group of its own
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/bring-down✓Run a service’s current image in a lower level
DELETE/orgs/{orgId}/projects/{projectId}/services/{serviceId}/level-copy✓Delete this level’s copy of a service and its routes; the level uses the one above, and the service leaves a group that builds here
POST/orgs/{orgId}/projects/{projectId}/own-database✓Give this level its own copy of a database it uses from above, empty or clone (restores the source’s latest backup)
MethodPathAuthDescription
DELETE/nodes/self-deregisternode tokenSelf-deregister a node using its registration token and node ID
GET/edge/snapshotedge node secretThe edge’s snapshot for a proxy with no database: routes, TCP ports, mesh gate state, page brands, stopped services. Only the node holding the cluster agent, with its mnode- secret and X-Meshploy-Node, on a CLUSTER_AGENT API; 401 otherwise. With routes and mesh (the versions held) it waits up to wait seconds (25 at most) for them to move, and answers 204 when they did not
POST/edge/tcp-statusedge node secretWhat became of TCP routes on the edge: {"routes":[{"id","status","last_error"}]}, status open, paused or failed
POST/nodes/self-registerregistration tokenSelf-register a node using a registration token. The node holding the cluster’s server on a CLUSTER_AGENT API also sends cluster_agent (certificate, token, port), only with a provisioning token minted for it: 422 for any other token, 409 when an agent is already registered. The same machine registering again after a revocation keeps its node
GET/orgs/{orgId}/cluster/headscale-preauth-key✓Get the most recent active Headscale preauth key
POST/orgs/{orgId}/cluster/headscale-preauth-key✓Generate a new Headscale preauth key for joining the WireGuard mesh
GET/orgs/{orgId}/cluster/join-token✓Get a token for joining the cluster: the server’s own on a gateway; through a cluster agent, one it mints that lasts an hour (expires_at)
GET/orgs/{orgId}/projects/{projectId}/map✓Everything a level’s map is drawn from, in one read: services, stacks, routes, volumes, troubles, build hints, and which services read another’s published variables
GET/orgs/{orgId}/placement✓Org admins. Where every service runs: each node’s allocatable room and what its pods request, each service’s pods, requests, pinned node and the nodes its volumes’ data is bound to
GET/orgs/{orgId}/placement/nodes/{node}/what-if✓Org admins. What would happen if the cluster node went down: per service, keeps running, moves (and where), or stays down and why. Nothing is stopped
GET/orgs/{orgId}/cluster/mesh-health✓Report whether the control plane can reach Headscale
GET/orgs/{orgId}/cluster/orphans✓List cluster workloads that no service owns
DELETE/orgs/{orgId}/cluster/orphans/{namespace}/{name}✓Remove a cluster workload that no service owns
POST/orgs/{orgId}/node-provisioning-tokens✓Create a single-use node provisioning token. cluster_agent: true mints the one for the node holding the cluster’s server, with its cluster agent; only on a CLUSTER_AGENT API (422 otherwise), and only while none is registered (409)
GET/orgs/{orgId}/node-registration-token✓Get the node registration token
POST/orgs/{orgId}/node-registration-token✓Generate (or rotate) the node registration token
GET/orgs/{orgId}/nodes✓List nodes in an organization
POST/orgs/{orgId}/nodes✓Register a new node
GET/orgs/{orgId}/nodes/{nodeId}✓Get a node
PATCH/orgs/{orgId}/nodes/{nodeId}✓Update a node
DELETE/orgs/{orgId}/nodes/{nodeId}✓Remove a node from Headscale, the cluster and Meshploy, in that order. 200 {"removed": true} when it is gone; 202 {"removed": false, "error": ...} while Headscale has not confirmed the peer is removed, which is retried every minute. Calling it again retries at once
POST/orgs/{orgId}/nodes/{nodeId}/cancel-removal✓Stop a node removal that is waiting for Headscale; 409 if none is
DELETE/orgs/{orgId}/nodes/{nodeId}/cluster-agent✓ adminRevoke the node’s cluster agent: the cluster is no longer reached through it, open requests to it are cut, and a new agent may register. 204; 404 if the node carries none
GET/orgs/{orgId}/nodes/{nodeId}/metrics✓Get live resource metrics for a node (requires node_exporter)
GET/orgs/{orgId}/nodes/{nodeId}/containers✓List containers running on a node that Meshploy does not manage
GET/orgs/{orgId}/discovery✓List endpoints and containers on this org’s nodes that Meshploy does not route
POST/orgs/{orgId}/discovery/ignores✓Record that a discovered endpoint is known and correct as it is. Not called by the console: Discovery is a view of a machine, not a list to clear. Kept for a future “new endpoint appeared” notification, which needs a baseline
DELETE/orgs/{orgId}/discovery/ignores/{ignoreId}✓Remove that record
MethodPathAuthDescription
GET/orgs/{orgId}/projects/{projectId}/services✓List services in a project
POST/orgs/{orgId}/projects/{projectId}/services✓Create a service
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}✓Get a service
PATCH/orgs/{orgId}/projects/{projectId}/services/{serviceId}✓Update a service
DELETE/orgs/{orgId}/projects/{projectId}/services/{serviceId}✓Delete a service
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/build-config✓Get build config for a service
PATCH/orgs/{orgId}/projects/{projectId}/services/{serviceId}/build-config✓Create or update build config for a service
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/build-config/deploy-token✓Regenerate the per-service webhook deploy token
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/build-config/env-vars✓Get build-time environment variables for a service
PUT/orgs/{orgId}/projects/{projectId}/services/{serviceId}/build-config/env-vars✓Set build-time environment variables for a service
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/database-config✓Get database config for a database service
PATCH/orgs/{orgId}/projects/{projectId}/services/{serviceId}/database-config✓Change a database’s network access
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/db/query✓Execute a database query
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/db/schema✓Introspect database schema
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/env-vars✓Get decrypted env vars for a service
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/pods✓List running pods for a service; runtime_class is gvisor for a sandboxed pod
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/pods/metrics✓Live CPU and memory usage per pod (requires metrics-server)
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/reset✓Wipe and re-provision a database (destructive)
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/start✓Start a service
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/stop✓Stop a service
MethodPathAuthDescription
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments✓List deployments for a service
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments✓Trigger a new deployment
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments/{deploymentId}✓Get a deployment
DELETE/orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments/{deploymentId}✓Cancel an active deployment
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments/{deploymentId}/logs/stream✓Stream a deployment’s build log (SSE)
DELETE/orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments/{deploymentId}/record✓Delete a deployment record
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/deployments/{deploymentId}/rollback✓Roll back to a previous successful deployment; 409 when its image was removed from the registry
GET/orgs/{orgId}/projects/{projectId}/image-retention✓A level’s services made on their own that keep every image they build
POST/orgs/{orgId}/projects/{projectId}/image-retention✓Make them keep their last 3 images, removing the rest now
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/redeploy✓Run the current image again, without building; keeps where the image came from
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/source✓Upload a folder, a tar.gz as the body, as the service’s source (?name= its folder name, ?deploy=true to deploy it). Needs the deploy permission; up to 100 MB; stored in the service’s registry, only the latest kept
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/dependents✓Services and jobs, at any level, whose variables come from this service today
GET/orgs/{orgId}/projects/{projectId}/health✓Services in a project that are not staying up, and why: out of memory (with the limit), crashing (with the exit code), image pull, cannot start; and each service’s suggestions from its last build (hints), with their fixes
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/hints/{kind}/dismiss✓Set aside one kind of suggestion (start_command, memory, port) for a service, for everyone
GET/orgs/{orgId}/activity✓Recent deployments and job runs across the projects the caller can see, with each deployment’s level and where its image came from
GET/orgs/{orgId}/overview✓The workspace overview: what needs attention (failures, offline nodes, missing backups, waiting promotions, hotfixes; domains to admins), each count broken down across every level, and delivery over 14 days (runs per day; deploys a week, change failure rate, time to recover and staging-to-production time, all for production; a deploy sparkline per project)
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/logs✓Snapshot of a service’s container logs
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/logs/stream✓Stream a service’s container logs (SSE)
MethodPathAuthDescription
POST/orgs/{orgId}/projects/{projectId}/apply✓Upsert a stack from an inline compose manifest and reconcile it
GET/orgs/{orgId}/projects/{projectId}/stacks✓List stacks for a project
POST/orgs/{orgId}/projects/{projectId}/stacks✓Create a new stack
POST/orgs/{orgId}/projects/{projectId}/stacks/meshploy-config✓Write out every x-meshploy setting an apply would default
GET/orgs/{orgId}/projects/{projectId}/stacks/{stackId}✓Get a stack
PUT/orgs/{orgId}/projects/{projectId}/stacks/{stackId}✓Update a stack’s spec and variables, and the images its built services keep (rollback_enabled, image_retention), set on them at once
DELETE/orgs/{orgId}/projects/{projectId}/stacks/{stackId}✓Delete a stack
POST/orgs/{orgId}/projects/{projectId}/stacks/{stackId}/apply✓Apply the stack spec - reconcile services. deploy: false writes the records and rolls nothing out; files carries what the spec names by path when the server cannot read it (a migration sending a git stack’s repository files from the old checkout)
GET/orgs/{orgId}/projects/{projectId}/stacks/{stackId}/runs✓The stack’s Syncs and Applies, newest first, each with its rollout’s state
GET/orgs/{orgId}/projects/{projectId}/stacks/{stackId}/runs/{runId}✓One Sync or Apply: its services by depends_on layer, each with its deployment, and what the apply said
POST/orgs/{orgId}/projects/{projectId}/stacks/{stackId}/destroy✓Destroy the services this stack created, keeping the stack
GET/orgs/{orgId}/projects/{projectId}/stacks/{stackId}/services✓List services belonging to a stack
POST/orgs/{orgId}/projects/{projectId}/stacks/{stackId}/sync✓Fetch spec from git source and re-apply
MethodPathAuthDescription
POST/orgs/{orgId}/projects/{projectId}/templates/{templateId}/deploy✓Deploy a template into a project as a stack
GET/templates✓List one-click templates
POST/templates/refresh✓Re-read the template catalog from its source
GET/templates/{templateId}✓Get a template (manifest + compose)
GET/templates/{templateId}/iconpublicTemplate icon image
MethodPathAuthDescription
GET/orgs/{orgId}/projects/{projectId}/jobs✓List jobs in a project
POST/orgs/{orgId}/projects/{projectId}/jobs✓Create a job or cron job
GET/orgs/{orgId}/projects/{projectId}/jobs/{jobId}✓Get a job
PATCH/orgs/{orgId}/projects/{projectId}/jobs/{jobId}✓Update a job
DELETE/orgs/{orgId}/projects/{projectId}/jobs/{jobId}✓Delete a job
GET/orgs/{orgId}/projects/{projectId}/jobs/{jobId}/runs✓List run history for a job
DELETE/orgs/{orgId}/projects/{projectId}/jobs/{jobId}/runs/{runId}✓Delete a job run record
POST/orgs/{orgId}/projects/{projectId}/jobs/{jobId}/trigger✓Manually trigger a job run
MethodPathAuthDescription
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/mounts✓List a service’s volume mounts
GET/orgs/{orgId}/projects/{projectId}/volumes✓List volumes
POST/orgs/{orgId}/projects/{projectId}/volumes✓Create a volume
GET/orgs/{orgId}/projects/{projectId}/volumes/{volumeId}✓Get a volume
DELETE/orgs/{orgId}/projects/{projectId}/volumes/{volumeId}✓Delete a volume (must be unattached)
GET/orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/backup✓Get a volume’s backup config
PUT/orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/backup✓Set a volume’s backup config
DELETE/orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/backup✓Remove a volume’s backup config
POST/orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/mounts✓Attach a volume to a service
DELETE/orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/mounts/{mountId}✓Detach a volume mount
PUT/orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/node✓Pin a volume to a node, or clear the pin to auto-schedule
GET/orgs/{orgId}/projects/{projectId}/volumes/{volumeId}/placement✓Where the volume’s claim is actually bound or pinned
MethodPathAuthDescription
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/variable-groups✓List variable groups attached to service
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/variable-groups✓Attach variable group to service
DELETE/orgs/{orgId}/projects/{projectId}/services/{serviceId}/variable-groups/{groupId}✓Detach variable group from service
GET/orgs/{orgId}/projects/{projectId}/jobs/{jobId}/variable-groups✓List variable groups attached to a job
POST/orgs/{orgId}/projects/{projectId}/jobs/{jobId}/variable-groups✓Attach a variable group to a job
DELETE/orgs/{orgId}/projects/{projectId}/jobs/{jobId}/variable-groups/{groupId}✓Detach a variable group from a job
GET/orgs/{orgId}/projects/{projectId}/variable-groups✓List variable groups
POST/orgs/{orgId}/projects/{projectId}/variable-groups✓Create variable group
GET/orgs/{orgId}/projects/{projectId}/variable-groups/{groupId}✓Get variable group
PATCH/orgs/{orgId}/projects/{projectId}/variable-groups/{groupId}✓Update variable group
DELETE/orgs/{orgId}/projects/{projectId}/variable-groups/{groupId}✓Delete variable group
PUT/orgs/{orgId}/projects/{projectId}/variable-groups/{groupId}/items✓Upsert variable group item
GET/orgs/{orgId}/projects/{projectId}/variable-groups/{groupId}/items/{itemId}/value✓Reveal one item’s value. A list never carries a secret’s value, so reading one is its own request, gated on project update rather than view
DELETE/orgs/{orgId}/projects/{projectId}/variable-groups/{groupId}/items/{itemId}✓Delete variable group item
MethodPathAuthDescription
GET/orgs/{orgId}/projects/{projectId}/config-files✓List the project’s config files
POST/orgs/{orgId}/projects/{projectId}/config-files✓Create a config file
GET/orgs/{orgId}/projects/{projectId}/config-files/{fileId}✓Get a config file and the services mounting it
PATCH/orgs/{orgId}/projects/{projectId}/config-files/{fileId}✓Replace a config file’s content, re-applying every service using it
DELETE/orgs/{orgId}/projects/{projectId}/config-files/{fileId}✓Delete a config file that no service mounts
POST/orgs/{orgId}/projects/{projectId}/config-files/{fileId}/attach/{serviceId}✓Mount a config file into a service
DELETE/orgs/{orgId}/projects/{projectId}/config-files/{fileId}/attach/{serviceId}✓Unmount a config file from a service
MethodPathAuthDescription
GET/orgs/{orgId}/projects/{projectId}/routes✓List routes in a project
POST/orgs/{orgId}/projects/{projectId}/routes✓Create a route
GET/orgs/{orgId}/projects/{projectId}/routes/{routeId}✓Get a route
DELETE/orgs/{orgId}/projects/{projectId}/routes/{routeId}✓Delete a route
POST/orgs/{orgId}/projects/{projectId}/routes/{routeId}/targets✓Add a path target to a route
PATCH/orgs/{orgId}/projects/{projectId}/routes/{routeId}/targets/{targetId}✓Update a route target
DELETE/orgs/{orgId}/projects/{projectId}/routes/{routeId}/targets/{targetId}✓Delete a route target
POST/orgs/{orgId}/projects/{projectId}/routes/{routeId}/move✓Move a route to another base domain, keeping its subdomain, zone and targets; keep_redirect leaves the old hostname answering with a 301
POST/orgs/{orgId}/projects/{projectId}/routes/{routeId}/verify-hostname✓Verify DNS ownership of a custom-domain route via TXT record
POST/orgs/{orgId}/projects/{projectId}/routes/{routeId}/publish✓Serve a paused route again
POST/orgs/{orgId}/projects/{projectId}/routes/{routeId}/pause✓Keep a route, answer 404 and issue no certificate. Create one paused with published: false
GET/orgs/{orgId}/routes✓List all routes in an organization
GET/orgs/{orgId}/tcp-routes✓List every published TCP port in an organization, with the gateway’s own
GET/orgs/{orgId}/projects/{projectId}/tcp-routes✓List TCP routes in a project
POST/orgs/{orgId}/projects/{projectId}/tcp-routes✓Publish a TCP port on the gateway
GET/orgs/{orgId}/projects/{projectId}/tcp-routes/{routeId}✓Get a TCP route
POST/orgs/{orgId}/projects/{projectId}/tcp-routes/{routeId}/publish✓Open a paused TCP route’s port again
POST/orgs/{orgId}/projects/{projectId}/tcp-routes/{routeId}/pause✓Close a TCP route’s port and keep the route
PATCH/orgs/{orgId}/projects/{projectId}/tcp-routes/{routeId}✓Change a TCP route’s port or who may connect
DELETE/orgs/{orgId}/projects/{projectId}/tcp-routes/{routeId}✓Stop publishing a TCP port
GET/orgs/{orgId}/edge-fallback✓Where the proxy sends hostnames it has no route for during a migration whose edge was taken first; null when nothing
PUT/orgs/{orgId}/edge-fallback✓Org admin: send the listed hostnames to the old platform’s edge on its side port while no route serves them
DELETE/orgs/{orgId}/edge-fallback✓Org admin: stop sending anything to the old platform’s edge
MethodPathAuthDescription
GET/internal/domain-checkpublic (Caddy)Caddy ask endpoint for On-Demand TLS
GET/internal/ondemand-tls-checkpublic (Caddy)Caddy ask endpoint for On-Demand TLS (self-managed DNS mode)
GET/orgs/{orgId}/domains✓List domains for an organization (primary first)
POST/orgs/{orgId}/domains✓Add a base domain; with dns_provider_id, an on-demand one whose records the provider writes, verified at once; with project_id, reserved for that project
PUT/orgs/{orgId}/domains/{domainId}/project✓Reserve a domain for one project and its levels, or give it back to every project (project_id empty)
GET/orgs/{orgId}/domains/{domainId}✓Get a domain
POST/orgs/{orgId}/domains/{domainId}/verify✓Check the ownership TXT record for a domain
GET/orgs/{orgId}/domains/{domainId}/routes✓Hostnames served under a base domain, across every project
POST/orgs/{orgId}/domains/{domainId}/retire✓Start retiring a base domain: no new routes, everything already on it keeps serving
DELETE/orgs/{orgId}/domains/{domainId}/retire✓Stop retiring a base domain
POST/orgs/{orgId}/domains/{domainId}/make-primary✓Move the primary here. The old primary keeps serving its console, api and headscale names until it is removed
GET/orgs/{orgId}/domains/{domainId}/nodes✓Nodes whose control connection goes through this domain’s headscale name - what stops a former primary being removed
POST/orgs/{orgId}/nodes/{nodeId}/control-moved✓Record that a node now reaches Headscale through the primary. Records what the operator did on the machine; it does not move it
GET/orgs/{orgId}/domains/{domainId}/integrations✓Git provider registrations - a GitHub App’s URLs, an OAuth redirect, repository push hooks - still pointing at this domain
POST/orgs/{orgId}/git-integrations/{id}/move-hooks✓Move an integration’s repository push hooks to the primary’s API address through the provider’s API, per repository
POST/orgs/{orgId}/git-integrations/{id}/registration-updated✓Record that a registration was changed at the provider by hand. For an OAuth redirect, Meshploy starts sending the new URI
GET/orgs/{orgId}/domains/{domainId}/deploy-hooks✓Services whose CI deploy webhook was last called through this domain, as observed from where calls arrive
DELETE/orgs/{orgId}/services/{serviceId}/deploy-hook-call✓Forget a deploy webhook’s last caller, for a CI job that no longer exists. A later call records it again
GET/orgs/{orgId}/custom-domains✓Hostnames that belong to a route rather than a base domain
PATCH/orgs/{orgId}/domains/{domainId}/dns-mode✓Change how a domain’s DNS is arranged
PUT/orgs/{orgId}/domains/{domainId}/dns-provider✓Choose who keeps an on-demand domain’s records: a connected DNS provider (written and read back, verifying the domain) or by hand (Meshploy’s records removed)
DELETE/orgs/{orgId}/domains/{domainId}✓Remove a base domain. A verified one must be retiring and hold no routes; no node may still reach the mesh through it, no git provider may still call it, and no CI job may still deploy through it. An unverified one can go at once

Verifying a domain, changing its DNS mode or removing one records the new domain set in the host agent’s inbox and asks it to regenerate Caddy and CoreDNS. The API writes no configuration itself and reloads nothing: it has no root on the host and no Docker socket.

MethodPathAuthDescription
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups✓List backup configs for a service
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups✓Add a backup config for a service
PATCH/orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups/{id}✓Update a backup config
DELETE/orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups/{id}✓Delete a backup config
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups/{id}/objects✓List restore points for a backup config
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups/{id}/restore✓Restore a database from a backup object
POST/orgs/{orgId}/projects/{projectId}/services/{serviceId}/backups/{id}/trigger✓Manually trigger a backup
GET/orgs/{orgId}/system-backup✓Get system backup config for an org
PUT/orgs/{orgId}/system-backup✓Create or update system backup config
DELETE/orgs/{orgId}/system-backup✓Delete system backup config
GET/orgs/{orgId}/system-backup/objects✓List restore points for the system backup
POST/orgs/{orgId}/system-backup/restore✓Restore system database from a backup object
POST/orgs/{orgId}/system-backup/trigger✓Manually trigger the system backup
MethodPathAuthDescription
GET/orgs/{orgId}/email-config✓Get the org SMTP configuration
PUT/orgs/{orgId}/email-config✓Create or update the org SMTP configuration
DELETE/orgs/{orgId}/email-config✓Remove the org SMTP configuration
POST/orgs/{orgId}/email-config/test✓Send a test email to one address; a failed send returns success: false and the error
GET/notification-events✓List the events a notification channel can subscribe to
GET/orgs/{orgId}/notification-channels✓List notification channels
POST/orgs/{orgId}/notification-channels✓Create a notification channel
PUT/orgs/{orgId}/notification-channels/{id}✓Update a notification channel
DELETE/orgs/{orgId}/notification-channels/{id}✓Delete a notification channel
POST/orgs/{orgId}/notification-channels/{id}/test✓Send a test notification and record the attempt
GET/orgs/{orgId}/notification-channels/{id}/deliveries✓A channel’s delivery attempts, newest first (?status=failed, ?limit=, ?before=<timestamp> to page back)
POST/orgs/{orgId}/notification-deliveries/{id}/retry✓Send a recorded attempt again
MethodPathAuthDescription
GET/gitea/callbackOAuth stateGitea OAuth callback
GET/github/app-callbackOAuth stateGitHub App installation callback
GET/github/callbackOAuth stateGitHub OAuth callback
GET/gitlab/callbackOAuth stateGitLab OAuth callback
GET/orgs/{orgId}/git-integrations✓List git integrations
POST/orgs/{orgId}/git-integrations✓Create a GitLab, Gitea or Bitbucket integration from a token
POST/orgs/{orgId}/git-integrations/github✓Start a GitHub App integration (manifest flow)
POST/orgs/{orgId}/git-integrations/oauth✓Start a GitLab, Gitea or Bitbucket OAuth App connection
DELETE/orgs/{orgId}/git-integrations/{id}✓Delete a git integration
GET/orgs/{orgId}/git-integrations/{id}/branches✓List branches for a repository
POST/orgs/{orgId}/detect-stack✓Look at a repository’s branch (shallow clone of its tree and small files) and suggest a builder, port, start command and memory limit for a new service
GET/orgs/{orgId}/git-integrations/{id}/install-url✓Get GitHub App install URL for a specific integration
GET/orgs/{orgId}/git-integrations/{id}/oauth-reconnect✓Re-generate OAuth authorization URL for a pending integration
GET/orgs/{orgId}/git-integrations/{id}/push-hook✓Where the provider should deliver pushes, and the secret to sign them with (org admin)
GET/orgs/{orgId}/git-integrations/{id}/repos✓List repositories for a git integration
GET/orgs/{orgId}/registry-integrations✓List container registry integrations
POST/orgs/{orgId}/registry-integrations✓Add a container registry integration
DELETE/orgs/{orgId}/registry-integrations/{id}✓Remove a container registry integration
GET/orgs/{orgId}/storage-integrations✓List object storage integrations
POST/orgs/{orgId}/storage-integrations✓Add an object storage integration
DELETE/orgs/{orgId}/storage-integrations/{id}✓Remove an object storage integration
GET/orgs/{orgId}/dns-providers✓List connected DNS providers
POST/orgs/{orgId}/dns-providers✓Connect a DNS provider; the token must be able to write records in at least one zone
POST/orgs/{orgId}/dns-providers/{id}/check✓Check its token again and keep the zones it may write in
DELETE/orgs/{orgId}/dns-providers/{id}✓Disconnect a DNS provider; refused while a domain uses it
MethodPathAuthDescription
GET/orgs/{orgId}/nodes/{nodeId}/terminalticketWebSocket: shell on a node
GET/orgs/{orgId}/projects/{projectId}/services/{serviceId}/pods/{podName}/terminalticketWebSocket: shell in a pod
POST/terminal/ticket✓Mint a single-use ticket for a terminal WebSocket
MethodPathAuthDescription
POST/webhooks/deploy/{serviceId}deploy tokenInbound deploy webhook
POST/webhooks/github/{integrationId}HMACInbound GitHub push webhook
POST/webhooks/git/{provider}/{integrationId}HMAC / tokenInbound push webhook for GitLab, Gitea/Forgejo and Bitbucket
MethodPathAuthDescription
GET/system/login-infopublicThe console and API addresses for a CLI finding this server, on the domain it was reached through when that domain serves the platform, else the primary’s; and the version
GET/system/edge✓The gateway’s edge name (edge.<primary domain>), which a custom hostname points at with a CNAME, its public address, and whether the name resolves there now
GET/system/exposure✓Report whether this gateway runs without a host firewall
POST/system/notices/{key}/dismiss✓Dismiss a console advisory for the current user
GET/system/channels✓Describe the stable and edge channels, where this server is, and whether it may switch
GET/system/upgrade✓Report whether this server can be upgraded from the console, and the last upgrade
POST/system/upgrade✓Queue an upgrade of this server to the latest build on its channel, a switch to the other channel ({"channel": "edge"}), or a switch to the Enterprise images the active licence grants ({"edition": "enterprise"}). Instance owner only
GET/system/edge-upgrade✓Through a cluster agent, the edge’s upgrade: a request still waiting, the one named by ?request=, and the updater’s last run as the edge reports it. The server’s owner only
POST/system/edge-upgrade✓Asks the edge to upgrade itself and its cluster agent (channel: stable or edge, else its own), as Meshploy does release by release; its updater runs it. 202 with the request id. The server’s owner only
GET/system/version✓Get current and latest platform version
GET/system/migrate/dokploy✓Instance owner: the host agent’s latest Dokploy detection and plan, and the state of each request
POST/system/migrate/dokploy/{kind}✓Instance owner: run a stage of the migration. detect and plan are read-only on the host. credential mints the migration agent’s token and leaves it in the inbox for the agent to take once. prepare is stage 1 - the Meshploy side, services stopped and routes paused, Dokploy untouched. move is stage 2 for one group (body: group) and is the first kind that stops anything. cutover is stage 3, handing over ports 80 and 443; with edge_first it runs before every group has moved, and the old edge keeps serving them from a side port. rollback undoes one group, or everything when no group is given; 409 when the agent is not reporting
GET/system/host-agent✓Whether the gateway’s host agent is reporting, its version and the firewall it found
POST/system/check-updates✓Ask GitHub for the newest release or build now, past the caches; at most one real check every 30 seconds
MethodPathAuthDescription
GET/healthpublicHealth check
GET/install.shpublicInstall script, with this gateway’s address filled in
GET/join/macos.shpublicJoin a Mac as a mesh-only node
GET/join/windows.ps1publicJoin a Windows machine as a mesh-only node
GET/POST/mcpagent tokenRemote MCP (Streamable HTTP), permission-scoped
GET/uninstall.sh✓Uninstall script

GET /orgs/{orgId}/nodes enriches each node with live Headscale peer data (online status, last seen, FQDN). When a node has a stored headscale_id the lookup is O(1). Nodes without an ID fall back to an IP scan and store the ID as a side-effect for future calls.

Worker nodes authenticate with a registration or provisioning token rather than a user JWT:

  • Self-register — called by install.sh. Accepts either a mreg-<hex> registration token (reusable, org-wide) or a mprov-<hex> provisioning token (single-use, with expiry). Creates the node record and returns the node ID. A cluster node also gets its k3s join token there; when none can be had the node is registered anyway, with its secret, and k3s_join_error says why, since the single-use token it spent cannot register it again.
  • Provision (POST /nodes/provision): the public call before the mesh, made with a provisioning token. It answers the mesh credentials, the API’s own mesh address (api_mesh_url, on API_PORT), and cluster_agent: true for a token minted for the cluster’s server, which install.sh installs as an edge.
  • Self-deregister — called by uninstall.sh. Removes the node from Headscale, the k3s cluster, and the database.

VariableRequiredDescription
DATABASE_URLYesPostgreSQL DSN
JWT_SECRETYesSecret for signing JWTs
ENCRYPTION_KEYYesExactly 32 characters: AES-256-GCM field encryption
API_PORTNoListen port (default: 4000)
API_BASE_URLNoPublic base URL of the API (default: http://localhost:4000)
FRONTEND_URLNoConsole URL (default: http://localhost:5173)
SETUP_TOKENNoGates the first registration; set by install.sh. Empty disables the check
SIGN_IN_LINK_KEYNoLets whoever holds it make one-time console sign-in links (POST /system/sign-in-links). Empty (the default) turns them off
HEADSCALE_URLNoHeadscale API URL
HEADSCALE_API_KEYNoHeadscale API key
HEADSCALE_USERNoHeadscale user pre-auth keys are created under (default: meshploy)
KUBECONFIGNoPath to kubeconfig; empty = in-cluster
K3S_SERVER_URLNoOverride the k3s API URL (needed when the API runs in Docker)
K3S_TLS_SERVER_NAMENoName the cluster certificate is verified against when K3S_SERVER_URL rewrites the address
K3S_SKIP_TLS_VERIFYNoEscape hatch: disables authentication of the cluster connection. Leave unset
K3S_TOKENNoNode token for workers joining the cluster
BUILDER_IMAGENoOverride the builder container image
DOMAINNoBase domain; seeds the org domain record
MESH_IPNoGateway’s WireGuard mesh IP; seeds the gateway node
GATEWAY_HOSTNAMENoGateway hostname, used for gateway node seeding
PUBLIC_IPNoGateway public IP, backfilled on the gateway node record
HOST_GATEWAY_IPNoDocker bridge IP, used when the API runs in Docker to reach the gateway’s node_exporter
FIREWALL_STATENoWhat install.sh saw on the host: none, ufw or firewalld. Drives the console’s exposure notice
NODEPORT_ADDRESSESNoThe CIDR kube-proxy binds published ports to, e.g. 100.64.0.0/10 for the mesh. Empty means every interface, which the console warns about when a database is published
FIREWALL_CHECKED_ATNoWhen install.sh checked the firewall (RFC3339)
BUILTIN_REGISTRY_ENDPOINTNoSeed a built-in registry row per org (<host>:<port>)
TEMPLATE_DIRNoLocal template catalog directory; overrides the remote repo
TEMPLATE_REPONoGitHub owner/repo the catalog is fetched from (default: meshploy/meshploy-templates)
TEMPLATE_REPO_REFNoGit ref for the catalog repo (default: main)
TEMPLATE_REFRESH_INTERVALNoHow often the catalog cache refreshes (default: 1h)

Terminal window
cd apps/api
go run main.go

API at http://localhost:4000. The OpenAPI spec is at /openapi.json and needs a bearer token, like every non-public route.

Database migrations run automatically on startup via db.Migrate().