Skip to content

Instantly share code, notes, and snippets.

@sbusso
Created April 2, 2025 09:36
Show Gist options
  • Save sbusso/df100d0b328e22fb97c300aa97a4dc40 to your computer and use it in GitHub Desktop.
Save sbusso/df100d0b328e22fb97c300aa97a4dc40 to your computer and use it in GitHub Desktop.
Minimal Go backend with Clerk authentication
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/clerk/clerk-sdk-go/v2"
clerkhttp "github.com/clerk/clerk-sdk-go/v2/http"
"github.com/joho/godotenv"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/labstack/gommon/log"
)
// ClerkMiddleware creates a middleware for Echo that handles Clerk authentication
func ClerkMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Use the Clerk SDK's middleware function under the hood
handler := clerkhttp.WithHeaderAuthorization()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Update Echo's context with the modified request context from Clerk
c.SetRequest(r)
// Continue processing the request
next(c)
}))
// Call the Clerk middleware handler
handler.ServeHTTP(c.Response().Writer, c.Request())
// The status code is already written by the Clerk middleware if there was an error
// So we only return nil to prevent Echo from trying to write a response again
return nil
}
}
func main() {
// Load environment variables from .env file
if err := godotenv.Load(); err != nil {
log.Warn("No .env file found")
}
// Initialize Clerk with API key from environment variable
clerkAPIKey := os.Getenv("CLERK_API_KEY")
if clerkAPIKey == "" {
log.Fatal("CLERK_API_KEY environment variable is required")
}
clerk.SetKey(clerkAPIKey)
// Create a new Echo instance
e := echo.New()
e.Logger.SetLevel(log.INFO)
// Add middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// Public routes
e.GET("/", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"access": "public",
})
})
// Protected routes
api := e.Group("/api")
api.Use(ClerkMiddleware)
api.GET("", func(c echo.Context) error {
// Get the session claims from the context using the Clerk SDK helper
claims, ok := clerk.SessionClaimsFromContext(c.Request().Context())
if !ok {
return c.JSON(http.StatusUnauthorized, map[string]string{
"access": "unauthorized",
})
}
return c.JSON(http.StatusOK, map[string]string{
"user_id": claims.Subject,
})
})
api.GET("/user/profile", getUserProfile)
e.GET("/health", healthCheck)
// Start server
serverPort := os.Getenv("SERVER_PORT")
if serverPort == "" {
serverPort = "3000"
}
// Start server in a goroutine so it doesn't block
go func() {
if err := e.Start(fmt.Sprintf(":%s", serverPort)); err != nil && err != http.ErrServerClosed {
e.Logger.Fatal("shutting down the server")
}
}()
// Wait for interrupt signal to gracefully shutdown the server
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// Graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal(err)
}
}
// Health check handler
func healthCheck(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"status": "healthy",
})
}
// Get user profile handler
func getUserProfile(c echo.Context) error {
claims, ok := clerk.SessionClaimsFromContext(c.Request().Context())
if !ok {
return c.JSON(http.StatusUnauthorized, map[string]string{
"access": "unauthorized",
})
}
// Build profile data
profile := map[string]interface{}{
"id": claims.ID,
}
return c.JSON(http.StatusOK, profile)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment