---
title: "Go"
description: "A Go process behind the renderer — functions, guards and actions in Go."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.rsc-kit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Go

Go owns the request: sessions, auth, the database. The renderer owns
rendering, because that half is React. A server component reaches Go by
calling `rpc()`, which leaves the renderer as an ordinary POST; a `middleware.ts`
names guards Go runs before a page renders; a server action the browser calls
is a Go function the build wrote a stub for.

A [BAP](/hosts/backend-answered-pages) with Go behind it. There is no
JavaScript to write for Go: the app is what the scaffold writes, and the Go
side is one package answering
[the endpoint every backend answers](/hosts/your-own-backend).

```sh
go get github.com/rsc-kit/go
```

## Wiring

In a Go module, `init` sees `go.mod` and does the JavaScript half — the
project's first `package.json` if it has none, the route tree,
`vite.config.ts`, `.env` with `RSC_BACKEND` and a generated secret, the scripts
— and prints the Go half for you to add. Nothing in the Go module is touched:

```sh
bunx rsc-kit@latest init            # in the directory with go.mod
bun create rsc-kit@latest my-app --backend=http://127.0.0.1:8080   # or a new app
```

What it wrote is two lines, read by `vite` in development and by the built
server in production:

```ini title=".env"
RSC_BACKEND=http://127.0.0.1:8080
RSC_HOST_CALL_SECRET=a-long-random-string
```

And the endpoint, on whatever mux you already have:

```go title="main.go"
reg := rsckit.NewRegistry()
// … Register, Middleware, RegisterAction below …

callback, err := rsckit.NewCallbackHandler(reg, os.Getenv("RSC_HOST_CALL_SECRET"))
if err != nil {
log.Fatal(err) // refuses to exist without a secret
}

mux := http.NewServeMux()
mux.Handle("POST /__rsc/host-call", callback)
http.ListenAndServe("127.0.0.1:8080", mux)
```

The renderer forwards any url its route tree does not own to `RSC_BACKEND`,
so a Go route on that mux — `/login`, a webhook, an upload — is reachable at
the renderer's origin too.

### Running it locally

Two processes: the Go program, and Vite, which is the renderer in
development. Run Go with `RSC_DEBUG=1`, so a failure says where in Go it
happened:

```sh
RSC_DEBUG=1 go run .     # the backend, on 127.0.0.1:8080
bun run dev              # the renderer; open its address
```

Never set `RSC_DEBUG` in production: a trace names your files.

### Environment variables

| Variable | Read by | What it is |
| --- | --- | --- |
| `RSC_BACKEND` | the renderer | Where the Go endpoint is, e.g. `http://127.0.0.1:8080`. Falls back to `APP_URL`. Read from `.env` by `vite`, and from the environment by the built server; the url forwarding is fixed at build time. |
| `RSC_HOST_CALL_SECRET` | both | The shared secret. The renderer sends it; your Go code passes it to `NewCallbackHandler`. Without it neither side talks. |
| `RSC_HOST_CALL_PATH` | the renderer | The endpoint's path. Default `/__rsc/host-call`. |
| `RSC_DEBUG` | Go | `1` sends a failure's trace with its answer. Development only. |
| `RSC_TRUST_FORWARDED` | the renderer | `1` passes the visitor's `X-Forwarded-For` through to Go. Only behind a proxy you control that writes it. |

The Go adapter reads only `RSC_DEBUG` itself. The secret reaches it through
your code, as above.

## Reading from Go

Register an ordinary Go function with `Handle`, and its types reach the app:

```go
reg.Handle("Orders.recent", func(ctx context.Context, limit int) ([]Order, error) {
// The visitor's cookie, forwarded from the page request: this runs as
// them, not as nobody.
return db.RecentOrders(ctx, rsckit.HeadersFrom(ctx).Get("Cookie"), limit)
})
```

```tsx title="src/app/orders/page.tsx"
const orders = await rpc('Orders.recent', 5)   // Order[]; rpc('Orders.recent', 'five') fails the typecheck
```

