Last updated 2026-09-04Development preview

Connect services

Declare private runtime calls, understand streamed HTTP responses, and separate service identity from user authorization.


Sometimes one backend needs an answer from another before it can continue. A service edge describes that relationship: which runtime is calling, which runtime receives the call, and what the caller names that connection.

Fluffy resolves the destination address and invocation identity. Your programs still define the HTTP method, path, request body, and response behavior.

#Understand a service edge

The edge name belongs to the caller. For example, records-api can identify a connection from worker to the api runtime in application example-app. Its generated client is named after the edge, not after an arbitrary URL supplied by the application.

Adding the edge does not make every runtime a caller. Another runtime needs its own uses.services entry and generated package. This is the same per-runtime model shown in How access works.

#Service calls and messages have different completion points

A service call returns an HTTP response from the destination. A message publish returns when the messaging service accepts the message, before the consumer necessarily runs. Use the response or asynchronous workflow that your application actually needs; changing transport does not remove failure handling.

A timeout does not prove the destination performed no work. Design retry-sensitive operations with a stable request identity or another appropriate idempotency mechanism.

#The caller's identity is not the end user's identity

Private invocation authenticates the calling runtime. If the operation is performed on behalf of a person, the destination still needs an application-defined way to authorize that operation. Fluffy's GCP service adapter uses the authorization header for runtime invocation; it is not a transparent channel for an arbitrary browser bearer token.

#Configure a private call

#Declare a service edge

Add this to an existing worker in fluffy-chainsaw.yaml. Here the application is named example-app and contains an api runtime:

yaml
runtimes:
  worker:
    uses:
      services:
        - name: records-api
          application: example-app
          runtime: api
          actions: [invoke]

Keep the destination's declared port and actual handler. For another application, set its declared application/runtime names and make the destination available to the selected workspace/deployment. Do not substitute a public URL for those identities.

#Call the API from your runtime

Generate the worker's SDK for your application's name:

fluffy-chainsaw generate --application example-app --runtime worker --language typescript

Use the sidecar setup for your selected language. These examples use example-app and module example.com/app; adjust the imports to your application. TypeScript uses address and transport; Python uses channel. Go passes its connection and a deadline-bearing context to the function:

import * as runtimev1 from './.fluffy/generated/runtimes/worker/typescript/fluffy/runtime/example-app/worker.js'
import * as servicesv1 from './.fluffy/generated/runtimes/worker/typescript/fluffy/services/v1/services.js'

const client = new runtimev1.ServicesRecordsApiClient(address, transport)
const status = await new Promise<number>((resolve, reject) => {
  const stream = client.call({ deadline: Date.now() + 5000 })
  let status = 0
  stream.on('data', (response: servicesv1.CallResponse) => {
    if (response.status) status = response.status
  })
  stream.on('error', reject)
  stream.on('end', () => resolve(status))
  stream.end(servicesv1.CallRequest.fromPartial({ method: 'GET', path: '/health' }))
})
if (status < 200 || status >= 300) throw new Error(`API returned HTTP ${status}`)

The first request message carries HTTP metadata. Subsequent messages can stream body chunks up to 64 KiB. Close the send side and consume the response stream. For a response with a body, process its body chunks rather than discarding them as this health check does.

A successful gRPC exchange can carry an unsuccessful HTTP response. Inspect both: a transport failure and HTTP 404 need different fixes.

#Run and verify

Implement the destination's /health handler, run the application locally, and make the call from the worker.

For a call across applications, select both in the local command. Confirm an allowed call succeeds, an unknown path returns the expected HTTP error, and a runtime without the edge has no generated client for it.

#Expose a service to a browser

Browser exposure is declared on a web entrypoint, separately from a private runtime edge:

yaml
webEntrypoints:
  web:
    exposedServices:
      api:
        runtime: api

Add this to an existing entrypoint with its artifact or runtime target. Its generated browser package gains services.api.call. It uses the same-origin deployment document instead of the private runtime gRPC connection. Continue to Authentication before exposing private application data.

#Reference: supported service behavior

ItemConstraint
Edge identityname, destination application, and destination runtime are required
Actioninvoke generates Call
Duplicate edge nameRejected within one runtime
DestinationMust resolve to the declared runtime; a local target needs a port
Request pathA valid origin-relative path, not a substitute destination URL
Body chunksAt most 64 KiB per streamed message
ResponseHTTP status and body are distinct from RPC transport success

The GCP adapter requires lowercase valid header names and rejects reserved headers including authorization, host, content-length, and hop-by-hop connection headers. It supplies its own invocation authorization. Do not forward unfiltered browser headers into a private call.

The generated Unwrap is a provider-native escape hatch where supported; it does not grant an undeclared destination. The portable Call path is the normal local/cloud service interface.

#Find the failing boundary

A missing client indicates an absent edge, wrong target, or stale generation. A 404 indicates an absent destination route. A timeout needs the destination's availability, processing behavior, and the caller's deadline checked. A rejected header is a request-contract issue. Give each failure its specific correction instead of broadening exposure to make the symptom disappear.