Skip to main content

Project Setup

This guide walks you through creating a new Go project that uses Gate from scratch. If you prefer a pre-configured setup, see the Plugin Template instead.

Prerequisites

Before you begin, ensure you have:
  • Go 1.21 or higher installed (download here)
  • Basic Go knowledge (modules, packages, functions)
  • A text editor (VS Code, GoLand, Vim, etc.)
  • Terminal access for running commands
Verify your Go installation:
go version
# Should output: go version go1.21 or higher

Creating a New Project

1

Create Project Directory

Create a new directory for your project:
mkdir my-gate-proxy
cd my-gate-proxy
2

Initialize Go Module

Initialize a new Go module:
go mod init github.com/yourusername/my-gate-proxy
Replace github.com/yourusername/my-gate-proxy with your actual module path.
3

Add Gate Dependency

Add Gate as a dependency:
go get -u go.minekube.com/gate@latest
This will download Gate and update your go.mod file.
4

Create Main File

Create a main.go file with the following content:
package main

import (
    "context"
    "go.minekube.com/gate/cmd/gate"
    "go.minekube.com/gate/pkg/edition/java/proxy"
)

func main() {
    // Add our plugin to be initialized on Gate start
    proxy.Plugins = append(proxy.Plugins, proxy.Plugin{
        Name: "MyProxy",
        Init: func(ctx context.Context, p *proxy.Proxy) error {
            return initPlugin(p)
        },
    })

    // Execute Gate entrypoint and block until shutdown
    gate.Execute()
}

func initPlugin(p *proxy.Proxy) error {
    // Your plugin initialization code goes here
    return nil
}
5

Create Configuration File

Create a config.yml file for Gate’s configuration:
config:
  bind: 0.0.0.0:25565
  motd: "&bMy Gate Proxy"
  
  servers:
    lobby:
      address: localhost:25566
    survival:
      address: localhost:25567
  
  try:
    - lobby
This configures Gate to:
  • Listen on port 25565
  • Connect players to the “lobby” server first
  • Fall back to other servers if lobby is unavailable
6

Download Dependencies

Download all module dependencies:
go mod download
go mod tidy
7

Build and Run

Build and run your proxy:
go run .
You should see output indicating Gate has started:
INFO  Gate is now running on 0.0.0.0:25565

Project Structure

A typical Gate project structure:
my-gate-proxy/
├── main.go              # Entry point
├── plugin.go            # Plugin logic (optional)
├── commands.go          # Command definitions (optional)
├── events.go            # Event handlers (optional)
├── go.mod               # Module definition
├── go.sum               # Dependency checksums
├── config.yml           # Gate configuration
└── README.md            # Documentation

Understanding go.mod

After running go get, your go.mod should look like:
module github.com/yourusername/my-gate-proxy

go 1.21

require go.minekube.com/gate v0.62.3

require (
    connectrpc.com/connect v1.19.1 // indirect
    github.com/go-logr/logr v1.4.3 // indirect
    github.com/robinbraemer/event v0.1.1 // indirect
    github.com/spf13/viper v1.21.0 // indirect
    go.minekube.com/brigodier v0.0.2 // indirect
    go.minekube.com/common v0.3.0 // indirect
    // ... other indirect dependencies
)
Key dependencies you’ll commonly use:
  • go.minekube.com/gate - Core proxy functionality
  • go.minekube.com/brigodier - Command framework
  • go.minekube.com/common - Minecraft components (chat, colors)
  • github.com/robinbraemer/event - Event system

Adding Common Features

Register a Command

Create commands.go:
package main

import (
    "fmt"
    "go.minekube.com/brigodier"
    "go.minekube.com/common/minecraft/component"
    "go.minekube.com/gate/pkg/command"
    "go.minekube.com/gate/pkg/edition/java/proxy"
)

func registerCommands(p *proxy.Proxy) {
    // Register /ping command
    p.Command().Register(
        brigodier.Literal("ping").Executes(
            command.Command(func(c *command.Context) error {
                player, ok := c.Source.(proxy.Player)
                if !ok {
                    return c.Source.SendMessage(
                        &component.Text{Content: "Pong!"},
                    )
                }
                
                return player.SendMessage(&component.Text{
                    Content: fmt.Sprintf("Pong! Your ping is %s", player.Ping()),
                })
            }),
        ),
    )
}
Update main.go to call it:
func initPlugin(p *proxy.Proxy) error {
    registerCommands(p)
    return nil
}

Handle Events

Create events.go:
package main

import (
    "github.com/robinbraemer/event"
    "go.minekube.com/common/minecraft/color"
    "go.minekube.com/common/minecraft/component"
    "go.minekube.com/gate/pkg/edition/java/proxy"
)

