Better Auth is so good that I **almost** switched programming languages

Better Auth is so good that I almost switched programming languages
When it comes to building APIs and services, my go-to language of choice is well, Go. And for good reason. Personally, I find that Go strikes a decent balance when it comes to safety, performance, and ease of implementation. While it may not excel in all these areas compared to other languages, I find that when it comes to being pragmatic, Go is a language that really shines.
Despite this pragmatism, however, there is one area where I find Go to be less ideal compared to other web-focused languages: authentication.
The Authentication Problem in Go
If you take a look at other web-first languages such as Ruby with Ruby on Rails or PHP with Laravel, it's incredibly easy to set up a project that has authentication built in. However, when it comes to Go, the same can't really be said.
While the Go standard library does provide a couple of packages that can be used for authentication primitives—things such as password hashing and OAuth2—these are by no means a complete auth solution.
Now I know what you're thinking: comparing the standard library of Go to these more full-featured frameworks such as Rails and Laravel isn't exactly an apples-to-apples comparison. And you're right. However, even when you consider third-party packages available to Go, the situation is still kind of bleak.
The best package available for Go is probably Auth Boss, which to be fair is pretty feature-rich. However, it unfortunately requires you to set up a lot of the integration yourself with your own system, including things such as database storage and API endpoints.
Enter Better-Auth
This isn't exactly a bad thing. However, when you compare it to another open source package that I've been using recently—Better-Auth—the difference is night and day.
Better-Auth makes it incredibly easy to add authentication into your application that works with your API layer, your front end, and even your chosen database and its driver without you needing to write any of the implementation yourself.
Key Features
In addition to basic authentication, Better-Auth makes it incredibly simple to add other auth features into your application through its fantastic plugin system:
- Two-factor authentication
- One-time passwords
- Organizations
- Payment handling (with Stripe and Polar.sh plugins)
All of these features are available if you happen to use a third-party auth provider with Go, such as Clerk or Auth0. However, unlike these platforms, which can sometimes cost an eye-watering amount (for example, two-factor authentication support on Clerk is $100 a month), Better-Auth allows you to have all of this for free.
The plugin system also provides other features that can often be tedious to implement when building a new SaaS product, including handling payments with plugins for Stripe and Polar.sh that handle all the complexity around products, checkouts, subscriptions, and even webhooks.
All this not only makes Better-Auth my favorite auth package, but it's perhaps my favorite library when it comes to building a SaaS product.
The Catch
However, there is unfortunately one catch: Better-Auth isn't available for Go. Instead, it's only available for TypeScript.
For some people, this isn't too much of a problem, but for myself, it presents a bit of an issue. While I currently do use TypeScript on the front end with Next.js, I still like to use Go when it comes to back-end services such as APIs or other systems.
So, what is a mixed language full-stack developer to do? Well, one option is to just stop using Go and instead use TypeScript on the back end. While I have been tempted a couple of times to do this over the past few weeks, I fortunately haven't yet needed to do so.
This is because it's still possible to use Better-Auth when it comes to a Go backend API, thanks again to its fantastic plugin system.
This article is sponsored by boot.dev. Use code DREAMSOFCODE to get 25% off your first payment.
Setting Up Better-Auth with Go
Prerequisites
The first thing we're going to need is actually a TypeScript server with a database connection. Fortunately, this isn't as tricky as it might seem, especially if you're using a meta framework such as Next.js, Nuxt, or SolidStart.
In my case, I like to use Next.js with Drizzle, which has great support for Better-Auth and also allows me to export database migrations, which is really useful when working with SQL databases.
However, if you're using just a front-end framework by itself (something like React, Vue, or Solid), you're going to need basically a dedicated auth server powered by something like Hono or similar. Personally, I would only really recommend using this approach with a meta framework just because it's a lot easier.
Installing Better-Auth
With your TypeScript server selected, the next thing to do is to install Better-Auth. This step is basically following the installation documentation, which is pretty easy to navigate.
For a really quick setup, you can basically paste the URL of Better-Auth into Claude or similar AI tools and it will set it up for you. Although I do recommend doing this by hand the first time just to better understand it.
For this article, I've created a sample project that contains two applications:
- The Next.js application which has both Better-Auth and Drizzle already up and running
- The Go API which we can use to test authentication once we've integrated everything together
You can find the complete code in the GitHub repository.
Working with JWT Tokens
The easiest way to achieve Go integration is to make use of another one of Better-Auth's plugins: specifically the JWT plugin, which we can use to generate JSON Web Tokens for authentication with our Go API.
If you're unfamiliar with JWTs, they're basically a self-contained JSON-encoded token that includes both user data and verification through a cryptographic signature, which makes them perfect for authenticating across services.
In our case, we'll use this JWT to not only prove that the user has been authenticated, but to also pull out some of the user's information, including their ID, name, and email address.
Adding the JWT Plugin
To add the JWT plugin to Better-Auth is incredibly easy. All we have to do is import the function and add it to the plugins array in the Better-Auth configuration:
import { betterAuth } from "better-auth"
import { jwt } from "better-auth/plugins/jwt"
export const auth = betterAuth({
// ... other config
plugins: [
jwt()
]
})
Then run the Better-Auth CLI generate or migrate command depending on your setup.
Authenticating Requests in Go
Setting Up JWT Parsing
Rather than implementing JWT parsing by hand, I'm going to use what I think is perhaps the best package when it comes to working with JSON Web Tokens in Go: the JWX package by lestrrat-go.
go get github.com/lestrrat-go/jwx/v2
Here's how to parse the JWT token in Go:
package auth
import (
"net/http"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/lestrrat-go/jwx/v2/jwk"
)
type User struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
}
func UserFromRequest(r *http.Request) (*User, error) {
// Parse the JWT token from the request
token, err := jwt.ParseRequest(r)
if err != nil {
return nil, err
}
// Get user ID from subject
userID := token.Subject()
// Extract email and name from claims
var email, name string
token.Get("email", &email)
token.Get("name", &name)
return &User{
ID: userID,
Email: email,
Name: name,
}, nil
}
Obtaining the User ID
By default when using Better-Auth, the user ID is stored inside the token's subject. Therefore we can pull it out by using the Subject()
method of the parsed token.
Verifying Token Signatures with JWKS
An important step when using JWTs is verifying that the token was actually created by your authentication server. Better-Auth makes this key set available through an API endpoint when using the JWT plugin: /api/auth/jwks
.
func UserFromRequest(r *http.Request) (*User, error) {
// Fetch the key set from Better-Auth
keySet, err := jwk.Fetch(context.Background(), "http://localhost:3000/api/auth/jwks")
if err != nil {
return nil, err
}
// Parse and verify the JWT token
token, err := jwt.ParseRequest(r, jwt.WithKeySet(keySet))
if err != nil {
return nil, err
}
// Extract user information
userID := token.Subject()
var email, name string
token.Get("email", &email)
token.Get("name", &name)
return &User{
ID: userID,
Email: email,
Name: name,
}, nil
}
In a production setting, you'd likely want to use the cache functionality of the JWK package to cache the key set rather than fetching it every time.
Caching Tokens on the Client
As mentioned, there's actually a caveat with sending tokens directly from the client. For each request to our Go API, we're actually making two requests: one to obtain the token from the auth server and the second being the actual request to the Go API.
While this works, it's not the most efficient approach. The solution is caching.
Token Caching Implementation
Here's a simple example of how to implement token caching:
class APIClient {
private cachedToken: string | null = null;
private tokenExpiry: number | null = null;
async getToken(): Promise {
// Check if cached token is still valid
if (this.cachedToken && this.tokenExpiry && Date.now() User ID: {userData.user.id}
}
Request Proxying
My favorite approach when it comes to sending authenticated requests is to perform request proxying. Rather than the client sending a request directly to the Go API, it instead sends it to the Next.js server, which then forwards it to the Go API in the backend.
Benefits of Request Proxying
- Enhanced Security: The JWT never comes back down to the client
- No CORS Setup: You don't need to set up CORS on your API server
- No Token Caching: No need to implement token caching on the client
Implementation
Here's an example API route that proxies requests:
// pages/api/[...path].ts
import { auth } from "@/lib/auth"
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
// Get token from Better-Auth
const token = await auth.api.getToken({
headers: req.headers
})
// Proxy request to Go API
const response = await fetch(`http://localhost:8080${req.url}`, {
method: req.method,
headers: {
...req.headers,
"Authorization": `Bearer ${token}`
},
body: req.method !== 'GET' ? JSON.stringify(req.body) : undefined
})
const data = await response.json()
res.json(data)
}
Caveats
There is one major caveat with request proxying: you're essentially doing a double hop from your front-end to the Go API. However, this is only really an issue if:
- Your servers aren't co-located in the same place
- You have bandwidth constraints between the two servers
- You're using a serverless platform like Vercel or Railway
Most of the time, if you keep your servers co-located and use a VPS, these caveats aren't too much of an issue.
Conclusion
With this setup, I've managed to continue using the language I most prefer for building back-end services (Go) while still being able to use what I think is the best authentication package out there (Better-Auth).
This approach has allowed me to build full-stack applications with a Go API and a TypeScript front-end framework like Next.js, getting the best of both worlds.
However, that being said, if TypeScript keeps getting packages that are as good as Better-Auth, then perhaps it's just a matter of time before I end up switching over completely!
Useful Links
- Authly Repository - Complete example code
- Better-Auth - Official documentation
- JWX Package - Go JWT library
- JWT.io - JWT debugging tool
- boot.dev - Learn backend development with Go and TypeScript (use code DREAMSOFCODE for 25% off)
If you're looking to learn how to build backend web services using either TypeScript or Go, I recommend checking out boot.dev. They even have a course on authentication that covers JWTs in more detail than I've covered in this article. Use the coupon code DREAMSOFCODE to get 25% off your first purchase.