- **Any parameters, in order.** `rpc()`'s arguments are bound to the
  function's parameters positionally, whatever JSON can carry: numbers,
  strings, structs, slices, maps. A leading `context.Context` is the call's.
- **Optional and variadic.** A trailing pointer parameter may be left out
  and is `nil`; a variadic one takes the rest.
- **What it returns.** A value and an error, only an error, or only a value.
- **Types for the app.** The parameter and result types are written to
  `rsc-host.json` as JSON Schema. The build turns each struct into a
  TypeScript interface in the `RscHost` namespace, by its `json` tags:
  `omitempty` is optional, a pointer is `| null`, `time.Time` is a string.
  A type with its own `MarshalJSON` is `unknown`, since its shape is its own.
- **Empty, not null.** A nil slice or map in the result is sent as `[]` or
  `{}`, as its type says; only a nil pointer is `null`.

`Register` still takes the untyped form, binding by hand:

```go
reg.Register("Orders.recent", func(ctx context.Context, args rsckit.Args) (any, error) {
var limit int
if err := args.Bind(&limit); err != nil {
    return nil, err
}

// The visitor's cookie, forwarded from the page request: this runs as
// them, not as nobody.
session := rsckit.HeadersFrom(ctx).Get("Cookie")

return db.RecentOrders(ctx, session, limit)
})
```

```tsx title="src/app/orders/page.tsx"
export default async function Orders() {
  const orders = await rpc<Order[]>('Orders.recent', 5)

  return <ul>{orders.map((o) => <li key={o.id}>{o.number}</li>)}</ul>
}
```

`args.Bind` decodes the positional arguments `rpc()` was given; too few is an
error, extra ones are ignored. Whatever the function returns is encoded as
JSON and decoded on the other side as whatever `rpc<T>()` was told: an
untyped function is `unknown` until you say.

### When it fails

In development, set `Debug` on the callback handler, or `RSC_DEBUG=1` in the
environment, and a failure says where in Go it happened: a panic sends the
frames it unwound, an error the chain it wraps. The renderer shows them under
its own stack, as the error's cause:

```text
Error: Host call "Orders.recent" failed: host function panicked: runtime error: slice bounds out of range
Caused in the backend by Backend panic(runtime.boundsError): …
at backend/main.go:64 main.main.func3
```

Never in production: a trace names your files.

### Refusing

Return a value and it is the result. Return one of these and the render is
told what happened, rather than handed a 500:

| return | the render gets |
| --- | --- |
| `rsckit.InvalidField("name", "…")`, `rsckit.Invalid(map)` | 422, each message under its input on the form that submitted |
| `rsckit.Unauthenticated()` | 401, the engine's own authentication error |
| `rsckit.Unauthorized("…")` | 403 |
| `rsckit.Redirect("/login")` | the browser goes there |
| `rsckit.Refuse(429, "slow down")` | that status, kept |
| any other `error` | 500, with the message |

Wrapped errors still answer as what they are — `errors.As` finds the refusal
inside `fmt.Errorf("…: %w", err)` — and a panic becomes an error for that one
call rather than taking the server down.

## Route middleware

```ts title="src/app/admin/middleware.ts"
export const middleware = ['auth', 'can:manage-orders', 'throttle:60,1']
```

```go
reg.Middleware("auth", func(ctx context.Context, _ string) error {
if !signedIn(rsckit.HeadersFrom(ctx)) {
    return rsckit.Redirect("/login")
}
return nil
})

reg.Middleware("can", func(ctx context.Context, ability string) error {
if !allowed(ctx, ability) {
    return rsckit.Unauthorized("you may not " + ability)
}
return nil
})
```

The renderer sends the list before anything at or below `admin/` renders —
including a page it froze at build time, before the file is served. A guard
receives what follows the colon in its name (`"manage-orders"`, `"60,1"`,
`""`); guards run in order and stop at the first refusal. A name nothing is
registered for refuses rather than passes: a declared check that silently
does not happen is the failure this exists to prevent.

## Server actions

