Skip to content

Add connect(): serve over an outbound connection to Restate Cloud - #822

Merged
slinkydeveloper merged 22 commits into
mainfrom
cloud-exp-revamp
Oct 31, 2026
Merged

slinkydeveloper merged 22 commits into
mainfrom
cloud-exp-revamp

Conversation

@slinkydeveloper

@slinkydeveloper slinkydeveloper commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

restate.connect({ services, name }) is the counterpart of serve() for Restate Cloud: the services are served over an outbound connection to the environment, with no inbound listener, no tunnel container and no extra package. serve() is unchanged; there is no mode detection and nothing in the environment flips one into the other.

import * as restate from "@restatedev/restate-sdk";

restate.connect({ services: [greeter], name: "greeter-v1" });

Configuration is two environment variables (or the matching options):

  • RESTATE_CONNECT_KEY — one URL-shaped string from the Cloud UI (Development → Configure SDK): restate://<api key>@<environment id>.env.<region domain>/?signing-key=…, carrying the credential, the environment, the region and the request-identity key. Optional tunnel-srv / tunnel-servers parameters override discovery; unknown parameters are ignored with one warning.
  • RESTATE_CONNECT_NAME — the deployment identity (connect({ name }) in code). No inference: an unnamed deployment is an error that says what to set.

On the first successful handshake the SDK logs how to register, once:

Connecting to Restate Cloud environment env_… (<region domain>)
Connected as "greeter-v1"
Register it once:
  restate deployments register connect://greeter-v1
  or open https://cloud.restate.dev/to/overview?register-deployment=connect://greeter-v1

