Ploy
Ploy

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:

ploy.yaml
kind: dynamic
build: pnpm build
out: dist
service:
  API: api

The 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:

ploy.yaml
service:
  API:
    project: api
    branch: main

Bindings 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:

env.d.ts
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:

src/index.ts
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.yaml explicitly 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:

.env
PLOY_SERVICE_API_URL=http://localhost:3003

Without 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