```go
reg.HandleAction("ordersCancel", "Orders.cancel", func(ctx context.Context, id int) error {
if err := orders.Cancel(ctx, id); err != nil {
    return err
}

rsckit.Revalidate(ctx, "orders")

return nil
})

// The actions and every function, for the build: stubs, and rpc()'s type.
reg.WriteManifest("rsc-host.json")
```

```tsx title="src/app/orders/CancelButton.tsx"
'use client'
import { ordersCancel } from '../../server-actions.generated'

export function CancelButton({ id }: { id: number }) {
  return <button onClick={() => ordersCancel(id)}>Cancel</button>
}
```

The stub is typed from the Go function: `ordersCancel(id: number): Promise<void>`.
A form can post to an action directly, `<Form action={ordersCreate}>`, and its
fields arrive as the first parameter: a struct, with numbers and booleans
coerced by its types and a repeated field a slice.

`Revalidate` names a [section](/guides/sections) or `page`, and the answer to
the action carries it re-rendered rather than the browser being told to ask
again.

Have Vite write the manifest, so it cannot go stale: give the program a flag
that writes it and exits, and name the command in the config. It runs as dev
and every build start, and a build fails if it does:

```ts title="vite.config.ts"
rscKit({ hostManifest: { command: ['go', 'run', '.', '-manifest', '../rsc-host.json'], cwd: 'backend' } })
```

The manifest also lists every registered function, so `rpc()` is typed by
the names Go holds and `rpc('Orders.recnet')` fails the typecheck. Tests
answer these functions without Go running:
[Testing it](/hosts/backend-answered-pages#testing-it).

## Saying data changed

An action can say what it changed. A webhook, a job or another visitor's
action cannot reach the tab that is showing the data - so a section names
what it depends on, and Go says when that changed, from anywhere with a
context:

```tsx title="src/app/t/[team]/repos.section.tsx"
export default section('repos', Repos, { refreshOn: ({ params }) => [`team:${params.team}:repos`] })
```

```go
// in the GitHub webhook handler
reg.Changed(ctx, "team:"+teamID+":repos")
```

Every open tab showing the section refreshes it, the moment the webhook
lands, with nothing polling. The registry answers `__rsc.changed` by holding
the renderer's ask until a version moves - woken at once by a `Changed` in
this process, checking the store every `rsckit.TagsPoll` (1s) for one made
by another instance.

Versions live in a `TagStore`. The default is in memory, which is one
instance; with more than one, a webhook lands on whichever instance the
balancer picked, so give them all the same store:

```go
// CREATE TABLE rsc_tags (name TEXT PRIMARY KEY, version BIGINT NOT NULL)
reg.Names(&rsckit.SQLTags{DB: db, Placeholder: rsckit.Dollar})   // Postgres; nil Placeholder is "?"
```

Anything with a `Bump` and a `Versions` fits. [Sections](/guides/sections#refreshing-on-a-change-from-outside)
has the whole picture.

## Production

`bun run build` and `bun run start` with the same two variables in the
renderer's environment. Put the renderer in front and restrict
`/__rsc/host-call` at the web server, as [Laravel's page](/hosts/laravel/#which-process-faces-the-internet)
shows; a Go worker is held for the length of a host call, never a render.

Or put Go in front: `rsckit.NewRenderer(url)` is a streaming reverse proxy
to the renderer, and `rsckit.NewHandler(renderer, callback, path)` routes
the callback path to the endpoint and everything else through it. Both sides
mark what they forward, so a url neither owns is a 404 rather than a loop.

On a Worker, the two variables are a var and a secret in `wrangler.json`,
and the Go process has to be reachable from Cloudflare's network — an
`https` origin behind the secret and, ideally, an allow-list, since loopback
is not an option there.

---

The runnable version is
[`examples/go-backend`](https://github.com/rsc-kit/rsc-kit/tree/main/examples/go-backend)
in the repository: `bun run backend`, `bun run dev`, open the renderer.

Source: https://docs.rsc-kit.dev/hosts/go/index.mdx