Errors link the Cloud UI page they refer to (/to/developers/integration#configure-sdk and #configure-client). A non-retryable stop is explained through the SDK logger at error level, in plain words with the fix, e.g. for a rejected key:

Restate Cloud rejected the connect key for environment env_…: it is invalid, revoked, or for another environment.
Create a new key at https://cloud.restate.dev/to/developers/integration#configure-sdk and set RESTATE_CONNECT_KEY.
Not retrying. Fix the configuration and restart.

API

  • ConnectOptions = EndpointOptions & { name?, key?, connection?: ConnectionOptions }. ConnectionOptions holds server discovery (servers, serversSrv), workerId and the tuning (reconnect, drain, HTTP/2 limits, tls: ConnectTlsOptions).
  • ReconnectRetryPolicy has the shape of the invocation and client retry policies (initialInterval, maxInterval, exponentiationFactor, maxAttempts, maxDuration); the bounds default to false since a deployment should keep reconnecting, and reaching one is a fatal stop like any other.
  • Connection: ready, name, close(), shutdown(). close()/shutdown() reject with the fatal error if the connection had already stopped on a non-retryable failure. Registration is by name; the deployment URL stays internal (logged at debug, always on port 9080).
  • @restatedev/restate-sdk-clients: connect() without a url reads RESTATE_INGRESS_KEY (same grammar, from Development → Configure Client) and uses https://<host>:8080 with the key as bearer. Nothing set still fails fast, since a URL was always required.
  • parseCloudKey in restate-sdk-core is the shared key parser.

Compatibility

  • @restatedev/restate-sdk-tunnel is untouched: identical to main, its own copy of the engine, its own tests. restate.connect() has its own copy of the engine inside restate-sdk; nothing is shared or adapted between the two, so existing connectTunnel() users see no change at all.
  • The older variable spellings keep resolving behind the two new ones, undocumented: RESTATE_INPROC_* (restate-operator), RESTATE_TUNNEL_NAME, and the expanded RESTATE_* set (environment id, token or token file, region or SRV / servers, signing key). An operator-configured pod (RESTATE_INPROC_* plus a token file) works with connect({ services }) unchanged; there is an end-to-end test for it.

Build fix

packages/tests/restate-e2e-services built with tsc -b and referenced the SDK's composite project, so it emitted raw declaration files into restate-sdk/dist, overwriting tsdown's flattened node.d.ts whenever an entry source had changed and breaking api-extractor on the node entry. It now builds with tsc --project and without the references (turbo already builds the dependencies first, and the package resolves them through node_modules).

Verification

Full pipeline green (76/76 tasks): format, lint, types, build, exports, API report, tests. The fake-cloud engine tests run through connect() with a key (and, for their legacy spelling, through a small test-only shim); new tests cover key parsing, name/key resolution order, error messages, the banner, the operator setup, and the clients defaults.

Release note

Connect to Restate Cloud with connect(). restate.connect({ services, name: "greeter-v1" }) serves your services over an outbound connection to a Restate Cloud environment — no inbound listener, no tunnel container, no extra package. It is configured by RESTATE_CONNECT_KEY, one string from the Cloud UI (Development → Configure SDK), and RESTATE_CONNECT_NAME (or the name option), the deployment identity you register with restate deployments register connect://<name> or from the Cloud UI link the SDK prints on startup. serve() is unchanged. @restatedev/restate-sdk-tunnel is unchanged. @restatedev/restate-sdk-clients: connect() without a url reads RESTATE_INGRESS_KEY (Development → Configure Client).

🤖 Generated with Claude Code

slinkydeveloper and others added 7 commits October 30, 2026 10:07
The in-process tunnel client moves from @restatedev/restate-sdk-tunnel into
restate-sdk under src/endpoint/tunnel and is exported from the /node entry
(connectTunnel plus its option/handle types), so serve() can connect to
Restate Cloud without an extra package. The engine builds its handler from
NodeEndpoint directly and takes Duration/millis helpers from
restate-sdk-core; the fetch and lambda entries are untouched.

@restatedev/restate-sdk-tunnel stays as a compatibility re-export of
@restatedev/restate-sdk/node with a workspace peer range. Its tests move to
restate-sdk/test/tunnel and run against the SDK source.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One URL-shaped string identifies a Cloud environment and carries its
credential: restate://<api key>@<bare environment id>.env.<region domain>/
with optional query parameters. The SDK's connect() reads it from
RESTATE_CONNECT_KEY and the clients package from RESTATE_INGRESS_KEY, so the
parser lives in restate-sdk-core: standard `new URL()` parsing, the API key
from the userinfo slot (optional, percent-decoded), the environment id and
region domain derived from the host, the parameters handed back for the
caller to interpret. A bad scheme, path, port, fragment or host is an error
naming the source variable and the expected shape.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
restate.connect({ services, name }) is the counterpart of serve() for Restate
Cloud: an outbound connection, no listener, no mode detection, nothing in the
environment flips one into the other. It is exported from the root and /node
entries and configured by two inputs:

- name: option → RESTATE_CONNECT_NAME → error. No inference; an unnamed
  deployment is a mistake and the error says what to set.
- key: option → RESTATE_CONNECT_KEY → error naming the variable and the
  Cloud UI page (Development → Configure SDK). The key's signing-key
  parameter is the identity key unless identityKeys is given; tunnel-srv /
  tunnel-servers (or the servers / serversSrv options) override the SRV
  derived from the region domain; unknown parameters are ignored with one
  warning.

The older spellings keep resolving behind those two, undocumented: the
RESTATE_INPROC_* names the restate-operator injects, RESTATE_TUNNEL_NAME,
and the expanded RESTATE_* variables (environment id, token or token file,
region or SRV / servers, signing key).

On ready the SDK logs how to register, through the SDK logger:

  Connecting to Restate Cloud environment env_… (<region domain>)
  Connected as "<name>"
  Register it once:
    restate deployments register connect://<name>
    or open https://cloud.restate.dev/to/overview?register-deployment=connect://<name>

The handle is Connection (ready, name, deploymentUrl, close, shutdown,
connectionCount, error) and the options ConnectOptions / ConnectTuningOptions
/ ConnectTlsOptions, none of them carrying tunnel-era names. The legacy
connectTunnel() with its original ConnectTunnelOptions / TunnelConnection
runs on the same engine but is not part of this package's surface: /node
exports it only as _connectTunnel (and _-prefixed types) for
@restatedev/restate-sdk-tunnel to re-export.

The engine (src/endpoint/tunnel) now takes fully resolved EngineOptions:
identity resolution moved to src/endpoint/connect, options.ts validates
tuning only, the deployment URL always uses port 9080 (never the zone port
in the proxy-url trailer), and diagnostics go through the SDK LoggerTransport
with levels.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The package re-exports the legacy API under its public names
(connectTunnel, ConnectTunnelOptions, TunnelConnection, TunnelTlsOptions)
from the _-prefixed internal exports of @restatedev/restate-sdk/node, which
no longer exposes connectTunnel itself. Options and behaviour are unchanged;
the function warns once that connect() replaces it. Description and README
point at connect() with RESTATE_CONNECT_KEY and RESTATE_CONNECT_NAME.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The ingress key from the Cloud UI (Development → Configure Client) has the
same grammar as the connect key, without a signing key:
restate://<api key>@<environment id>.env.<region domain>/. Without a url,
connect() uses https://<host>:8080 and the key as bearer (unless an
Authorization header is given); explicit options win; with neither a url
nor the variable it fails fast, as a url was always required. url is now
optional in ConnectionOpts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The greeter connected to Restate Cloud with
connect({ services, name: "greeter-v1" }), run with `pnpm greeter_connect`
after exporting RESTATE_CONNECT_KEY. greeter.ts stays on serve().

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`tsc -b` built the referenced SDK projects too, emitting their declaration
files into packages/libs/restate-sdk/dist. The SDK's own `tsc --noEmit`
(composite) records changed sources as pending emit, so that build then
overwrote tsdown's flattened entry declarations (node.d.ts) with raw
per-file ones, and api-extractor failed on the node entry whenever an
entry source had changed. Turbo already builds the dependencies first,
and this package resolves them through node_modules, so the references
were only there for `-b`; drop both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Test Results

  8 files  ± 0    8 suites  ±0   5m 41s ⏱️ + 2m 20s
 72 tests +12   72 ✅ +12  0 💤 ±0  0 ❌ ±0 
