# arikawa: a Go Discord library that separates the API, the gateway and the state

> arikawa is an ISC licensed Go library and framework for the Discord API, split into independent packages for the REST client, the websocket gateway and a stateful bot layer, with typed Snowflakes and a pluggable gateway cache. Its examples are honest about which of them Discord actually wants you to run.

**diamondburned/arikawa** — A Golang library and framework for the Discord API.

- Repository: https://github.com/diamondburned/arikawa
- Stars: 603 · Forks: 65
- Language: Go
- License: ISC
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/diamondburned-arikawa

## Three packages that do not need each other

arikawa is a Go library for the Discord API, and its main design decision is that the pieces are separable. The repository layout shows it plainly: an `api` package for the HTTP side, a `gateway` package for the websocket side, a `state` package for the stateful bot layer that sits on top of both, plus `session`, `utils`, `voice` and `discord` directories and a set of numbered examples under `0-examples/`.

The library highlights call out modularity first, with components divided into independent packages, and specifically the API client and the Websocket Gateway being fully independent. That means a program that only calls the REST API does not pay for gateway code, and a program that wants raw gateway events can use them without the state layer's caching semantics getting in the way.

The second highlight is a clear separation of models. API models and Gateway models are never mixed together, so a struct you receive from a REST call is not confused with an event payload. Discord's two sides do not agree on shape, and libraries that merge them leave you guessing which fields are guaranteed.

The third is that you can extend and intercept gateway events, which the documentation gives an example use case for: reading deleted messages. That capability is the heart of one of the shipped examples, and it is the reason the gateway package is useful on its own rather than only as an implementation detail of the state package.

## Typed Snowflakes make ID mix-ups a compile error

Discord identifies everything with snowflakes, a single 64-bit integer format used for users, channels, guilds and messages. Passing one of those around as a bare integer is the classic source of bugs in a Discord bot, and arikawa addresses it with typed Snowflakes. The stated effect is that it is much harder to accidentally use the wrong ID, and that it is impossible to use a channel ID as a message ID.

That is a compile time property rather than a runtime check, which is the whole point. A bot that handles moderation commands, where a channel identifier and a user identifier end up in adjacent variables, will simply fail to build when the types are mixed, rather than send a malformed request at three in the morning.

The same thinking shows up in the payload types. The bare minimum example builds an interaction response with `option.NewNullableString("Pong!")` rather than a plain string, which comes from the `utils/json/option` package and reflects Discord's own convention of distinguishing a field that is absent from one that is set to an empty value. A JSON payload where `null` and `""` mean different things cannot be modelled by Go's zero values alone.

Taken together, these are small decisions with a large effect on how much of a Discord bot you can write without a schema in your head.

## The gateway cache is pluggable and can fall back to the API

The fourth highlight is a pluggable Gateway cache, where custom caching implementations such as Redis can be plugged in, and where the library automatically falls back to the API if needed. That fallback is the interesting part, because a cache miss in a bot does not have to become a failed command.

The alternative to this design is a state layer with a fixed in-process cache, where every cache miss is an HTTP round trip and every restart is a cold cache. With the pluggable approach, a bot that runs on more than one instance can put Redis in front of the cache so that all instances see the same state, and a single instance bot can leave the default alone.

The dependency list in the module file gives a hint about how light this is. Beyond the standard library, the module requires `github.com/gorilla/schema`, `github.com/gorilla/websocket`, `golang.org/x/crypto` and `golang.org/x/time`. There is no Redis client in the required set, which tells you the cache implementation is something you bring rather than something the library ships.

The `state` package is the layer where this matters most, since it is the layer that decides what is cached and when it is refreshed. The examples give a hint of the scale involved: the undeleter example caches everything locally, including messages, in order to be able to notice when a message disappears.

## PreHandler is how the undeleter sees a message before it is gone

The undeleter example is the one that explains the state library's design better than any description could. The bot keeps a local state that caches everything, messages included, and it detects when someone deletes a message, logging the content into the console.

The mechanism is the PreHandler feature. PreHandler calls all handlers that are registered, separately from the session, before the state is updated. That ordering is the whole trick: when a message delete event arrives, the handler still sees the message content that was cached before the deletion was applied, so the text can be printed before it disappears from the cache.

An undeleter is also the clearest example of a capability with limits. Catching deletions requires the bot to be a member of the guild and to have received the events in the first place, and the cache only holds what the bot saw. The example is presented as a demonstration of the PreHandler feature rather than as a recommended use case, and the project also notes that Discord discourages bots that do not use the interactions API, which applies to this example and to the simple example.

The commands-hybrid example shows the direction the project would rather you go. It is a variant of the commands example where the program permits being hosted either as a Gateway-based daemon or as a web server using the Interactions Webhook API, and both examples demonstrate adding interaction commands into the bot as well as routing those commands to be executed.

## The ping bot shows the router, the state and the intents

The bare minimum example is described as the least amount of code recommended to have a bot that responds to a /ping. Its imports show the layered structure directly:

