|
| 1 | +--- |
| 2 | +title: "✂️ snips.sh retrospective: 1000+ stars later" |
| 3 | +date: 2024-10-04T13:50:16-04:00 |
| 4 | +draft: false |
| 5 | +tags: |
| 6 | + - golang |
| 7 | + - terminal |
| 8 | + - ssh |
| 9 | + - sqlite |
| 10 | + - tensorflow |
| 11 | +--- |
| 12 | + |
| 13 | +{{< tweet "1657139515557920770" >}} |
| 14 | + |
| 15 | +## What the snip? |
| 16 | + |
| 17 | +A tad bit over a year ago, I released https://snips.sh, a passwordless, anonymous SSH-powered pastebin with a human-friendly TUI and web UI. No logins, no passwords, nothing to install. It's ready-to-go on any machine that has SSH installed. |
| 18 | + |
| 19 | +It's a simple as: |
| 20 | + |
| 21 | +```bash |
| 22 | +echo 'this is amazing!' | ssh snips.sh |
| 23 | +``` |
| 24 | + |
| 25 | +I wanted an easy utility to copy code snippets to/from machines, a dead-simple web UI to link to line numbers and something to just dump code snippets. |
| 26 | + |
| 27 | +And the development community loved it. To my surprise, it rapidly gained popularity across social media. It even made the top of GitHub's [`/trending`](https://github.com/trending) under the Go language category for a couple days. |
| 28 | + |
| 29 | + |
| 30 | + |
| 31 | +Given I procrastinated for over a year making an original "release" blog post for snips, I figured a retrospective would be just as good. |
| 32 | + |
| 33 | +## The philosophy |
| 34 | + |
| 35 | +When designing snips, I wanted it to be as _simple_ as possible. If I learned anything from maintaining open source libraries and supporting public APIs used by millions of people, it's important to not bloat with verbose functionality that becomes a maintenance/compatibility nightmare. |
| 36 | + |
| 37 | +I manifested my inner Ken and Dennis and kept the [Unix Philosophy](https://en.wikipedia.org/wiki/Unix_philosophy) top of mind: |
| 38 | + |
| 39 | +> Write programs that do one thing and do it well. |
| 40 | +> Write programs to work together. |
| 41 | +> Write programs to handle text streams, because that is a universal interface. |
| 42 | +
|
| 43 | +And that's exactly what snips is. |
| 44 | + |
| 45 | +1. It's a snippet store (with a UI and TUI), nothing more. |
| 46 | +2. It works with other command line programs via pipes. |
| 47 | +3. The "API" is just text over stdin/stdout. |
| 48 | + |
| 49 | + |
| 50 | + |
| 51 | +But, this isn't just a command line utility. While I love the Unix Philosophy, it is not my creed. Just as much as I believe simplicity is key in software development, the user experience is just as important. This is often a very hard balance. |
| 52 | + |
| 53 | +Under the covers, snips.sh is a stateful remote resource that requires functionality beyond the simple input/output. And that's what the TUI is for. It's a shell into the user's snips. You can use the TUI to view snips syntax highlighted, edit attributes and delete them. |
| 54 | + |
| 55 | + |
| 56 | + |
| 57 | +As a developer building tools for developers, I know how comfortable most are in the terminal, which is why I chose that as the entrypoint over a web UI. They don't even need to lift their fingers off the keyboard. |
| 58 | + |
| 59 | +I also wanted the onboarding experience to be as smooth as possible. Here's how the upload works for a new user: |
| 60 | + |
| 61 | +{{< mermaid >}} |
| 62 | +%%{init: {'theme': 'dark' } }%% |
| 63 | +flowchart TD |
| 64 | + ssh([ssh session]) |
| 65 | + fail((fail)) |
| 66 | + onboard((onboard)) |
| 67 | + a@{ shape: diamond, label: "auth" } |
| 68 | + pk@{ shape: diamond, label: "exists?" } |
| 69 | + wf((write snip)) |
| 70 | + |
| 71 | + |
| 72 | + ssh --> a |
| 73 | + a --"password"--> fail |
| 74 | + a --"pubkey"-->pk |
| 75 | + |
| 76 | + pk --"no"--> onboard |
| 77 | + pk --"yes"--> wf |
| 78 | + onboard --> wf |
| 79 | + |
| 80 | +{{< /mermaid >}} |
| 81 | + |
| 82 | +The usage of snips.sh does require public key auth in order to identify users. If a connection attempt is made with a password, it fails and sends a message to stdout. This also helps prevent against bots and other things that like to poke at port 22. |
| 83 | + |
| 84 | +For any new users, if their public key doesn't exist in the database, we'll "onboard" which will create a new user record and associate a public key with it. A terms of service message is also printed for new users. Since we're able to create a new identity and automatically onboard, there is literally zero friction to get started. |
| 85 | + |
| 86 | +While it's nice to keep things as simple as can be, like anything with software engineering "it depends" on your use case. Personally, I'm a fan of making easy (and fun!) to use software, which might need a few engineering tradeoffs for a better user experience. |
| 87 | + |
| 88 | +As it turns out, people like easy to use software: |
| 89 | + |
| 90 | +{{< tweet "1657456167524917248" >}} |
| 91 | + |
| 92 | +{{< tweet "1657477244158189570" >}} |
| 93 | + |
| 94 | +## The technology |
| 95 | + |
| 96 | +All of snips.sh is written in Go, from the SSH app to the web UI. While Go may not be as creative or fast as other languages, I do find beauty in the simplicity. |
| 97 | + |
| 98 | +As luck might have it, there's also an organization called [Charm](https://charm.sh/) that builds amazing libraries for the command line, all built in Go. So surprise, snips.sh uses [plenty of Charm libraries](https://github.com/robherley/snips.sh/blob/b6c00d501f44ccddbbd323fb4cbaded1124aef5b/go.mod#L9-L13). They're a great group of people, you should totally check them out. |
| 99 | + |
| 100 | +There's no real fancy frameworks in snips.sh. All the web-based routes use the standard library's [`net/http`](https://pkg.go.dev/net/http) server along with the [`html/template`](https://pkg.go.dev/html/template) package for server side rendering. There is about ~120 lines of JavaScript and some old fashioned hand-rolled CSS to keep things as tiny as can be. |
| 101 | + |
| 102 | +As for the backing storage, I went with the most deployed database in the world, SQLite. Why SQLite you ask? |
| 103 | +1. It's _really_ fast.[^1] |
| 104 | +2. It's stupid simple to use. It's embedded and doesn't need extra resources/configuration. |
| 105 | +3. The database is all stored in a single file, making it easy to manage (and [backup](https://litestream.io/)). |
| 106 | +4. Some people much smarter than me have been scaling it like crazy.[^2] |
| 107 | + |
| 108 | +While some S3-compatible storage might have been first choice for some, I considered it overkill. Having to run another program or worse (connect to a cloud provider!) I figured the 1MB file size limit would be absolutely fine in a blob column, especially since it's compressed with [zstd](https://facebook.github.io/zstd/) too. |
| 109 | + |
| 110 | +For a lot of the fancy web UI rendering, I have to give credit to some amazing open source libraries: |
| 111 | +- [`alecthomas/chroma`](https://github.com/alecthomas/chroma): Syntax Highlighter |
| 112 | +- [`yuin/goldmark`](https://github.com/yuin/goldmark): Markdown Parser |
| 113 | +- [`microcosm-cc/bluemonday`](https://github.com/microcosm-cc/bluemonday): HTML Sanitizer |
| 114 | +- [`tdewolff/minify`](https://github.com/tdewolff/minify): Asset Minifier |
| 115 | + |
| 116 | +And that's pretty much it! Keeping the technology simple means it's just as easy for someone else to run snips.sh on their own hardware. And that's exactly why we have a [self hosting guide](https://snips.sh/docs/self-hosting.md) and publish an multi-arch container image to GitHub Container Registry: |
| 117 | + |
| 118 | +``` |
| 119 | +ghcr.io/robherley/snips.sh |
| 120 | +``` |
| 121 | + |
| 122 | +Given the simple tech stack, it's pretty easy to get going after a couple volume mounts and environment variables. |
| 123 | + |
| 124 | +## The tensorflow-sized elephant in the room |
| 125 | + |
| 126 | +So, one _must have_ that I wanted for snips is to automatically detect the uploaded code language. To do this, I used a tensorflow model, [`yoeo/guesslang`](https://github.com/yoeo/guesslang). This is actually the same model that [Visual Studio Code](https://code.visualstudio.com/) uses, but they use [Tensorflow.js](https://www.tensorflow.org/js/), you can check it out at [`Microsoft/vscode-languagedetection`](https://github.com/Microsoft/vscode-languagedetection). |
| 127 | + |
| 128 | +But we do not have server side JS here, we're in a compiled language. This was my first hurdle, and I ended up writing [`robherley/guesslang-go`](https://github.com/robherley/guesslang-go) which uses some wrappers around libtensorflow's C API. |
| 129 | + |
| 130 | +Unfortunately, this means we lose the ability to make a static executable and need to sacrifice portability: |
| 131 | + |
| 132 | +{{< terminal >}} |
| 133 | +you@local$ docker run -it --entrypoint=ldd ghcr.io/robherley/snips.sh /usr/bin/snips.sh |
| 134 | + linux-vdso.so.1 (0x00007ffd219a8000) |
| 135 | + libtensorflow.so.2 => /usr/local/lib/libtensorflow.so.2 (0x00007f2922485000) |
| 136 | + libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f292225c000) |
| 137 | + libtensorflow_framework.so.2 => /usr/local/lib/libtensorflow_framework.so.2 (0x00007f292036d000) |
| 138 | + librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f2920368000) |
| 139 | + libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f2920363000) |
| 140 | + libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f292027a000) |
| 141 | + libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f2920275000) |
| 142 | + libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f2920049000) |
| 143 | + libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f2920029000) |
| 144 | + /lib64/ld-linux-x86-64.so.2 (0x00007f2933a32000) |
| 145 | +{{< /terminal >}} |
| 146 | + |
| 147 | +Even worse, look how big this is! |
| 148 | + |
| 149 | +{{< terminal >}} |
| 150 | +you@local$ docker run -it --entrypoint=ls ghcr.io/robherley/snips.sh -lah /usr/local/lib |
| 151 | +total 416M |
| 152 | +drwxr-xr-x 1 root root 4.0K Sep 13 15:53 . |
| 153 | +drwxr-xr-x 1 root root 4.0K Aug 8 14:03 .. |
| 154 | +-r-xr-xr-x 1 root root 375M Sep 13 15:51 libtensorflow.so.2 |
| 155 | +-r-xr-xr-x 1 root root 42M Sep 13 15:51 libtensorflow_framework.so.2 |
| 156 | +{{< /terminal >}} |
| 157 | + |
| 158 | +Yikes. Not ideal. |
| 159 | + |
| 160 | +I did search for alternatives, like relying on chroma's [built in lexers to identify the language](https://github.com/alecthomas/chroma#identifying-the-language) but it was not good enough for small snippets. Other language detection features of editors and other tools like GitHub's [linguist](https://github.com/github-linguist/linguist) rely on file extensions, which we don't have. |
| 161 | + |
| 162 | +This is a prime example of making sacrifices for an extremely useful feature. It does put a smile on my face when I see the correct language detected on upload. |
| 163 | + |
| 164 | +Another huge gotcha with libtensorflow is the lack of support for many architectures. Luckily this can be solved with some compiler flags (`-tags noguesser`) and a multiarch container image, but some users lose that critical functionality. |
| 165 | + |
| 166 | +This is an area that I am not very strong in. I'd love any suggestions on this topic, feel free to [open an issue](https://github.com/robherley/snips.sh/issues). |
| 167 | + |
| 168 | +## The ship |
| 169 | + |
| 170 | +I started on this side project at the beginning of 2023[^3], and "released" it via social media in May of that same year. This was my first real side-hack that turned into a pretty useful tool, and I felt a warm welcome from the developer community. |
| 171 | + |
| 172 | +My largest audience was Twitter, having over 120k views on [my tweet](https://x.com/robherley/status/1657139515557920770). Some retweets from folks like [@mxcl](https://twitter.com/mxcl) (creator of Homebrew) and [@charmcli](https://twitter.com/charmcli) really helped get it to the right audience. |
| 173 | + |
| 174 | +Surprisingly, even folks on reddit took it pretty well! |
| 175 | + |
| 176 | +{{< reddit "https://www.reddit.com/r/golang/comments/13fyp1k/snipssh_passwordless_anonymous_sshpowered_pastebin/" "snips.sh: passwordless, anonymous SSH-powered pastebin" >}} |
| 177 | + |
| 178 | +I was delighted to see snips.sh in all difference communities: |
| 179 | +- [console.dev](https://console.dev) made a review: https://console.dev/tools/snips |
| 180 | +- [@JeremiahSecrist](https://github.com/JeremiahSecrist) published a nixpkg: https://mynixos.com/nixpkgs/package/snips-sh |
| 181 | +- [@Sanix-Darker](https://github.com/Sanix-Darker) made a nvim extension: https://github.com/Sanix-Darker/snips.nvim |
| 182 | + |
| 183 | +Shortly after release, we already had issues and contributions coming in too! |
| 184 | + |
| 185 | +It truly was a great ship! 🚢 |
| 186 | + |
| 187 | +## The numbers |
| 188 | + |
| 189 | +### Connections |
| 190 | + |
| 191 | +Users can reach snips.sh via HTTP or SSH. The request metrics are emitted to DataDog but unfortunately I only have up to a little over a year's worth of retention, so here's since July 2023: |
| 192 | + |
| 193 | +{{< row >}} |
| 194 | + |
| 195 | +{{< count n="130844" label="HTTP Requests" color="green-6" >}} |
| 196 | + |
| 197 | +{{< count n="116889" label="SSH Sessions" color="amber-6" >}} |
| 198 | + |
| 199 | +{{</ row >}} |
| 200 | + |
| 201 | +Note: the above SSH sessions are for successfully authenticated users. If we include all non-authenticated (anything hitting port 22), snips.sh has seen **2.148 million** unique SSH sessions. |
| 202 | + |
| 203 | +### App |
| 204 | + |
| 205 | +This entire app is hosted on a [Digital Ocean](https://digitalocean.com) droplet to keep costs low. The database is still relatively small around **~24MB**, which is not including backups on [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces) via [litestream](https://litestream.io). |
| 206 | + |
| 207 | +{{< row >}} |
| 208 | + |
| 209 | +{{< count n="1534" label="Users" color="purple-6" >}} |
| 210 | + |
| 211 | +{{< count n="2486" label="Files" color="blue-9" >}} |
| 212 | + |
| 213 | +{{< count n="57" label="Langs" color="teal-9" >}} |
| 214 | + |
| 215 | +{{< /row >}} |
| 216 | + |
| 217 | +After copying the sqlite database to my host machine and running aggregations, we can see a nice time series of usage: |
| 218 | + |
| 219 | + |
| 220 | + |
| 221 | + |
| 222 | + |
| 223 | +Unsurprisingly, we had a huge burst of users during the "Twitter hype period" and gradually slowed down. While I would have loved to market this more, my goal wasn't to make a disrupting product, just a fun developer tool. Plus, over the lifecycle of this release, I was busy planning an engagement and then my wedding! |
| 224 | + |
| 225 | +Back to the metrics, we had the usual suspects of popular files. |
| 226 | + |
| 227 | + |
| 228 | + |
| 229 | +You can find the full language list... [on snips](https://snips.sh/f/yfojYVMqSU)! |
| 230 | + |
| 231 | +### Open Source |
| 232 | + |
| 233 | +We've had some great contributions like [zstd compression](https://github.com/robherley/snips.sh/pull/46), [arm64 support](https://github.com/robherley/snips.sh/pull/42), bugfixes and more. Dependabot is also carrying the weight a bit with **~65** Pull Requests alone. |
| 234 | + |
| 235 | +{{< row >}} |
| 236 | + |
| 237 | +{{< count n="148" label="Commits" color="green-9" >}} |
| 238 | + |
| 239 | +{{< count n="175" label="Pull Requests" color="purple-6" >}} |
| 240 | + |
| 241 | +{{< count n="26" label="Issues" color="teal-9" >}} |
| 242 | + |
| 243 | +{{< count n="1028" label="Stars" color="amber-9" >}} |
| 244 | + |
| 245 | +{{< count n="13" label="Contributors" color="pink-9" >}} |
| 246 | + |
| 247 | +{{</ row >}} |
| 248 | + |
| 249 | +## Thanks! |
| 250 | + |
| 251 | +Appreciate all the contributions and kind words people have given me throughout this project! It gives me the motivation to keep on building, and you should too 💪 |
| 252 | + |
| 253 | +[^1]: https://sqlite.org/fasterthanfs.html |
| 254 | +[^2]: https://fly.io/docs/litefs/ |
| 255 | +[^3]: https://github.com/robherley/snips.sh/commit/4982dafd6204d56c7670aa2ef258638e318447f4 |
| 256 | +[^4]: https://github.com/robherley/snips.sh/pkgs/container/snips.sh |
0 commit comments