tako docs guide reference api
GitHub

calls.md — cross-service calls

Package = service (§2.12). mkdir makes a service, not YAML: the analyzer partitions the package tree, and an exported Endpoint var in another package is the RPC surface — typed, visible at the call site, no client code to maintain. Events (Topic/Queue) are the preferred inter-service style; sync calls exist for the hops that must be sync, and their costs are surfaced, never hidden.

Declare

From examples/shop/billing/billing.go:

// Charge is the RPC surface: an EXPORTED Endpoint var, callable from
// other service packages as billing.Charge.Call(ctx, req) — typed,
// visible at the call site, no client code to maintain. Route.Private
// closes the front door: only granted service identities invoke
// (browsers and curl get 403 from Cloud Run itself).
var Charge = tako.API("POST /charge", charge, tako.Route{Private: true})

Route.Private is IAM-only ingress, not network-internal — a settled decision; VPC anything is a later explicit opt-in. Private and public routes cannot share a package (Cloud Run enforces ingress per service).

Use

receipt, err := billing.Charge.Call(ctx, billing.ChargeReq{OrderID: o.ID, AmountCents: o.TotalCents})

The hop is deliberately visible at the call site. Typed errors cross verbatim: billing's tako.Errorf(422, "amount must be positive") is placeOrder's 422 to handle, never a mystery 500. Calls never silently retry. Call cycles are impossible by construction — calling a service means importing its package, and Go forbids import cycles.

The caller's session propagates AUTOMATICALLY: the X-Tako-Session header carries the session id, and the callee resolves it against the shared session store — presenting the id is equivalent to presenting the cookie, the record lookup IS the verification, and revocation kills the identity mid-chain. Billing sees the same verified user; a worker's anonymity stays honest.

Local

In dev, tests, and co-located packages .Call is a direct in-process function call — a breakpoint in placeOrder steps straight into charge. Remote transports are wired only for genuinely remote callees, so nothing changes shape locally.

Cloud

One authenticated HTTP round trip over the endpoint's own route. The caller mints an OIDC identity token from the metadata server for the callee's URL (its own service account's identity, cached ~50 minutes; Cloud Run validates it at the front door), and three headers ride along: X-Tako-Session (identity), X-Tako-Deadline (an absolute deadline the callee honors), X-Tako-Request (trace id, so tako logs correlates a chain). Deadline budgets: the caller propagates its remaining deadline minus a 500ms margin; below the 1s floor the call fails before the network does.

IAM is derived, never hand-written: every call edge grants the caller's service account roles/run.invoker on the callee, and the deploy summary shows it — billing reads ingress: IAM-only (callers: shop-sa). Deploy prices sync hops honestly (warnCallEdges in cmd/tako/deploy.go):

⚠ sync call shop → billing lands on a scale-to-zero service — cold starts bill the caller's latency; give billing tako.Service(tako.ServiceConfig{Profile: tako.ProfileLatency}) (min-instance cost) or make the interaction async
⚠ sync call chain a → b → c — waiting callers hold their own request slots and couple the services' autoscaling; prefer events past one hop

Test it

Call across in-process and assert the typed error and the propagated user — examples/shop/billing_test.go:

_, err := billing.Charge.Call(ctx, billing.ChargeReq{OrderID: "o1", AmountCents: 0})
he, ok := err.(interface{ HTTPStatus() int })
if !ok || he.HTTPStatus() != 422 || !strings.Contains(err.Error(), "amount must be positive") {
	t.Fatalf("err = %v — billing's 422 must be the caller's 422", err)
}

Errors you'll meet

Not yet