Service Bindings
Call another deployment in your organization directly from a worker, over Ploy's private network instead of its public URL.
Service Bindings
A service binding lets one deployment call another deployment in the same organization as if it were a local function. The request is routed inside the platform — it never resolves a public hostname, never leaves the private network, and never pays for TLS or a round trip through the edge.
Use one when you split an app into several projects: a frontend that calls an internal API, a public worker in front of a private backend, or a shared service several projects consume.
Configuration
Declare the binding in the calling project's ploy.yaml, naming the target
project:
kind: dynamic
build: pnpm build
out: dist
service:
API: apiThe key (API) is the binding name available on your worker's env. The value
(api) is the name of another project in the same organization.
By default the binding invokes the target project's production deployment — the same deployment its production URL serves. Pin it to a branch instead with the object form:
service:
API:
project: api
branch: mainBindings belong to the project, and production calls through them, so only a production deploy changes them. A preview deployment uses the bindings of the last production deploy, which keeps a branch push from retargeting production's calls.
Run ploy types to generate TypeScript types:
import type { ServiceBinding } from "@meetploy/types";
export interface Env {
API: ServiceBinding;
}Usage
The binding exposes a single fetch() method. Relative URLs resolve against
the target deployment, so you can call a path directly:
export default {
async fetch(request, env) {
const response = await env.API.fetch("/users");
const users = await response.json();
return Response.json({ users });
},
};You can also forward the incoming request, including its method, headers and body:
const response = await env.API.fetch(
new Request(new URL("/orders", request.url), request),
);The target worker runs normally: it sees its own environment variables and bindings, writes its own logs, and its deployment is the one billed for the invocation.
In the target deployment's request logs, a call that arrived through a binding is marked Internal and names the project that made it, so traffic from your own services is distinguishable from traffic from the internet. Each call gets its own request ID, even when the caller forwards its incoming request.
Access and isolation
- Same organization only. A binding can only ever reach a project your
organization owns. A deploy that names a project your organization does not
have fails. If the target is later deleted, moved to another organization, or
has no successful deployment, calls through the binding answer
503. - The caller declares the binding. There is no implicit access: a
deployment is only reachable through a binding another project's
ploy.yamlexplicitly asks for. - The target is fixed by the platform. The calling worker cannot choose which deployment answers. Ploy resolves the target when the worker is loaded and strips any deployment pin the caller tries to send.
- The call is internal. Traffic is routed inside the platform rather than over the target's public URL. The target's public URL keeps working; the binding is an additional, internal way in.
A new deployment of the target project is picked up automatically, usually within two minutes of it going live.
Chained bindings are allowed (A calls B, B calls C), but a cycle is broken:
once a request has passed through eight bindings, Ploy answers 508 Loop Detected instead of recursing further.
Local development
ploy dev runs one project at a time, so there is nothing local for a service
binding to resolve. Point the binding at another running dev server with an
environment variable named after it:
PLOY_SERVICE_API_URL=http://localhost:3003Without that variable, calling the binding returns 503 with a message naming
the variable to set — it never silently falls back to the deployed project.
How is this guide?
Last updated on