323 runs  +56  323 ✅ +56  0 💤 ±0  0 ❌ ±0 

Results for commit 2b766d1. ± Comparison against base commit 27a6c96.

♻️ This comment has been updated with latest results.

slinkydeveloper and others added 15 commits October 30, 2026 17:32
The legacy package no longer needs anything from restate-sdk beyond the
public API: connectTunnel() translates its options into connect()'s —
tunnelName → name, the identity fields → the key string they encode
(restate://<authToken>@<bare env id>.env.<region>.restate.cloud/?signing-key=…),
the flat tuning → connection, tunnelDiagnosticLogger → a logger transport
that routes the engine's `tunnel:` lines to the hook — and maps the
Connection handle back onto TunnelConnection (tunnelName, proxyUrl,
deploymentUrl, …). The _-prefixed internal exports, the legacy types and
src/endpoint/connect/tunnel.ts are gone from restate-sdk, and the expanded
variables are now read from the environment only.

The engine tests keep their legacy spelling through the same adapter,
imported from the tunnel package's source, so the adapter is exercised
against the engine end to end; the tunnel package gets unit tests for the
option mapping.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
connect() is documented in the SDK README and release note; the examples
package keeps greeter.ts on serve().

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
parseCloudKey moves from restate-sdk-core, where it was public only so the
clients package could share it, into restate-sdk/src/endpoint/connect/key.ts,
unexported. The clients package gets its own trimmed parser for the ingress
key (same grammar and error messages, no parameters to interpret), so no
package exposes the parser. The parser also lowercases the host, rejects a
bare id that still carries env_, no longer echoes the URL path in errors,
and reports an empty tunnel-servers parameter as such.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The package is exactly what main ships — its own copy of the tunnel engine,
its own tests — plus a deprecation notice in the README, the package
description and the connectTunnel() doc comment. restate.connect() in
restate-sdk has its own copy of the engine; nothing is shared or adapted
between the two, so existing connectTunnel() users see no change at all.

The engine tests in restate-sdk keep their legacy spelling through a small
test-only shim onto connect().

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The "Connecting to Restate Cloud environment …" line is logged once the
engine has validated the tuning, so an invalid servers entry no longer
prints it before throwing. The doc comment's {@link serve} becomes plain
text (serve is not in scope for api-extractor). A new test runs connect()
with only the RESTATE_INPROC_* variables and a token file, as the
restate-operator configures a pod, against the fake cloud.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
No deprecation notice either; the package is untouched by this change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Registration is by name (connect://<name>); the URL stays internal to the
engine and is logged at debug.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
connectionCount and error leave the public handle; close() and shutdown()
reject with the fatal error when the connection had already stopped on a
non-retryable failure, so it still surfaces where the caller waits. The
engine tests read the counters through connectEngine(), the un-projected
entry point. ConnectTuningOptions folds into ConnectionOptions: it was the
shared base for the legacy options, which no longer exist here. The public
types are now exactly those reachable from connect()'s signature:
ConnectOptions, ConnectionOptions, ConnectTlsOptions, ReconnectRetryPolicy,
Connection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…etry policies

A non-retryable stop is now explained through the SDK logger at error
level, in plain words with the fix: a rejected key ("Restate Cloud rejected
the connect key for environment env_…: it is invalid, revoked, or for
another environment. Create a new key at <Cloud UI link> and set
RESTATE_CONNECT_KEY."), a rejected name, a failed startupReady gate, or
giving up reconnecting. The engine's own "tunnel: FATAL — …" line moves to
debug. The engine gains an onFatal hook for this.

ReconnectRetryPolicy takes the shape of the invocation and client retry
policies: initialInterval, maxInterval, exponentiationFactor, plus
maxAttempts and maxDuration (number | false). Both bounds default to false,
the reconnect loop stays unbounded unless asked; when a bound is reached
the engine stops with "gave up reconnecting after …", surfaced like every
other fatal stop (ready/close()/shutdown() reject, explained in the log).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ingressFromKey is private to the clients package and its connect() doc is
shorter; the ConnectOptions docs are reworded. The maxAttempts / maxDuration
fields of ReconnectRetryPolicy, lost to an editor save in the previous
commit, are back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The connection is always HTTP/2, so the handler always runs full-duplex;
serve() does not expose the mode either.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nobody needs a readiness gate (call connect() when ready) or a switch to
tell Restate Cloud that this engine does not support a drain protocol it
implements. The startup gate machinery leaves the engine; both drain
protocols are always advertised. The tests of the switched-off paths go
with them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…te Cloud

From the run: DNS resolution failures count toward the reconnect bounds
while nothing is connected (a wrong tunnel-srv no longer retries forever
when a bound is set); no SNI for IP-literal servers (Node's DEP0123
warning); the engine's own "service ready" line moves to debug, the banner
says it.

From the review: neither parser echoes the host or path in errors any more
(a key pasted with a lost `@` put the credential in the message); every
parse error links the Cloud UI page; the clients parser matches the SDK's
(case-insensitive host, no env_ in the bare id, header-safe token); a
connect key for one environment next to RESTATE_[INPROC_]ENVIRONMENT_ID
for another warns that the key wins; an empty tunnel-srv or both tunnel-srv
and tunnel-servers in a key are errors naming the parameters; reaching a
reconnect bound stops the deployment only when no other connection is
alive, otherwise that server is dropped until discovery brings it back;
stale doc references to the former API are gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@slinkydeveloper
slinkydeveloper merged commit d3796c2 into main Oct 31, 2026
14 checks passed
@slinkydeveloper
slinkydeveloper deleted the cloud-exp-revamp branch October 31, 2026 18:33
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 31, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant