diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index edc704f7..3cb4ec04 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -57,6 +57,7 @@ jobs:
kotlin-hello-world-lambda-cdk.zip
typescript-hello-world.zip
+ typescript-hello-world-kubernetes.zip
typescript-hello-world-bun.zip
typescript-hello-world-cloudflare-worker.zip
typescript-hello-world-deno.zip
@@ -73,6 +74,7 @@ jobs:
typescript-nextjs-fullstack.zip
go-hello-world.zip
+ go-hello-world-kubernetes.zip
go-hello-world-lambda-cdk.zip
go-tour-of-orchestration.zip
go-tour-of-workflows.zip
@@ -162,4 +164,4 @@ jobs:
secrets: inherit
with:
source-directory: go/templates/go
- destination-repo: go-template
\ No newline at end of file
+ destination-repo: go-template
diff --git a/.tools/prepare_release_zip.sh b/.tools/prepare_release_zip.sh
index 33a928f2..48fb3101 100755
--- a/.tools/prepare_release_zip.sh
+++ b/.tools/prepare_release_zip.sh
@@ -53,6 +53,7 @@ create_release_zip typescript/end-to-end-applications/chat-bot typescript-chat-b
create_release_zip typescript/end-to-end-applications/ai-image-workflows typescript-ai-image-workflows
create_release_zip typescript/patterns-use-cases typescript-patterns-use-cases
create_release_zip typescript/templates/node typescript-hello-world
+create_release_zip typescript/templates/node-kubernetes typescript-hello-world-kubernetes
create_release_zip typescript/templates/lambda typescript-hello-world-lambda
create_release_zip typescript/templates/bun typescript-hello-world-bun
create_release_zip typescript/templates/cloudflare-worker typescript-hello-world-cloudflare-worker
@@ -69,6 +70,7 @@ create_release_zip go/integrations/go-lambda-cdk go-hello-world-lambda-cdk
create_release_zip go/integrations/knative-go go-knative-go
create_release_zip go/patterns-use-cases go-patterns-use-cases
create_release_zip go/templates/go go-hello-world
+create_release_zip go/templates/go-kubernetes go-hello-world-kubernetes
create_release_zip go/tutorials/tour-of-orchestration-go go-tour-of-orchestration
create_release_zip go/tutorials/tour-of-workflows-go go-tour-of-workflows
diff --git a/.tools/run_go_tests.sh b/.tools/run_go_tests.sh
index 4d14fd98..3b8fd70b 100755
--- a/.tools/run_go_tests.sh
+++ b/.tools/run_go_tests.sh
@@ -14,6 +14,7 @@ function go_build_check() {
go_build_check $PROJECT_ROOT/go/basics
go_build_check $PROJECT_ROOT/go/templates/go
+go_build_check $PROJECT_ROOT/go/templates/go-kubernetes
go_build_check $PROJECT_ROOT/go/integrations/knative-go
go_build_check $PROJECT_ROOT/go/integrations/go-lambda-cdk/lambda
go_build_check $PROJECT_ROOT/go/tutorials/tour-of-orchestration-go
diff --git a/.tools/run_node_tests.sh b/.tools/run_node_tests.sh
index 2d26a6a7..a2b0fd6b 100755
--- a/.tools/run_node_tests.sh
+++ b/.tools/run_node_tests.sh
@@ -12,6 +12,7 @@ function npm_install_check() {
npm_install_check $PROJECT_ROOT/typescript/basics
npm_install_check $PROJECT_ROOT/typescript/templates/node
+npm_install_check $PROJECT_ROOT/typescript/templates/node-kubernetes
npm_install_check $PROJECT_ROOT/typescript/templates/lambda
npm_install_check $PROJECT_ROOT/typescript/templates/cloudflare-worker
npm_install_check $PROJECT_ROOT/typescript/templates/vercel
diff --git a/.tools/update_go_examples.sh b/.tools/update_go_examples.sh
index 7b2df205..cbd72a43 100755
--- a/.tools/update_go_examples.sh
+++ b/.tools/update_go_examples.sh
@@ -28,6 +28,7 @@ function bump_go_sdk() {
bump_go_sdk $PROJECT_ROOT/go/basics
bump_go_sdk $PROJECT_ROOT/go/templates/go
+bump_go_sdk $PROJECT_ROOT/go/templates/go-kubernetes
bump_go_sdk $PROJECT_ROOT/go/integrations/knative-go
bump_go_sdk $PROJECT_ROOT/go/integrations/go-lambda-cdk/lambda
bump_go_sdk $PROJECT_ROOT/go/tutorials/tour-of-orchestration-go
diff --git a/.tools/update_node_examples.sh b/.tools/update_node_examples.sh
index b7c6fd6d..d6a323fe 100755
--- a/.tools/update_node_examples.sh
+++ b/.tools/update_node_examples.sh
@@ -47,6 +47,7 @@ function bump_restate_sdk_deps() {
# Update all projects with package.json
bump_restate_sdk_deps $PROJECT_ROOT/typescript/basics
bump_restate_sdk_deps $PROJECT_ROOT/typescript/templates/node
+bump_restate_sdk_deps $PROJECT_ROOT/typescript/templates/node-kubernetes
bump_restate_sdk_deps $PROJECT_ROOT/typescript/templates/lambda
bump_restate_sdk_deps $PROJECT_ROOT/typescript/templates/typescript-testing
bump_restate_sdk_deps $PROJECT_ROOT/typescript/templates/bun
diff --git a/go/README.md b/go/README.md
index e7ce7243..17ada071 100644
--- a/go/README.md
+++ b/go/README.md
@@ -45,6 +45,7 @@ Examples integrating Restate with other tools and frameworks:
Starter templates for new projects:
- **[Go Template](templates/go)** [
](templates/go/greeter.go)
+- **[Go Kubernetes Template](templates/go-kubernetes)**: Deploy to Kubernetes with an in-process tunnel and Kubernetes YAML files.
## Tutorials
diff --git a/go/templates/go-kubernetes/.agents/plugins/marketplace.json b/go/templates/go-kubernetes/.agents/plugins/marketplace.json
new file mode 100644
index 00000000..b9c8574c
--- /dev/null
+++ b/go/templates/go-kubernetes/.agents/plugins/marketplace.json
@@ -0,0 +1,22 @@
+{
+ "name": "restatedev-plugin",
+ "interface": {
+ "displayName": "Restate"
+ },
+ "plugins": [
+ {
+ "name": "restatedev",
+ "source": {
+ "source": "git-subdir",
+ "url": "https://github.com/restatedev/skills.git",
+ "path": "./plugins/restatedev",
+ "ref": "main"
+ },
+ "policy": {
+ "installation": "AVAILABLE",
+ "authentication": "ON_INSTALL"
+ },
+ "category": "Coding"
+ }
+ ]
+}
diff --git a/go/templates/go-kubernetes/.claude/settings.json b/go/templates/go-kubernetes/.claude/settings.json
new file mode 100644
index 00000000..1c69c4f5
--- /dev/null
+++ b/go/templates/go-kubernetes/.claude/settings.json
@@ -0,0 +1,13 @@
+{
+ "extraKnownMarketplaces": {
+ "restatedev-plugin": {
+ "source": {
+ "source": "github",
+ "repo": "restatedev/skills"
+ }
+ }
+ },
+ "enabledPlugins": {
+ "restatedev@restatedev-plugin": true
+ }
+}
diff --git a/go/templates/go-kubernetes/.dockerignore b/go/templates/go-kubernetes/.dockerignore
new file mode 100644
index 00000000..60efd53b
--- /dev/null
+++ b/go/templates/go-kubernetes/.dockerignore
@@ -0,0 +1,6 @@
+.git
+.idea
+.agents
+.claude
+*.zip
+go-kubernetes
diff --git a/go/templates/go-kubernetes/.gitignore b/go/templates/go-kubernetes/.gitignore
new file mode 100644
index 00000000..3fb70c32
--- /dev/null
+++ b/go/templates/go-kubernetes/.gitignore
@@ -0,0 +1,5 @@
+go-kubernetes
+
+# Restate
+.restate
+restate-data
diff --git a/go/templates/go-kubernetes/Dockerfile b/go/templates/go-kubernetes/Dockerfile
new file mode 100644
index 00000000..38bafea6
--- /dev/null
+++ b/go/templates/go-kubernetes/Dockerfile
@@ -0,0 +1,12 @@
+FROM golang:1.25 AS builder
+
+WORKDIR /src
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
+RUN CGO_ENABLED=0 go build -trimpath -o /service .
+
+FROM gcr.io/distroless/static-debian12:nonroot
+COPY --from=builder /service /service
+ENTRYPOINT ["/service"]
diff --git a/go/templates/go-kubernetes/README.md b/go/templates/go-kubernetes/README.md
new file mode 100644
index 00000000..fbe7c04b
--- /dev/null
+++ b/go/templates/go-kubernetes/README.md
@@ -0,0 +1,16 @@
+# Hello world - Go Kubernetes template
+
+Sample Restate service using the Go SDK, with an in-process tunnel, a Dockerfile, and Kubernetes YAML files.
+
+See the [Kubernetes service deployment guide](https://docs.restate.dev/services/deploy/kubernetes) for more information on using this template to deploy to Kubernetes.
+
+Build the container image with `docker build -t /my-restate-service:0.0.1 .`.
+The Restate operator injects the tunnel configuration when using `tunnelMode: in-process`. The authentication token file is mounted on the deployment.
+
+Invoke `Greeter/Greet` with a JSON object body: `{"name":"World"}`.
+
+## Using AI coding tools
+
+If you use Claude Code or Codex, then the Restate plugin will automatically be installed. For Cursor, consult the [skills repo README](https://github.com/restatedev/skills).
+
+Plugin repo: https://github.com/restatedev/skills/tree/main
diff --git a/go/templates/go-kubernetes/go.mod b/go/templates/go-kubernetes/go.mod
new file mode 100644
index 00000000..2b98ad77
--- /dev/null
+++ b/go/templates/go-kubernetes/go.mod
@@ -0,0 +1,26 @@
+module github.com/restatedev/examples/go/templates/go-kubernetes
+
+go 1.25.0
+
+require (
+ github.com/restatedev/sdk-go v1.0.3
+ github.com/restatedev/sdk-go/x/tunnel v0.1.1
+)
+
+require (
+ github.com/bahlo/generic-list-go v0.2.0 // indirect
+ github.com/buger/jsonparser v1.1.2 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/golang-jwt/jwt/v5 v5.2.3 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/invopop/jsonschema v0.14.0 // indirect
+ github.com/mr-tron/base58 v1.2.0 // indirect
+ github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
+ github.com/tetratelabs/wazero v1.9.0 // indirect
+ go.opentelemetry.io/otel v1.44.0 // indirect
+ go.opentelemetry.io/otel/trace v1.44.0 // indirect
+ go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
+ golang.org/x/net v0.49.0 // indirect
+ golang.org/x/text v0.33.0 // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
+)
diff --git a/go/templates/go-kubernetes/go.sum b/go/templates/go-kubernetes/go.sum
new file mode 100644
index 00000000..cefa47ed
--- /dev/null
+++ b/go/templates/go-kubernetes/go.sum
@@ -0,0 +1,52 @@
+github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
+github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
+github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
+github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
+github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8iyZjg=
+github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I=
+github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
+github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
+github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
+github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/restatedev/sdk-go v1.0.3 h1:pDXoFot3eQk5T//2MpTTfwXsMtDgHeoh0grweW8kZxE=
+github.com/restatedev/sdk-go v1.0.3/go.mod h1:wBPSXBOgBvwTQhLqujDSeW1BA480JXh6ZcIervk7tnA=
+github.com/restatedev/sdk-go/x/tunnel v0.1.1 h1:fIuo9BdzKTJ5qFdDHlAEEctu5bLbFN7WJmxCs7omrk8=
+github.com/restatedev/sdk-go/x/tunnel v0.1.1/go.mod h1:/KEBmcooRbYmnii0DiIIxC0vsuBuf0N4xBVAr3K+F3Q=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
+github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
+go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
+go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
+go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
+go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
+go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
+go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
+go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
+go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
+golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
+golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
+google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
+google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/go/templates/go-kubernetes/greeter.go b/go/templates/go-kubernetes/greeter.go
new file mode 100644
index 00000000..dfd3fe2c
--- /dev/null
+++ b/go/templates/go-kubernetes/greeter.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+ "time"
+
+ restate "github.com/restatedev/sdk-go"
+)
+
+// Greeter is a struct which represents a Restate service; reflection will turn exported methods into service handlers
+type Greeter struct{}
+
+type Greeting struct {
+ Name string `json:"name"`
+}
+
+func (Greeter) Greet(ctx restate.Context, greeting Greeting) (string, error) {
+ name := greeting.Name
+ // Durably execute a set of steps; resilient against failures
+ greetingId := restate.UUID(ctx).String()
+
+ if _, err := restate.Run(ctx,
+ func(ctx restate.RunContext) (restate.Void, error) {
+ return SendNotification(greetingId, name)
+ },
+ restate.WithName("Notification"),
+ ); err != nil {
+ return "", err
+ }
+
+ if err := restate.Sleep(ctx, 1*time.Second); err != nil {
+ return "", err
+ }
+
+ if _, err := restate.Run(ctx,
+ func(ctx restate.RunContext) (restate.Void, error) {
+ return SendReminder(greetingId, name)
+ },
+ restate.WithName("Reminder"),
+ ); err != nil {
+ return "", err
+ }
+
+ // Respond to caller
+ return "You said hi to " + name + "!", nil
+}
diff --git a/go/templates/go-kubernetes/k8s/cloud-env.yaml b/go/templates/go-kubernetes/k8s/cloud-env.yaml
new file mode 100644
index 00000000..0764e7ff
--- /dev/null
+++ b/go/templates/go-kubernetes/k8s/cloud-env.yaml
@@ -0,0 +1,12 @@
+apiVersion: restate.dev/v1beta1
+kind: RestateCloudEnvironment
+metadata:
+ name: my-cloud-env
+spec:
+ environmentId: env_...
+ signingPublicKey: publickeyv1_...
+ region: eu
+ authentication:
+ secret:
+ name: my-cloud-environment-secret
+ key: token
\ No newline at end of file
diff --git a/go/templates/go-kubernetes/k8s/deployment.yaml b/go/templates/go-kubernetes/k8s/deployment.yaml
new file mode 100644
index 00000000..847feae9
--- /dev/null
+++ b/go/templates/go-kubernetes/k8s/deployment.yaml
@@ -0,0 +1,32 @@
+apiVersion: restate.dev/v1beta1
+kind: RestateDeployment
+metadata:
+ name: service
+spec:
+ replicas: 1
+ restate:
+ register:
+ cloud: my-cloud-env
+ tunnelMode: in-process
+ selector:
+ matchLabels:
+ app: service
+ template:
+ metadata:
+ labels:
+ app: service
+ spec:
+ containers:
+ - name: service
+ image: /my-restate-service:0.0.1
+ env:
+ - name: RESTATE_INPROC_AUTH_TOKEN_FILE
+ value: /var/run/secrets/restate.cloud/token
+ volumeMounts:
+ - name: restate-cloud-tunnel-auth
+ mountPath: /var/run/secrets/restate.cloud
+ readOnly: true
+ volumes:
+ - name: restate-cloud-tunnel-auth
+ secret:
+ secretName: restate-cloud-tunnel-auth
diff --git a/go/templates/go-kubernetes/main.go b/go/templates/go-kubernetes/main.go
new file mode 100644
index 00000000..b0c9bff6
--- /dev/null
+++ b/go/templates/go-kubernetes/main.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "context"
+ "log/slog"
+ "os"
+ "os/signal"
+ "syscall"
+
+ restate "github.com/restatedev/sdk-go"
+ "github.com/restatedev/sdk-go/server"
+ "github.com/restatedev/sdk-go/x/tunnel"
+)
+
+func main() {
+ ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+ defer stop()
+
+ srv := server.NewRestate().
+ Bind(restate.Reflect(Greeter{}))
+
+ // The Restate operator injects the RESTATE_INPROC_* tunnel configuration.
+ if err := tunnel.NewTunnel(srv).Start(ctx); err != nil {
+ slog.Error("application exited unexpectedly", "err", err.Error())
+ os.Exit(1)
+ }
+}
diff --git a/go/templates/go-kubernetes/utils.go b/go/templates/go-kubernetes/utils.go
new file mode 100644
index 00000000..b1086510
--- /dev/null
+++ b/go/templates/go-kubernetes/utils.go
@@ -0,0 +1,25 @@
+package main
+
+import (
+ "fmt"
+ restate "github.com/restatedev/sdk-go"
+ "math/rand"
+)
+
+func SendNotification(greetingId string, name string) (restate.Void, error) {
+ if rand.Float32() < 0.7 && name == "Alice" { // 70% chance of failure
+ fmt.Printf("[👻 SIMULATED] Failed to send notification: %s - %s\n", greetingId, name)
+ return restate.Void{}, fmt.Errorf("[👻 SIMULATED] Failed to send notification: %s - %s", greetingId, name)
+ }
+ fmt.Printf("Notification sent: %s - %s\n", greetingId, name)
+ return restate.Void{}, nil
+}
+
+func SendReminder(greetingId string, name string) (restate.Void, error) {
+ if rand.Float32() < 0.7 && name == "Alice" { // 70% chance of failure
+ fmt.Printf("[👻 SIMULATED] Failed to send reminder: %s\n", greetingId)
+ return restate.Void{}, fmt.Errorf(" [👻 SIMULATED] Failed to send reminder: %s", greetingId)
+ }
+ fmt.Printf("Reminder sent: %s\n", greetingId)
+ return restate.Void{}, nil
+}
diff --git a/typescript/README.md b/typescript/README.md
index 8d5f4373..11a19f23 100644
--- a/typescript/README.md
+++ b/typescript/README.md
@@ -68,6 +68,7 @@ Complete applications built with Restate:
Starter templates for new projects:
- **[Node Template](templates/node)**
+- **[Node.js Kubernetes Template](templates/node-kubernetes)**: Deploy to Kubernetes with an ingress tunnel and Kubernetes YAML files.
- **[Bun Template](templates/bun)**
- **[CloudFlare Workers Template](templates/cloudflare-worker)**
- **[Deno Template](templates/deno)**
@@ -79,4 +80,4 @@ Starter templates for new projects:
Step-by-step guides to learn Restate:
- **[Tour of Orchestration](tutorials/tour-of-orchestration-typescript)**: An introduction to microservice orchestration with Restate as described in the [documentation](https://docs.restate.dev/tour/microservice-orchestration).
-- **[Tour of Workflows](tutorials/tour-of-workflows-typescript)**: An introduction to resilient workflows with Restate as described in the [documentation](https://docs.restate.dev/tour/workflows).
\ No newline at end of file
+- **[Tour of Workflows](tutorials/tour-of-workflows-typescript)**: An introduction to resilient workflows with Restate as described in the [documentation](https://docs.restate.dev/tour/workflows).
diff --git a/typescript/templates/node-kubernetes/.agents/plugins/marketplace.json b/typescript/templates/node-kubernetes/.agents/plugins/marketplace.json
new file mode 100644
index 00000000..b9c8574c
--- /dev/null
+++ b/typescript/templates/node-kubernetes/.agents/plugins/marketplace.json
@@ -0,0 +1,22 @@
+{
+ "name": "restatedev-plugin",
+ "interface": {
+ "displayName": "Restate"
+ },
+ "plugins": [
+ {
+ "name": "restatedev",
+ "source": {
+ "source": "git-subdir",
+ "url": "https://github.com/restatedev/skills.git",
+ "path": "./plugins/restatedev",
+ "ref": "main"
+ },
+ "policy": {
+ "installation": "AVAILABLE",
+ "authentication": "ON_INSTALL"
+ },
+ "category": "Coding"
+ }
+ ]
+}
diff --git a/typescript/templates/node-kubernetes/.claude/settings.json b/typescript/templates/node-kubernetes/.claude/settings.json
new file mode 100644
index 00000000..1c69c4f5
--- /dev/null
+++ b/typescript/templates/node-kubernetes/.claude/settings.json
@@ -0,0 +1,13 @@
+{
+ "extraKnownMarketplaces": {
+ "restatedev-plugin": {
+ "source": {
+ "source": "github",
+ "repo": "restatedev/skills"
+ }
+ }
+ },
+ "enabledPlugins": {
+ "restatedev@restatedev-plugin": true
+ }
+}
diff --git a/typescript/templates/node-kubernetes/.dockerignore b/typescript/templates/node-kubernetes/.dockerignore
new file mode 100644
index 00000000..6a9bc407
--- /dev/null
+++ b/typescript/templates/node-kubernetes/.dockerignore
@@ -0,0 +1,4 @@
+.*
+node_modules
+dist
+Dockerfile
diff --git a/typescript/templates/node-kubernetes/.gitignore b/typescript/templates/node-kubernetes/.gitignore
new file mode 100644
index 00000000..b5fa0646
--- /dev/null
+++ b/typescript/templates/node-kubernetes/.gitignore
@@ -0,0 +1,22 @@
+# Node
+node_modules
+dist
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# Restate
+.restate
+restate-data
\ No newline at end of file
diff --git a/typescript/templates/node-kubernetes/Dockerfile b/typescript/templates/node-kubernetes/Dockerfile
new file mode 100644
index 00000000..fc680951
--- /dev/null
+++ b/typescript/templates/node-kubernetes/Dockerfile
@@ -0,0 +1,21 @@
+FROM node:22-alpine
+
+# dumb-init helps handling SIGTERM and SIGINT correctly
+RUN apk add dumb-init
+
+WORKDIR /usr/src/app
+
+# copy package.json and package-lock.json separately to cache dependencies
+COPY package*.json .
+RUN npm install
+
+COPY --chown=node:node . .
+
+RUN npm run build
+
+RUN npm prune --production
+ENV NODE_ENV=production
+
+EXPOSE 9080
+USER node
+CMD ["dumb-init", "node", "./dist/app.js"]
diff --git a/typescript/templates/node-kubernetes/README.md b/typescript/templates/node-kubernetes/README.md
new file mode 100644
index 00000000..8ce4c170
--- /dev/null
+++ b/typescript/templates/node-kubernetes/README.md
@@ -0,0 +1,11 @@
+# Hello world - Node.js Kubernetes template
+
+Sample Restate service using the TypeScript SDK on Node.js, with an ingress tunnel, a Dockerfile, and Kubernetes YAML files.
+
+See the [Kubernetes service deployment guide](https://docs.restate.dev/services/deploy/kubernetes) for more information on using this template to deploy to Kubernetes.
+
+## Using AI coding tools
+
+If you use Claude Code or Codex, then the Restate plugin will automatically be installed. For Cursor, consult the [skills repo README](https://github.com/restatedev/skills).
+
+Plugin repo: https://github.com/restatedev/skills/tree/main
diff --git a/typescript/templates/node-kubernetes/k8s/cloud-env.yaml b/typescript/templates/node-kubernetes/k8s/cloud-env.yaml
new file mode 100644
index 00000000..0764e7ff
--- /dev/null
+++ b/typescript/templates/node-kubernetes/k8s/cloud-env.yaml
@@ -0,0 +1,12 @@
+apiVersion: restate.dev/v1beta1
+kind: RestateCloudEnvironment
+metadata:
+ name: my-cloud-env
+spec:
+ environmentId: env_...
+ signingPublicKey: publickeyv1_...
+ region: eu
+ authentication:
+ secret:
+ name: my-cloud-environment-secret
+ key: token
\ No newline at end of file
diff --git a/typescript/templates/node-kubernetes/k8s/deployment.yaml b/typescript/templates/node-kubernetes/k8s/deployment.yaml
new file mode 100644
index 00000000..847feae9
--- /dev/null
+++ b/typescript/templates/node-kubernetes/k8s/deployment.yaml
@@ -0,0 +1,32 @@
+apiVersion: restate.dev/v1beta1
+kind: RestateDeployment
+metadata:
+ name: service
+spec:
+ replicas: 1
+ restate:
+ register:
+ cloud: my-cloud-env
+ tunnelMode: in-process
+ selector:
+ matchLabels:
+ app: service
+ template:
+ metadata:
+ labels:
+ app: service
+ spec:
+ containers:
+ - name: service
+ image: /my-restate-service:0.0.1
+ env:
+ - name: RESTATE_INPROC_AUTH_TOKEN_FILE
+ value: /var/run/secrets/restate.cloud/token
+ volumeMounts:
+ - name: restate-cloud-tunnel-auth
+ mountPath: /var/run/secrets/restate.cloud
+ readOnly: true
+ volumes:
+ - name: restate-cloud-tunnel-auth
+ secret:
+ secretName: restate-cloud-tunnel-auth
diff --git a/typescript/templates/node-kubernetes/package.json b/typescript/templates/node-kubernetes/package.json
new file mode 100644
index 00000000..7787a7cd
--- /dev/null
+++ b/typescript/templates/node-kubernetes/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "restate-ts-template",
+ "version": "0.0.1",
+ "description": "Template for JavaScript/TypeScript services running with Restate (https://github.com/restatedev/) ",
+ "main": "app.js",
+ "type": "commonjs",
+ "scripts": {
+ "build": "tsc --noEmitOnError",
+ "start": "node ./dist/app.js",
+ "dev": "tsx watch ./src/app.ts",
+ "prebundle": "rm -rf dist",
+ "bundle": "esbuild src/app.ts --bundle --minify --sourcemap --platform=node --target=es2020 --outfile=dist/app.js",
+ "postbundle": "cd dist && zip -r index.zip app.js*",
+ "app": "npm run start",
+ "app-dev": "npm run dev"
+ },
+ "dependencies": {
+ "@restatedev/restate-sdk": "^1.17.0",
+ "@restatedev/restate-sdk-tunnel": "^1.17.0",
+ "zod": "^4.3"
+ },
+ "devDependencies": {
+ "@types/node": "^22",
+ "esbuild": "^0.25.4",
+ "tsx": "^4.19.2",
+ "typescript": "^5.4.5"
+ }
+}
diff --git a/typescript/templates/node-kubernetes/src/app.ts b/typescript/templates/node-kubernetes/src/app.ts
new file mode 100644
index 00000000..5e197672
--- /dev/null
+++ b/typescript/templates/node-kubernetes/src/app.ts
@@ -0,0 +1,34 @@
+import * as restate from "@restatedev/restate-sdk";
+import { sendNotification, sendReminder } from "./utils";
+
+import { z } from "zod";
+import { connectTunnel } from "@restatedev/restate-sdk-tunnel";
+
+const Greeting = z.object({
+ name: z.string().default("World"),
+});
+
+const GreetingResponse = z.object({
+ result: z.string(),
+});
+
+const greeter = restate.service({
+ name: "Greeter",
+ handlers: {
+ greet: restate.createServiceHandler(
+ { input: restate.serde.schema(Greeting), output: restate.serde.schema(GreetingResponse) },
+ async (ctx: restate.Context, { name }) => {
+ // Durably execute a set of steps; resilient against failures
+ const greetingId = ctx.rand.uuidv4();
+ await ctx.run("Notification", () => sendNotification(greetingId, name));
+ await ctx.sleep({ seconds: 1 });
+ await ctx.run("Reminder", () => sendReminder(greetingId, name));
+
+ // Respond to caller
+ return { result: `You said hi to ${name}!` };
+ },
+ ),
+ },
+});
+
+connectTunnel({ services: [greeter] });
diff --git a/typescript/templates/node-kubernetes/src/utils.ts b/typescript/templates/node-kubernetes/src/utils.ts
new file mode 100644
index 00000000..bb5e7599
--- /dev/null
+++ b/typescript/templates/node-kubernetes/src/utils.ts
@@ -0,0 +1,20 @@
+// You can remove this file.
+// It's only purpose is providing stubs for the template.
+
+export function sendNotification(greetingId: string, name: string) {
+ if (Math.random() < 0.7 && name == "Alice") {
+ // 70% chance of failure
+ console.error(`[👻 SIMULATED] Failed to send notification: ${greetingId} - ${name}`);
+ throw new Error(`[👻 SIMULATED] Failed to send notification ${greetingId} - ${name}`);
+ }
+ console.log(`Notification sent: ${greetingId} - ${name}`);
+}
+
+export function sendReminder(greetingId: string, name: string) {
+ if (Math.random() < 0.7 && name == "Alice") {
+ // 70% chance of failure
+ console.error(`[👻 SIMULATED] Failed to send reminder: ${greetingId}`);
+ throw new Error(`[👻 SIMULATED] Failed to send reminder: ${greetingId}`);
+ }
+ console.log(`Reminder sent: ${greetingId}`);
+}
diff --git a/typescript/templates/node-kubernetes/tsconfig.json b/typescript/templates/node-kubernetes/tsconfig.json
new file mode 100644
index 00000000..f23f9bf1
--- /dev/null
+++ b/typescript/templates/node-kubernetes/tsconfig.json
@@ -0,0 +1,110 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig to read more about this file */
+
+ /* Projects */
+ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
+ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
+ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
+ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
+ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
+ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
+
+ /* Language and Environment */
+ "target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+ "lib": ["esnext"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+ // "jsx": "preserve", /* Specify what JSX code is generated. */
+ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
+ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
+ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
+ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
+ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
+ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
+
+ /* Modules */
+ "module": "nodenext", /* Specify what module code is generated. */
+ // "rootDir": "./", /* Specify the root folder within your source files. */
+ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
+ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
+ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
+ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
+ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
+ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
+ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
+ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
+ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
+ // "resolveJsonModule": true, /* Enable importing .json files. */
+ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
+ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
+
+ /* JavaScript Support */
+ "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
+ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+
+ /* Emit */
+ "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+ "declarationMap": true, /* Create sourcemaps for d.ts files. */
+ "sourceMap": true, /* Create source map files for emitted JavaScript files. */
+ "outDir": "./dist", /* Specify an output folder for all emitted files. */
+ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
+ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
+ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+ // "removeComments": true, /* Disable emitting comments. */
+ // "noEmit": true, /* Disable emitting files from a compilation. */
+ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
+ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
+ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+ // "newLine": "crlf", /* Set the newline character for emitting files. */
+ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
+ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
+ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
+ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
+ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
+
+ /* Interop Constraints */
+ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
+ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
+ "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
+ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
+ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
+ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+
+ /* Type Checking */
+ "strict": true, /* Enable all strict type-checking options. */
+ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
+ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
+ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
+ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
+ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
+ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
+ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
+ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
+ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
+ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
+ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
+ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
+ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
+ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
+ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
+
+ /* Completeness */
+ "skipLibCheck": true, /* Skip type checking all .d.ts files. */
+ "skipDefaultLibCheck": true /* Skip type checking .d.ts files that are included with TypeScript. */
+ },
+ "include": ["src/"]
+}