Wire exposes the host's configured runtime across an RPC boundary. The host
chooses which capabilities are safe to expose, where to listen, certificate
identities and trust roots, and authentication/authorization policy. Construction
never opens a listener. Explicit Run creates TCP transport at the supplied
address; Serve accepts a caller-created listener. Both use identical constructor
credentials and middleware. gRPC closes accepted listeners when serving returns.
A server without configured transport credentials provides no encryption or
authentication, including on loopback. client.New defaults to verified TLS with
system trust roots; connecting to an unsecured server requires WithInsecure.
Neither side automatically falls back to plaintext. WithInsecure disables
encryption and peer authentication, rather than disabling TLS certificate checks.
Wire does not load or generate certificates, validate JWTs/API keys, or add
credentials to protobuf messages or UAPI interfaces.
These examples share endpoint 127.0.0.1:50051. The host supplies a server
certificate with DNS SAN wire.test, signed by the CA in serverRoots.
Certificate loading and lifecycle are application responsibilities.
func serveTLS(ctx context.Context, hostRuntime api.Runtime, serverCertificate tls.Certificate) error {
transport := credentials.NewTLS(&tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{serverCertificate},
})
srv, err := server.New(hostRuntime, server.WithTransportCredentials(transport))
if err != nil {
return err
}
return srv.Run(ctx, "127.0.0.1:50051")
}The matching client verifies both the root and certificate identity:
func runTLS(ctx context.Context, serverRoots *x509.CertPool) (out *api.Output, err error) {
remote, err := client.New(ctx,
"127.0.0.1:50051",
client.WithTransportCredentials(credentials.NewTLS(&tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: serverRoots,
ServerName: "wire.test",
})),
)
if err != nil {
return nil, err
}
defer func() { err = errors.Join(err, remote.Close()) }()
return remote.Run(ctx, api.NewAnonymousSource("RETURN 1"))
}grpc.NewClient creates transport configuration without connection I/O.
client.New performs an actual Connect handshake. Never treat successful transport
construction as proof that TLS or authentication succeeded. Do not disable server
certificate verification.
For inventory.example.com:50051, the host supplies a server certificate whose
DNS SAN is inventory.example.com, signed by a CA trusted by the client system.
The host's chosen address must resolve to an interface on which it can listen.
The matching pair needs no custom client trust configuration:
func serveInventory(ctx context.Context, hostRuntime api.Runtime, certificate tls.Certificate) error {
srv, err := server.New(hostRuntime, server.WithTransportCredentials(
credentials.NewTLS(&tls.Config{Certificates: []tls.Certificate{certificate}}),
))
if err != nil {
return err
}
return srv.Run(ctx, "inventory.example.com:50051")
}
func runInventory(ctx context.Context) (out *api.Output, err error) {
remote, err := client.New(ctx, "inventory.example.com:50051")
if err != nil {
return nil, err
}
defer func() { err = errors.Join(err, remote.Close()) }()
return remote.Run(ctx, api.NewAnonymousSource("RETURN 1"))
}Default client TLS configurations are fresh per construction, with RootCAs nil
and ordinary chain/identity verification enabled. Wire does not load certificate
files, modify caller credentials, or own the external resources they reference.
Use the private-CA example's endpoint 127.0.0.1:50051, server identity
wire.test, and serverRoots. The client supplies a
certificate with client-authentication usage, signed by the CA in clientRoots.
Configure the host's TLS policy as:
serverTLS := &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{serverCertificate},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: clientRoots,
}
srv, err := server.New(hostRuntime,
server.WithTransportCredentials(credentials.NewTLS(serverTLS)),
)
if err != nil {
return err
}
return srv.Run(ctx, "127.0.0.1:50051")The matching client configuration is:
func runMTLS(ctx context.Context, serverRoots *x509.CertPool, certificate tls.Certificate) (out *api.Output, err error) {
clientTLS := &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: serverRoots,
ServerName: "wire.test",
Certificates: []tls.Certificate{certificate},
}
remote, err := client.New(ctx, "127.0.0.1:50051",
client.WithTransportCredentials(credentials.NewTLS(clientTLS)),
)
if err != nil {
return nil, err
}
defer func() { err = errors.Join(err, remote.Close()) }()
return remote.Run(ctx, api.NewAnonymousSource("RETURN 1"))
}Host middleware can inspect peer.FromContext(ctx) and
credentials.TLSInfo.State.VerifiedChains. Certificate verification authenticates
peers; deciding what each peer may do remains host authorization policy.
The following is host application code, not Wire authentication APIs. The host
supplies validateToken, including its own issuance, expiry, and authorization
rules. Failures do not disclose token values or validator error text.
func tokenInterceptors(validateToken func(context.Context, string) error) (
grpc.UnaryServerInterceptor, grpc.StreamServerInterceptor,
) {
authenticate := func(ctx context.Context) error {
values := metadata.ValueFromIncomingContext(ctx, "authorization")
if len(values) != 1 {
return status.Error(codes.Unauthenticated, "authentication required")
}
token, ok := strings.CutPrefix(values[0], "Bearer ")
if !ok || token == "" || validateToken(ctx, token) != nil {
return status.Error(codes.Unauthenticated, "authentication required")
}
return nil
}
unary := func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, next grpc.UnaryHandler) (any, error) {
if err := authenticate(ctx); err != nil {
return nil, err
}
return next(ctx, req)
}
stream := func(host any, ss grpc.ServerStream, _ *grpc.StreamServerInfo, next grpc.StreamHandler) error {
if err := authenticate(ss.Context()); err != nil {
return err
}
return next(host, ss)
}
return unary, stream
}Configure both RPC types with a server certificate. Mutual TLS can be added using the client-certificate policy above:
serverTLS := &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{serverCertificate},
}
unary, stream := tokenInterceptors(validateToken)
srv, err := server.New(hostRuntime,
server.WithTransportCredentials(credentials.NewTLS(serverTLS)),
server.WithUnaryInterceptors(unary),
server.WithStreamInterceptors(stream),
)
if err != nil {
return err
}
return srv.Run(ctx, "127.0.0.1:50051", server.WithShutdownTimeout(10*time.Second))Use connection-level client PerRPCCredentials so each unary operation and new
stream carries the token, including calls after client.New and detached cleanup:
type bearerToken string
func (token bearerToken) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
return map[string]string{"authorization": "Bearer " + string(token)}, nil
}
func (bearerToken) RequireTransportSecurity() bool { return true }
func runAuthenticated(ctx context.Context, serverRoots *x509.CertPool, hostIssuedToken string) (out *api.Output, err error) {
clientTLS := &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: serverRoots,
ServerName: "wire.test",
}
// Add the client certificate from the mTLS example if the host requires it.
remote, err := client.New(ctx, "127.0.0.1:50051",
client.WithTransportCredentials(credentials.NewTLS(clientTLS)),
client.WithPerRPCCredentials(bearerToken(hostIssuedToken)),
)
if err != nil {
return nil, err
}
defer func() { err = errors.Join(err, remote.Close()) }()
return remote.Run(ctx, api.NewAnonymousSource("RETURN 1"))
}A token attached only to the constructor context does not authenticate subsequent calls. Authentication hooks must cover unary and streaming RPCs across all services, including operation calls, execution watches, and debugger command/watch streams. Hosts can compute or refresh per-call credentials in their credential provider.
Use From for specialized gRPC dialers, interceptors, or transport limits. This
example uses the same 127.0.0.1:50051 TLS host and wire.test identity. The caller
owns the channel on construction failure and after all logical runtimes close:
func runBorrowed(ctx context.Context, serverRoots *x509.CertPool) (out *api.Output, err error) {
conn, err := grpc.NewClient("127.0.0.1:50051",
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
RootCAs: serverRoots, ServerName: "wire.test",
})),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(8 << 20)),
)
if err != nil {
return nil, err
}
defer func() { err = errors.Join(err, conn.Close()) }()
remote, err := client.From(ctx, conn)
if err != nil {
return nil, err
}
defer func() { err = errors.Join(err, remote.Close()) }()
return remote.Run(ctx, api.NewAnonymousSource("RETURN 1"))
}Server message limits still apply. From accepts no transport options and never
closes the borrowed connection. Multiple logical runtimes may share it without
acquiring one another's lifetimes. An owned channel remains alive after ordinary
runtime/plan API closure while admitted work or descendants retain it; callers
must close those descendants. Failed startup and final release attempt bounded
logical cleanup before owned channel closure, even when cleanup fails. Detached
rollback may add time after the startup deadline.
server.New and Server.Run apply every non-nil option once in registration order, collecting
failures with errors.Join. Nil options are rejected. Construction and startup
reservation/listening do not proceed when any option fails, even if a later
valid option overrides the same setting. Invalid options never run their setters.
Validation uses github.com/ziflex/go-options named builders and collection
validators. Their named outer errors contain relative diagnostic labels, such as
[2] for an interceptor or ["max connections"] for a limit field. Interceptor
failures retain ascending index order; ordering among invalid limit fields is
unspecified. These labels are diagnostic text, not machine-readable paths.
Collection wrappers set ValidationError.OmitValue and omit aggregate values.
Child causes and non-secret rejected values remain available, for example
unary interceptors: [2]: must not be nil: value=<nil>. Credentials and unrelated
runtime identity metadata are excluded from validation messages. Numeric values,
empty identity names, and nil entries provide rejected-value context.
The server accepted values and defaults are unchanged: empty interceptor lists are
no-ops, every limit is positive, and explicit non-positive shutdown timeouts fail.
Use errors.As to inspect the first matching validation error in the joined
chain, while retaining the complete error for reporting all failures:
var invalid gooptions.ValidationError
if errors.As(err, &invalid) {
return fmt.Errorf("invalid option %s: %w", invalid.Field, err)
}Import gooptions "github.com/ziflex/go-options". Standard errors.Is and
errors.As retain access to validator causes. These are constructor configuration
errors; RPC authentication and Wire's sanitized runtime error contracts remain
unchanged.
Client options follow the same ordered validation convention. Nil/typed-nil
credentials fail locally; repeated transport credentials use the last valid
value and repeated per-RPC options append providers in gRPC registration order.
WithInsecure is idempotent and conflicts with explicit transport credentials in
either order. No later option erases earlier validation failures. gRPC rejects
plaintext combined with credentials requiring transport security, without sending
protected metadata. Credential providers own refresh and concurrency behavior;
Wire adds no token cache or lifecycle. See client options.
Repeated interceptor options append in registration order. Wire recovery is the outermost wrapper, followed by host middleware, then the handler. Host middleware can reject requests before handler resource allocation or runtime work. Ordinary host authentication/authorization statuses pass through unchanged; invocation panics receive Wire's existing sanitized internal status. Captured interceptor lists are copied, but middleware closures must be safe for concurrent RPCs. Unary replacement contexts and wrapped stream contexts reach the next handler; incoming metadata and peer authentication information remain available.
Configured message and resource limits still apply with middleware installed. Do not use authentication as a substitute for runtime policy or resource limits.
Authentication is not tenant isolation. Wire's logical connection/resource ownership is not bound to a middleware principal. Do not assume different validated users are isolated by token verification alone.
Stream middleware runs at establishment. An already-open stream does not acquire automatic token revocation or expiry enforcement. Those policies, including cancellation of established streams when required, belong to the host.
Run closes its listener and manages shutdown with a default 30-second budget,
starting when shutdown begins. An earlier explicit deadline shortens it; later
callers cannot extend it. Timeout forces transport shutdown and returns an error
matching context.DeadlineExceeded, joined with other known failures. Hosted
cleanup may remain pending under its original owner; subsequent Shutdown calls
observe its retained settlement result. Successful return means managed cleanup
settled. Wire never closes the borrowed host runtime. See the
serving lifecycle.