func subscribeToEvents(p *proxy.Proxy) {
    // Listen for player login
    event.Subscribe(p.Event(), 0, onPlayerLogin)
    
    // Listen for server switch
    event.Subscribe(p.Event(), 0, onServerSwitch)
}

func onPlayerLogin(e *proxy.PostLoginEvent) {
    player := e.Player()
    player.SendMessage(&component.Text{
        Content: "Welcome to the server!",
        S:       component.Style{Color: color.Green},
    })
}

func onServerSwitch(e *proxy.ServerPostConnectEvent) {
    player := e.Player()
    if server := player.CurrentServer(); server != nil {
        player.SendMessage(&component.Text{
            Content: "You are now on: " + server.Server().ServerInfo().Name(),
            S:       component.Style{Color: color.Yellow},
        })
    }
}
Update main.go:
func initPlugin(p *proxy.Proxy) error {
    registerCommands(p)
    subscribeToEvents(p)
    return nil
}

Working with Components

Gate uses a component system for rich text:
import (
    "go.minekube.com/common/minecraft/color"
    "go.minekube.com/common/minecraft/component"
)

// Simple text
msg := &component.Text{
    Content: "Hello, world!",
}

// Colored text
msg := &component.Text{
    Content: "This is red!",
    S:       component.Style{Color: color.Red},
}

// Bold and italic
msg := &component.Text{
    Content: "Bold and italic",
    S: component.Style{
        Color:  color.Gold,
        Bold:   component.True,
        Italic: component.True,
    },
}

// Clickable text
msg := &component.Text{
    Content: "Click me!",
    S: component.Style{
        ClickEvent: component.SuggestCommand("/ping"),
        HoverEvent: component.ShowText(&component.Text{
            Content: "Click to run /ping",
        }),
    },
}

// Compound message
msg := &component.Text{
    Content: "Welcome ",
    Extra: []component.Component{
        &component.Text{
            Content: player.Username(),
            S:       component.Style{Color: color.Yellow},
        },
        &component.Text{Content: "!"},
    },
}

Complete Example

Here’s a complete main.go with commands and events:
package main

import (
    "context"
    "fmt"

    "github.com/robinbraemer/event"
    "go.minekube.com/brigodier"
    "go.minekube.com/common/minecraft/color"
    "go.minekube.com/common/minecraft/component"
    "go.minekube.com/gate/cmd/gate"
    "go.minekube.com/gate/pkg/command"
    "go.minekube.com/gate/pkg/edition/java/proxy"
)

func main() {
    proxy.Plugins = append(proxy.Plugins, proxy.Plugin{
        Name: "MyProxy",
        Init: func(ctx context.Context, p *proxy.Proxy) error {
            return initPlugin(p)
        },
    })
    gate.Execute()
}

func initPlugin(p *proxy.Proxy) error {
    registerCommands(p)
    subscribeToEvents(p)
    return nil
}

func registerCommands(p *proxy.Proxy) {
    // /ping command
    p.Command().Register(
        brigodier.Literal("ping").Executes(
            command.Command(func(c *command.Context) error {
                player, ok := c.Source.(proxy.Player)
                if !ok {
                    return c.Source.SendMessage(
                        &component.Text{Content: "Pong!"},
                    )
                }
                return player.SendMessage(&component.Text{
                    Content: fmt.Sprintf("Pong! Latency: %s", player.Ping()),
                    S:       component.Style{Color: color.Green},
                })
            }),
        ),
    )

    // /broadcast <message> command
    p.Command().Register(
        brigodier.Literal("broadcast").Then(
            brigodier.Argument("message", brigodier.StringPhrase).Executes(
                command.Command(func(c *command.Context) error {
                    message := c.String("message")
                    msg := &component.Text{
                        Content: "[Broadcast] " + message,
                        S:       component.Style{Color: color.Gold},
                    }
                    for _, player := range p.Players() {
                        _ = player.SendMessage(msg)
                    }
                    return nil
                }),
            ),
        ),
    )
}

func subscribeToEvents(p *proxy.Proxy) {
    event.Subscribe(p.Event(), 0, onPlayerLogin)
    event.Subscribe(p.Event(), 0, onServerSwitch)
}

func onPlayerLogin(e *proxy.PostLoginEvent) {
    player := e.Player()
    _ = player.SendMessage(&component.Text{
        Content: fmt.Sprintf("Welcome, %s!", player.Username()),
        S:       component.Style{Color: color.Green, Bold: component.True},
    })
}

