Openstatus www.openstatus.dev

๐Ÿ“ goat stack (#1221)

* ๐Ÿ“ goat stack

* ๐Ÿ–Š๏ธ improve goat stack article

* ๐Ÿš€

authored by

Thibault Le Ouay and committed by
GitHub
e60e1412 2cfdfb60

+231
apps/web/public/assets/posts/introducing-goatstack/bsky-post.png

This is a binary file and will not be displayed.

apps/web/public/assets/posts/introducing-goatstack/goat.png

This is a binary file and will not be displayed.

+231
apps/web/src/content/posts/introducing-goatstack.mdx
··· 1 + --- 2 + title: Introducing the GoaT stack 3 + description: 4 + A full-stack app template featuring a Golang server and a Vite + React SPA front end. 5 + author: 6 + name: Thibault Le Ouay Ducasse 7 + url: https://bsky.app/profile/thibaultleouay.dev 8 + avatar: /assets/authors/thibault.jpeg 9 + publishedAt: 2025-03-24 10 + image: /assets/posts/introducing-goatstack/goat.png 11 + tag: engineering 12 + --- 13 + 14 + ## What is the GoaT stack? 15 + 16 + The GoaT stack is a [full-stack app template](https://github.com/openstatusHQ/goat-stack) featuring a Golang server and a Vite + React SPA front end. 17 + 18 + We want the GoaT stack to have a great DX. 19 + 20 + ### What is included in the GoaT stack? 21 + 22 + **[SQLite](https://www.sqlite.org/)**: We have chosen SQLite as the database because we love it. Itโ€™s just a file. 23 + **[LiteStream](https://litestream.io/)**: To backup our database 24 + 25 + 26 + **[Protocol Buffer with ConnecTRPC](https://connectrpc.com/)**: For their http based protocol that works with Protocol Buffer. 27 + 28 + **[Chi](https://go-chi.io)**: For our Golang router\ 29 + **[connectrpc.com/connect](https://connectrpc.com/connect)**: To handle http based request 30 + 31 + 32 + **[React](https://react.dev/)**: Our frontend framework of choice.\ 33 + **[TanStack Query + Connect-Query](https://github.com/connectrpc/connect-query-es)**: to generate our typesafe query.\ 34 + **[TanStack Router](https://tanstack.com/router/latest)**: The typed router is a joy to work with. 35 + 36 + 37 + 38 + ### API: Schema First Approach 39 + 40 + Using Protocol Buffer we can design our API in `proto` file 41 + 42 + ```proto 43 + syntax = "proto3"; 44 + 45 + package goat.v1; 46 + 47 + enum Vote { 48 + YES = 0; 49 + NO = 1; 50 + } 51 + 52 + message VoteRequest { 53 + Vote Vote = 1; 54 + } 55 + message VoteResponse { 56 + bool Success = 1; 57 + } 58 + ``` 59 + 60 + ### Why not going full stack TypeScript 61 + 62 + 63 + <a href="https://bsky.app/profile/did:plc:eu6cezqsf5yocjsyc7mgkued/post/3lkeag64a2s2x"><Image 64 + alt="New users per week" 65 + src="/assets/posts/introducing-goatstack/bsky-post.png" 66 + width={650} 67 + height={575} 68 + /></a> 69 + 70 + 71 + We have worked in a large monorepo, we have often been frustrated by the number of times we had to restart our TypeScript LSP. 72 + 73 + 74 + 75 + 76 + We love Golang. We heavily rely on it for OpenStatus. 77 + 78 + ## How to get started with the Goat Stack 79 + 80 + 81 + ### Requirements 82 + 83 + To get started you need to have these installed on your computer 84 + 85 + - [Just](https://just.systems) 86 + - [Golang](https://go.dev/) 87 + - [pnpm](https://pnpm.io) 88 + - [Node](https://nodejs.org/en) 89 + 90 + 91 + 92 + ### Get Started 93 + 1. First clone our repository : [https://github.com/openstatusHQ/goat-stack](https://github.com/openstatusHQ/goat-stack) 94 + 95 + 2. Run 96 + ```bash 97 + just init 98 + ``` 99 + 100 + It will download all the dependancies. 101 + 102 + 3. Open your IDE and update `packages/proto/goat/v1/goat.proto` 103 + Add new procedure in the GoatService 104 + 105 + ```proto 106 + service GoatService { 107 + rpc GetVotes(GetVotesRequest) returns (GetVotesResponse) {} 108 + rpc Vote(VoteRequest) returns (VoteResponse) {} 109 + } 110 + 111 + message GetVotesRequest {} 112 + 113 + message GetVotesResponse { 114 + int64 Yes = 1; 115 + int64 No = 2; 116 + } 117 + 118 + ``` 119 + 120 + 4. Run `just buf` 121 + 122 + 5. Implement the handler in the server `apps/server/internal/goat/handler.go` 123 + 124 + ```go 125 + func (h *goatHandler) Vote(ctx context.Context, req *connect.Request[goatv1.VoteRequest]) (*connect.Response[goatv1.VoteResponse], error) { 126 + tx := h.db.MustBegin() 127 + var value string 128 + switch req.Msg.Vote { 129 + case goatv1.Vote_YES: 130 + value = "yes" 131 + break 132 + case goatv1.Vote_NO: 133 + value = "no" 134 + break 135 + default: 136 + break 137 + } 138 + r := tx.MustExec("INSERT INTO vote (timestamp, vote) VALUES ($1, $2)", time.Now().Unix(), value) 139 + tx.Commit() 140 + res := connect.NewResponse(&goatv1.VoteResponse{ 141 + Success: true, 142 + }) 143 + return res, nil 144 + } 145 + ``` 146 + 147 + 6. Start calling it in your React App with the newly generated query 148 + 149 + ```tsx 150 + // Our wrapper around tanstack query 151 + import { useMutation } from "@connectrpc/connect-query"; 152 + import { createFileRoute, Link, useRouter } from "@tanstack/react-router"; 153 + // Our generated query 154 + import { vote } from "../gen/proto/goat/v1/goat-GoatService_connectquery"; 155 + // Our generated types 156 + import { Vote } from "../gen/proto/goat/v1/goat_pb"; 157 + import { Button } from "@goat/ui/components/button"; 158 + 159 + export const Route = createFileRoute("/")({ 160 + component: App, 161 + }); 162 + 163 + function App() { 164 + // Use the mutation hook with our generated query 165 + const v = useMutation(vote); 166 + const { navigate } = useRouter(); 167 + return ( 168 + <div> 169 + <div > 170 + <p>Is this the ๐Ÿ stack?</p> 171 + <div> 172 + <Button 173 + variant={"outline"} 174 + disabled={v.isPending} 175 + onClick={async () => { 176 + await v.mutateAsync({ 177 + Vote: Vote.YES, 178 + }); 179 + navigate({ to: "/results" }); 180 + }} 181 + > 182 + Yes 183 + </Button> 184 + <Button 185 + variant={"outline"} 186 + disabled={v.isPending} 187 + onClick={async () => { 188 + await v.mutateAsync({ 189 + Vote: Vote.NO, 190 + }); 191 + navigate({ to: "/results" }); 192 + }} 193 + > 194 + No 195 + </Button> 196 + </div> 197 + </div> 198 + </div> 199 + ); 200 + } 201 + 202 + ```` 203 + 204 + 205 + ### How to deploy it. 206 + 207 + We provide 2 docker files to deploy the dashboard and the server where you want. For example our server for [GoatStack.dev](https://GoatStack.dev) is hosted on [Koyeb](https://www.koyeb.com/) and our dashboard on [Cloudflare](https://cloudflare.com/) 208 + 209 + Before deploying the server you need to update `apps/server/etc/litestream.yml` with your S3 compatible bucket key to backup your database. 210 + 211 + ```yaml 212 + dbs: 213 + - path: /data/db 214 + replicas: 215 + - type: s3 216 + endpoint: https://${CLOUDFLARE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com/ 217 + bucket: goat-stack 218 + access-key-id: ${CLOUDFLARE_R2_ACCESS_KEY_ID} 219 + secret-access-key: ${CLOUDFLARE_R2_SECRET_ACCESS_KEY} 220 + ``` 221 + 222 + 223 + ### Conclusion 224 + 225 + We hope you will enjoy using the GoaT stack as much as we do. We are looking forward to seeing what you will build with it. Feel free to reach out to us on [ping@openstatus](mailto:ping@openstatus.dev?subject=GoatStack) if you have any questions or feedback. 226 + 227 + And if you want to contribute to the GoaT stack, we would be more than happy to welcome you to our community. 228 + 229 + And create a free [OpenStatus account](/app/login) to monitor your server and get notified if something goes wrong. 230 + 231 + Happy coding! ๐Ÿ