```go
package main

import (
	"context"
	"log"
	"os"

	"github.com/diamondburned/arikawa/v3/api"
	"github.com/diamondburned/arikawa/v3/api/cmdroute"
	"github.com/diamondburned/arikawa/v3/gateway"
	"github.com/diamondburned/arikawa/v3/state"
	"github.com/diamondburned/arikawa/v3/utils/json/option"
)
```

The command itself is declared as data, a `CreateCommandData` slice with the name and description, and the handler is a router function:

```go
var commands = []api.CreateCommandData{{Name: "ping", Description: "Ping!"}}

func main() {
	r := cmdroute.NewRouter()
	r.AddFunc("ping", func(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
		return &api.InteractionResponseData{Content: option.NewNullableString("Pong!")}
	})
```

The rest wires the router into a state, declares which gateway intents to subscribe to, registers the commands with Discord and connects:

```go
	s := state.New("Bot " + os.Getenv("BOT_TOKEN"))
	s.AddInteractionHandler(r)
	s.AddIntents(gateway.IntentGuilds)

	if err := cmdroute.OverwriteCommands(s, commands); err != nil {
		log.Fatalln("cannot update commands:", err)
	}

	if err := s.Connect(context.TODO()); err != nil {
		log.Println("cannot connect:", err)
	}
}
```

Two details are worth copying exactly. The token comes from the `BOT_TOKEN` environment variable, and `OverwriteCommands` pushes the declared commands to Discord at startup, which is why the example can respond to a slash command without any registration step outside the program. The intents are declared explicitly with `AddIntents`, which is the mechanism that keeps a bot from receiving events it does not need.

The simpler `simple` example is run with `BOT_TOKEN="TOKEN" go run .`, but the project warns that Discord discourages bots that do not use the interactions API, so that example should not be used for bots.

## Integration tests need a real bot token

The test suite includes integration tests that require a bot token in the environment, and the documented invocation is short:

```sh
export BOT_TOKEN="<BOT_TOKEN>"
go test -tags integration -race ./...
```

The build tag matters, because the integration tests are excluded from a normal `go test` run and only execute when the tag is present. The race detector is switched on as well, which makes sense for a library that hands out websocket connections and shared state.

The module itself targets a recent Go, with the module path `github.com/diamondburned/arikawa/v3` and a `go 1.25.0` directive, and the repository ships a `go.work` file and a `flake.nix`, so a workspace and a Nix development environment are both supported paths.

On licence, the project is ISC, a short permissive licence with no copyleft obligation, which makes it straightforward to include in a commercial bot. On maintenance, the repository is not archived and the last push was on 2026-05-18, while the newest tagged release is v3.6.0 from 2025-09-04, ahead of v3.5.0 from 2025-05-03 and v3.4.0 from 2024-08-08. That gap between the last release tag and the last push is worth noting, since Discord's API changes without waiting for library releases, and it is the main thing to check before you build on this.

## Conclusion

arikawa suits Go developers who want a Discord bot or gateway client with typed IDs, interceptable events and a state layer, and who would rather assemble the api, gateway and state packages themselves than adopt a full framework. It does not suit anyone intending to automate a user account, because the project points at ningen for that and asks you not to use it for self-botting, which breaks Discord's terms of service, and it does not suit a team that wants a maintained path through Discord API changes, since the last push was on 2026-05-18 and the newest tagged release is v3.6.0 from 2025-09-04. Before starting, read the undeleter example for the PreHandler pattern and the commands-hybrid example if you want to serve the same commands from a gateway daemon and a webhook.

## FAQ

### What is the difference between the api, gateway and state packages in arikawa?

They are independent layers. The `api` package handles the HTTP side of the Discord API, the `gateway` package handles the websocket side, and the `state` package provides the stateful bot layer on top of both. API and Gateway models are kept separate so the two shapes never get mixed.

### How does arikawa let a bot read deleted messages?

The gateway package lets you extend and intercept gateway events, and the state library's PreHandler calls registered handlers before the state is updated. The undeleter example caches messages locally so that the content is still available when a delete event arrives.

### Can arikawa use a Redis cache for the gateway state?

The gateway cache is pluggable, so a custom implementation such as Redis can be provided, and the library falls back to the API when it needs data it does not have cached. The required dependencies in go.mod do not include a Redis client, so the implementation is yours to supply.

### Does arikawa support logging in as a user account?

The project states that user account support works, with much of it living in the separate ningen project, and asks that it not be used for self-botting because that is against Discord's terms of service. Bot tokens are the intended path.

### How do I run the arikawa integration tests?

Export a bot token as `BOT_TOKEN` and run `go test -tags integration -race ./...`. The integration tests are behind a build tag, so they are skipped in an ordinary test run.

## Sources

- [diamondburned/arikawa on GitHub](https://github.com/diamondburned/arikawa)
- [Issues](https://github.com/diamondburned/arikawa/issues)
- [License: ISC](https://github.com/diamondburned/arikawa/blob/v3/LICENSE)
- [README](https://github.com/diamondburned/arikawa/blob/v3/README.md)
- [Releases](https://github.com/diamondburned/arikawa/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/diamondburned-arikawa