func onServerSwitch(e *proxy.ServerPostConnectEvent) {
    player := e.Player()
    if server := player.CurrentServer(); server != nil {
        _ = player.SendMessage(&component.Text{
            Content: "Connected to " + server.Server().ServerInfo().Name(),
            S:       component.Style{Color: color.Aqua},
        })
    }
}

Building Your Project

Development Build

For quick testing:
go run .

Production Build

Create an optimized binary:
# Basic build
go build -o my-proxy

# Optimized build (smaller binary)
CGO_ENABLED=0 go build -ldflags="-s -w" -o my-proxy

# Run the binary
./my-proxy

Cross-Compilation

Build for different platforms:
# Linux (most common for servers)
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o my-proxy-linux

# Windows
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o my-proxy.exe

# macOS (Intel)
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -o my-proxy-macos-intel

# macOS (Apple Silicon)
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build -o my-proxy-macos-arm

Configuration Best Practices

Separate Environments

Create different config files for different environments:
config.dev.yml    # Development
config.prod.yml   # Production
config.test.yml   # Testing
Run with a specific config:
./my-proxy --config config.prod.yml

Environment Variables

Use environment variables for sensitive data:
config:
  forwarding:
    mode: modern
    # Secret loaded from environment variable
export GATE_VELOCITY_SECRET="your-secret-here"
./my-proxy

Validation

Gate validates your configuration on startup. Common issues:
# ❌ Invalid: missing address
servers:
  lobby: {}

# ✅ Valid: includes address
servers:
  lobby:
    address: localhost:25566

Troubleshooting

Error: go: module go.minekube.com/gate: Get "https://proxy.golang.org/..." failedSolutions:
  1. Check your internet connection
  2. Verify firewall isn’t blocking Go module proxy
  3. Try setting GOPROXY environment variable:
    export GOPROXY=https://proxy.golang.org,direct
    go get -u go.minekube.com/gate@latest
    
Error: import cycle not allowedSolution: Reorganize your code to avoid circular imports. For example:
// ❌ Bad: main.go imports plugin.go, plugin.go imports main.go

// ✅ Good: Use separate packages or interfaces
package main

package plugin
Error: bind: address already in useSolutions:
  1. Find and stop the process using the port:
    # Linux/macOS
    lsof -i :25565
    kill <PID>
    
    # Windows
    netstat -ano | findstr :25565
    taskkill /PID <PID> /F
    
  2. Or change the port in config.yml:
    config:
      bind: 0.0.0.0:25566
    
Error: config file not foundSolution: Ensure config.yml is in the same directory as your binary:
ls -la config.yml
# If missing, create it from the template above
Or specify the path explicitly:
./my-proxy --config /path/to/config.yml
Error: Players can’t connect to backend serversSolutions:
  1. Verify backend servers are running:
    telnet localhost 25566
    
  2. Check firewall rules allow connections
  3. Verify addresses in config.yml are correct
  4. Check backend server logs for connection attempts

Testing Your Proxy

Manual Testing

  1. Start your proxy:
    go run .
    
  2. Start a backend server (e.g., Paper, Spigot) on port 25566
  3. Connect with Minecraft client to localhost:25565

Unit Testing

Create main_test.go:
package main

import (
    "testing"
)

func TestPluginInit(t *testing.T) {
    // Test your plugin initialization
}
Run tests:
go test -v

Performance Tips

Use Goroutines for Async Operations

// ❌ Bad: Blocks the event handler
func onPlayerJoin(e *proxy.PostLoginEvent) {
    time.Sleep(5 * time.Second) // Blocks!
    e.Player().SendMessage(msg)
}

// ✅ Good: Runs asynchronously
func onPlayerJoin(e *proxy.PostLoginEvent) {
    player := e.Player()
    go func() {
        time.Sleep(5 * time.Second)
        player.SendMessage(msg)
    }()
}

Batch Player Operations

// Send message to all players efficiently
for _, player := range p.Players() {
    go func(p proxy.Player) {
        _ = p.SendMessage(msg)
    }(player)
}

Resource Cleanup

Use context cancellation:
func initPlugin(p *proxy.Proxy) error {
    ctx := context.Background()
    
    // Start background task
    go periodicTask(ctx)
    
    // Cleanup on shutdown
    event.Subscribe(p.Event(), 0, func(e *proxy.ShutdownEvent) {
        // Cancel context, close connections, etc.
    })
    
    return nil
}

Next Steps

Commands Guide

Learn how to create powerful commands

Events Guide

Master the event system

Simple Proxy Example

Study a complete working example

API Reference

Explore the full API documentation

Additional Resources

Build docs developers (and LLMs) love