We Built a Full Chat App in a Day — Here's How
+ +How we defined 13 services, built a production-grade chat app, and shipped it as a single binary using Go Micro's modular monolith pattern.
+ Read more → +diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..17aaceb287 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,51 @@ +name: goreleaser + +on: + push: + tags: + - 'v*.*.*' + +permissions: + contents: write + id-token: write + packages: write + attestations: write +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - + name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + - + name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - + name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + - + name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v7 + with: + distribution: goreleaser + version: '~> v2' + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000000..05eaf0e756 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,136 @@ +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 2 + +before: + hooks: + - go mod tidy + +builds: + - main: ./cmd/micro + id: micro + binary: micro + env: + - CGO_ENABLED=0 + - >- + {{- if eq .Os "darwin" }} + {{- if eq .Arch "amd64"}}CC=o64-clang{{- end }} + {{- if eq .Arch "arm64"}}CC=aarch64-apple-darwin20.2-clang{{- end }} + {{- end }} + {{- if eq .Os "windows" }} + {{- if eq .Arch "amd64" }}CC=x86_64-w64-mingw32-gcc{{- end }} + {{- end }} + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm + - arm64 + goarm: + - 7 + ignore: + - goos: windows + goarch: arm + + - main: ./cmd/protoc-gen-micro + id: protoc-gen-micro + binary: protoc-gen-micro + env: + - CGO_ENABLED=0 + - >- + {{- if eq .Os "darwin" }} + {{- if eq .Arch "amd64"}}CC=o64-clang{{- end }} + {{- if eq .Arch "arm64"}}CC=aarch64-apple-darwin20.2-clang{{- end }} + {{- end }} + {{- if eq .Os "windows" }} + {{- if eq .Arch "amd64" }}CC=x86_64-w64-mingw32-gcc{{- end }} + {{- end }} + goos: + - linux + - windows + - darwin + goarch: + - amd64 + - arm + - arm64 + goarm: + - 7 + ignore: + - goos: windows + goarch: arm + +archives: + - id: micro + ids: + - micro + formats: [tar.gz] + name_template: >- + {{ .Binary }}_ + {{- .Os }}_ + {{- .Arch }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + files: + - none* + format_overrides: + - goos: windows + formats: [zip] + + - id: protoc-gen-micro + ids: + - protoc-gen-micro + formats: [tar.gz] + name_template: >- + {{ .Binary }}_ + {{- .Os }}_ + {{- .Arch }} + {{- if .Arm }}v{{ .Arm }}{{ end }} + files: + - none* + format_overrides: + - goos: windows + formats: [zip] + +report_sizes: true + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +dockers_v2: + - + ids: + - micro + - protoc-gen-micro + images: + - "micro/micro" + - "ghcr.io/micro/go-micro" + tags: + - "v{{ .Version }}" + - "{{ if .IsNightly }}nightly{{ end }}" + - "{{ if not .IsNightly }}latest{{ end }}" + labels: + "io.artifacthub.package.readme-url": "https://raw.githubusercontent.com/micro/go-micro/refs/heads/master/README.md" + "io.artifacthub.package.logo-url": "https://www.gravatar.com/avatar/09d1da3ea9ee61753219a19016d6a672?s=120&r=g&d=404" + "org.opencontainers.image.description": "A Go Platform built for Developers" + "org.opencontainers.image.created": "{{.Date}}" + "org.opencontainers.image.title": "{{.ProjectName}}" + "org.opencontainers.image.revision": "{{.FullCommit}}" + "org.opencontainers.image.version": "{{.Version}}" + "org.opencontainers.image.source": "{{.GitURL}}" + "org.opencontainers.image.url": "{{.GitURL}}" + "org.opencontainers.image.licenses": "MIT" + + platforms: + - linux/amd64 + - linux/arm64 + + retry: + attempts: 5 + delay: 5s + max_delay: 2m diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..a2576e4faf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM alpine:latest +ARG TARGETPLATFORM +ENV USER=micro +ENV GROUPNAME=$USER +ARG UID=1001 +ARG GID=1001 +RUN addgroup --gid "$GID" "$GROUPNAME" \ + && adduser \ + --disabled-password \ + --gecos "" \ + --home "/micro" \ + --ingroup "$GROUPNAME" \ + --no-create-home \ + --uid "$UID" "$USER" + +ENV PATH=/usr/local/go/bin:$PATH +RUN apk --no-cache add git make curl +COPY --from=golang:1.26.0-alpine /usr/local/go /usr/local/go + +COPY $TARGETPLATFORM/micro /usr/local/go/bin/ +COPY $TARGETPLATFORM/protoc-gen-micro /usr/local/go/bin/ + +WORKDIR /micro +EXPOSE 8080 +ENTRYPOINT ["/usr/local/go/bin/micro"] +CMD ["server"] diff --git a/Makefile b/Makefile index 4f444703a7..fa5b4a338d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,14 @@ -.PHONY: test test-race test-coverage lint fmt install-tools proto clean help +NAME = micro +GIT_COMMIT = $(shell git rev-parse --short HEAD) +GIT_TAG = $(shell git describe --abbrev=0 --tags --always --match "v*") +GIT_IMPORT = go-micro.dev/v5/cmd/micro +BUILD_DATE = $(shell date +%s) +LDFLAGS = -X $(GIT_IMPORT).BuildDate=$(BUILD_DATE) -X $(GIT_IMPORT).GitCommit=$(GIT_COMMIT) -X $(GIT_IMPORT).GitTag=$(GIT_TAG) + +# GORELEASER_DOCKER_IMAGE = ghcr.io/goreleaser/goreleaser-cross:v1.25.7 +GORELEASER_DOCKER_IMAGE = ghcr.io/goreleaser/goreleaser:latest + +.PHONY: test test-race test-coverage lint fmt install-tools proto clean help gorelease-dry-run gorelease-dry-run-docker # Default target help: @@ -13,6 +23,9 @@ help: @echo " make proto - Generate protobuf code" @echo " make clean - Clean build artifacts" +$(NAME): + CGO_ENABLED=0 go build -ldflags "-s -w ${LDFLAGS}" -o $(NAME) cmd/micro/main.go + # Run tests test: go test -v ./... @@ -56,3 +69,14 @@ clean: find . -name "*.test" -type f -delete go clean -cache -testcache +# Try binary release +gorelease-dry-run: + docker run \ + --rm \ + -e CGO_ENABLED=0 \ + -v $(CURDIR):/$(NAME) \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -w /$(NAME) \ + $(GORELEASER_DOCKER_IMAGE) \ + --clean --verbose --skip=publish,validate --snapshot + diff --git a/examples/mcp/platform/README.md b/examples/mcp/platform/README.md index 6dd70601c7..7fdda5addd 100644 --- a/examples/mcp/platform/README.md +++ b/examples/mcp/platform/README.md @@ -63,10 +63,10 @@ service := micro.New("platform", mcp.WithMCP(":3001"), // This one line makes everything AI-accessible ) -service.Handle(users) -service.Handle(posts) -service.Handle(&CommentService{}) -service.Handle(&MailService{}) +service.Handle(&Users{}) +service.Handle(&Posts{}) +service.Handle(&Comments{}) +service.Handle(&Mail{}) ``` Each handler method becomes an MCP tool. The `@example` tags in doc comments give agents sample inputs to learn from. diff --git a/internal/website/blog/7.md b/internal/website/blog/7.md index f570510f37..143ea9a998 100644 --- a/internal/website/blog/7.md +++ b/internal/website/blog/7.md @@ -13,16 +13,20 @@ Here's the pitch: you have microservices. They already have well-defined endpoin With Go Micro + MCP, that gap is **zero lines of code**. -## The Setup: A Real Blogging Platform +## The Setup: A Blogging Platform -We'll use a microblogging platform built on Go Micro with four services as a demo: +We'll use a blogging platform as our example — inspired by [micro/blog](https://github.com/micro/blog), a real microblogging platform built on Go Micro with four domains: - **Users** — signup, login, profiles - **Posts** — blog posts with markdown, tags, link previews - **Comments** — threaded comments on posts - **Mail** — internal messaging -These services exist today. They were built for human users interacting through a web UI. No one was thinking about AI agents when they were written. +### A Note on Architecture + +Go Micro has always been a framework for building **multi-service, multi-process** systems. The [micro/blog](https://github.com/micro/blog) platform is a great example — each service runs as its own binary, communicates over RPC, and is independently deployable. If that's what you're after, check it out. + +For this walkthrough, we take a different approach: a **modular monolith**. All four domains live in a single process. This is a perfectly valid starting point — you get the clean separation of handler interfaces without the operational overhead of multiple services. And because Go Micro's handler registration works the same way in both models, you can break these out into separate services later as your team or requirements grow. No rewrite needed. ## One Line to Agent-Enable Everything @@ -40,7 +44,7 @@ service.Handle(&Mail{}) That `mcp.WithMCP(":3001")` starts an MCP gateway that: -1. Discovers all registered handlers from the service registry +1. Discovers all registered handlers on the service 2. Converts Go method signatures into JSON tool schemas 3. Extracts descriptions from doc comments 4. Serves it all as MCP-compliant tool definitions @@ -156,21 +160,17 @@ type CreatePostRequest struct { ## Adding MCP to Existing Services -If you already have Go Micro services running (like micro/blog), you have three options: +This demo runs everything in one process, but if you already have Go Micro services running as separate processes (like [micro/blog](https://github.com/micro/blog)), you have two additional options beyond the in-process approach shown above: -### Option 1: One-line in your service -```go -service := micro.New("blog", - mcp.WithMCP(":3001"), // Add this line -) -``` +### Option 1: Standalone gateway binary + +Point a gateway at your service registry and it discovers all running services automatically: -### Option 2: Standalone gateway binary ```bash micro-mcp-gateway --registry consul:8500 --address :3001 ``` -### Option 3: Sidecar in your deployment +### Option 2: Sidecar in your deployment ```yaml # docker-compose.yml services: @@ -184,7 +184,7 @@ services: - "3001:3001" ``` -All three discover services from the same registry. Zero changes to your service code. +Both discover services from the registry and expose them as MCP tools. Zero changes to your service code. ## Production Considerations @@ -220,4 +220,6 @@ The full example is at [`examples/mcp/platform/`](https://github.com/micro/go-mi We're working on a Kubernetes operator that automatically deploys MCP gateways alongside your services, request/response caching to reduce redundant calls from agents, and multi-tenant namespace isolation. See the [roadmap](/docs/roadmap-2026) for details. -The core idea is simple: microservices already have the right structure for AI tools. We just needed to bridge the protocol gap. With MCP, that bridge is one line of code. +The core idea is simple: well-structured services — whether running as a modular monolith or as independently deployed microservices — already have the right shape for AI tools. We just needed to bridge the protocol gap. With MCP, that bridge is one line of code. + +Whether you start with a single process like this demo or go straight to multi-service like [micro/blog](https://github.com/micro/blog), the MCP integration works the same way. diff --git a/internal/website/blog/8.md b/internal/website/blog/8.md new file mode 100644 index 0000000000..99ef90ac4c --- /dev/null +++ b/internal/website/blog/8.md @@ -0,0 +1,191 @@ +--- +layout: blog +title: "We Built a Full Chat App in a Day — Here's How" +permalink: /blog/8 +description: "How we defined 13 services, built a production-grade chat app, and shipped it as a single binary using Go Micro's modular monolith pattern." +--- + +# We Built a Full Chat App in a Day — Here's How + +*March 7, 2026 — By the Go Micro Team* + +We set out to answer a question: how fast can you go from a feature list to a working, production-grade application using Go Micro? The answer surprised us. + +We built **Micro Chat** — a full-featured chat platform with real-time messaging, AI integration, SSO, webhooks, full-text search, file uploads, and more. Thirteen domain services. One binary. One afternoon. + +Here's how we did it, and what it says about Go Micro's role in modern application architecture. + +## The Feature List + +We started with a list. Not a design doc, not a spec — a list of things a real chat app needs: + +- User registration, authentication, profiles, and roles +- Channels and direct messages +- Real-time messaging with WebSockets — typing indicators, read receipts, reactions, edit/delete +- User groups with membership and permissions +- Threaded replies on messages +- Full-text search across all messages +- Invite links with expiration and usage limits +- File uploads and message attachments +- Data export (JSON and CSV) +- Outbound webhooks with event subscriptions and HMAC signing +- AI assistant powered by Claude with tool use and vision +- MCP server exposing tools over JSON-RPC 2.0 +- SSO/OIDC with external identity providers +- Audit logging for admin and security events + +That's a lot. In a traditional microservices setup, you'd spend a week just on the infrastructure — service mesh, message broker, API gateway, deploy pipelines, Kubernetes manifests. We spent zero time on that. + +## One Service Per Domain + +Each feature maps to a service. Each service is a Go package under `service/`: + +``` +service/ +├── agent/ # Claude AI integration +├── audit/ # Audit logging +├── chats/ # Channels, DMs, messages, WebSocket hub +├── export/ # Data export +├── files/ # File uploads +├── groups/ # User groups +├── invites/ # Invite links +├── mcp/ # Model Context Protocol server +├── search/ # Full-text search (FTS5) +├── sso/ # SSO/OIDC +├── threads/ # Threaded replies +├── users/ # Auth, profiles, roles +└── webhooks/ # Outbound webhooks +``` + +Every service follows the same pattern: a struct, a constructor, and methods. No framework magic, no code generation, no annotations. Just Go. + +```go +// service/search/search.go +type Service struct{} + +func NewService() *Service { return &Service{} } + +func (s *Service) Search(filter SearchFilter) ([]SearchResult, int, error) { + // FTS5 query against SQLite +} +``` + +The simplicity is the point. A new team member can read any service top to bottom in five minutes. + +## Go Micro Ties It Together + +Here's where Go Micro earns its keep. Each domain is declared as a `micro.Service`, and they're all composed into a single runnable group: + +```go +gateway := micro.New("gateway", + micro.BeforeStart(func() error { + database.Init() + auth.Init() + searchSvc.InitFTS() + go wsHub.Run() + go httpServer.ListenAndServe() + return nil + }), + micro.AfterStop(func() error { + httpServer.Close() + database.Close() + return nil + }), +) + +usersSvc := micro.New("users") +chatsSvc := micro.New("chats") +groupsSvc := micro.New("groups") +agentSvc := micro.New("agent") +mcpSvc := micro.New("mcp") +searchSvc := micro.New("search") +threadsSvc := micro.New("threads") +webhooksSvc := micro.New("webhooks") +ssoSvc := micro.New("sso") +auditSvc := micro.New("audit") + +g := micro.NewGroup(gateway, usersSvc, chatsSvc, groupsSvc, agentSvc, + mcpSvc, searchSvc, threadsSvc, webhooksSvc, ssoSvc, auditSvc) + +g.Run() +``` + +`micro.NewGroup` handles lifecycle management — ordered startup, signal handling, graceful shutdown. You declare your services, compose them, and run. That's the entire `main.go`. + +The startup banner tells the story: + +``` +Micro Chat - Modular Monolith (go-micro.dev/v5) +───────────────────────────────────────── +Server: http://localhost:8080 +Claude AI: Configured (with tools) +MCP: Enabled +SSO/OIDC: Enabled +───────────────────────────────────────── +``` + +## Why a Modular Monolith? + +We could have built this as 13 separate microservices from the start. We deliberately didn't. Here's why: + +**Velocity.** A single binary means `go build && ./server`. No Docker Compose, no service discovery config, no inter-service networking. We went from zero to a working app in hours, not days. + +**Simplicity.** One database (SQLite), one process, one deploy. You can run this on a $5 VPS or your laptop. The operational overhead is effectively zero. + +**Clean boundaries anyway.** The service packages don't know about each other. `service/webhooks` has no idea `service/search` exists. The API layer composes them, but the domains are fully isolated. We get the architectural benefits of microservices without the infrastructure tax. + +**Cheap iteration.** Want to add audit logging? Create `service/audit`, add a few methods, wire it into the API handler. The cost of a new service is one package and two lines in `main.go`. We added SSO/OIDC support the same way — the pattern is always identical. + +## How It Breaks Out + +This is the real power of the modular monolith: it's not a dead end, it's a starting point. When scale or team structure demands it, the extraction path is clear. + +**Step 1: The interface already exists.** Every service has a clean method-based API. `search.Service.Search(filter)` doesn't change whether it's an in-process call or an RPC endpoint. + +**Step 2: Go Micro makes it native.** Replace the in-process call with a `micro.Client` call. The service moves to its own binary, registers with service discovery, and the caller barely changes. + +**Step 3: Extract incrementally.** Maybe `agent` (the AI service) needs its own deployment because it's making expensive API calls. Pull it out. Everything else stays in the monolith. You don't have to go all-or-nothing. + +**Step 4: The database splits last.** Each service already accesses only its own tables — users has `users`, search has `messages_fts`, SSO has `oidc_providers` and `oidc_users`. When you extract a service, you move its tables to a dedicated database. The code barely changes. + +The progression looks like this: + +``` +Day 1: Modular monolith (single binary, SQLite) +Month 3: Extract agent service (expensive AI calls) +Month 6: Add message broker for webhooks and audit (async events) +Year 1: Split database per service, full microservices where needed +``` + +You grow into microservices. You don't start there. + +## The Stack + +For the curious: + +- **Go Micro v5** — service lifecycle, composition, and the future extraction path +- **SQLite + FTS5** — embedded database with full-text search (swap for Postgres when ready) +- **Gorilla WebSocket** — real-time messaging with typing indicators and read receipts +- **Claude API** — AI agent with tool use, vision, and streaming +- **MCP (JSON-RPC 2.0)** — Model Context Protocol for AI tool integration +- **Go standard library** — `net/http`, `crypto`, `encoding/json` — minimal dependencies + +Total external dependencies: a handful. Total services: 13. Total binaries: 1. + +## What We Learned + +**Define services early, split them late.** Drawing domain boundaries at the start costs nothing. Deploying 13 separate services on day one costs everything. + +**Go Micro's group primitive is underrated.** `micro.NewGroup` is a small API with a big impact. It turns "a bunch of services" into "a managed application" with lifecycle hooks, signal handling, and graceful shutdown. + +**The modular monolith is not a compromise.** It's often the right architecture for most of a product's lifetime. You get the modularity of microservices, the simplicity of a monolith, and a clear path forward when you need to break things apart. + +**AI integration is just another service.** The `agent` service wraps the Claude API. The `mcp` service exposes tools over JSON-RPC. They're not special — they're domain services with the same constructor-and-methods pattern as everything else. That's how it should be. + +## Try It Yourself + +The full source is at [github.com/micro/chat](https://github.com/micro/chat). Clone it, run `go build ./cmd/server && ./server`, and you have a working chat app with 13 services in a single binary. + +Then start thinking about which service you'd extract first — and notice how easy the answer is, because the boundaries are already there. + +That's the modular monolith. That's Go Micro. diff --git a/internal/website/blog/index.html b/internal/website/blog/index.html index 28d5a2caf8..d86f36e146 100644 --- a/internal/website/blog/index.html +++ b/internal/website/blog/index.html @@ -10,6 +10,13 @@
How we defined 13 services, built a production-grade chat app, and shipped it as a single binary using Go Micro's modular monolith pattern.
+ Read more → +Group chat with AI. A real-time messaging app built on Go Micro with integrated AI agents.
+ +