Go Quickstart
Add CoreSDK to a Go service using net/http or Chi.
Phase 2 — not yet available. The Go SDK (github.com/coresdk-dev/sdk-go) ships in Phase 2. The code below reflects the planned API and will not compile today.
Go Quickstart
Prerequisites
- Go 1.21+
- A running identity provider that exposes a JWKS endpoint (Auth0, Clerk, Keycloak, etc.)
Install
Initialize your module (if you haven't already), then fetch the SDK:
go mod init github.com/your-org/your-service # skip if module already exists
go get github.com/coresdk-dev/sdk-go@latestInitialize the SDK
import sdk "github.com/coresdk-dev/sdk-go"
s := sdk.New(sdk.Config{
Tenant: "acme",
JWKSUrl: "https://your-idp.com/.well-known/jwks.json",
PolicyDir: "./policies",
OTELEndpoint: "http://localhost:4317", // optional
})| Field | Description |
|---|---|
Tenant | Your tenant slug — stamped on every span and error response |
JWKSUrl | JWKS endpoint used to verify incoming JWTs |
PolicyDir | Directory of .rego policy files (watched for live reload) |
OTELEndpoint | OTLP gRPC endpoint for traces and metrics (optional) |
Full net/http example
A complete main.go using the standard library:
package main
import (
"encoding/json"
"log/slog"
"net/http"
sdk "github.com/coresdk-dev/sdk-go"
)
type Order struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Description string `json:"description"`
}
func main() {
s := sdk.New(sdk.Config{
Tenant: "acme",
JWKSUrl: "https://your-idp.com/.well-known/jwks.json",
PolicyDir: "./policies",
})
mux := http.NewServeMux()
// Wrap each handler with s.Require() to enforce a policy action.
// s.Require() itself is middleware, so it chains with http.HandlerFunc.
mux.Handle("GET /api/orders", s.Require("orders:read")(http.HandlerFunc(listOrders)))
mux.Handle("POST /api/orders", s.Require("orders:write")(http.HandlerFunc(createOrder)))
mux.Handle("GET /api/orders/{id}", s.Require("orders:read")(http.HandlerFunc(getOrder)))
// s.Handler() wraps the entire mux with JWT validation + tenant context.
// It must be the outermost wrapper so context is populated before Require() runs.
handler := s.Handler(mux)
slog.Info("listening", "addr", ":3000")
if err := http.ListenAndServe(":3000", handler); err != nil {
slog.Error("server error", "err", err)
}
}
func listOrders(w http.ResponseWriter, r *http.Request) {
user := sdk.UserFromContext(r.Context())
tenant := sdk.TenantFromContext(r.Context())
orders, err := db.Orders.ForUser(user.ID, tenant.ID)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(orders)
}
func getOrder(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // Go 1.22+ pattern matching
user := sdk.UserFromContext(r.Context())
tenant := sdk.TenantFromContext(r.Context())
order, err := db.Orders.Get(id, tenant.ID)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(order)
}
func createOrder(w http.ResponseWriter, r *http.Request) {
user := sdk.UserFromContext(r.Context())
tenant := sdk.TenantFromContext(r.Context())
var payload struct {
Description string `json:"description"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
order, err := db.Orders.Create(user.ID, tenant.ID, payload.Description)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(order)
}Chi router example
Chi's middleware stack makes it easy to apply s.Middleware() globally and s.Require() per route group:
package main
import (
"encoding/json"
"log/slog"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
sdk "github.com/coresdk-dev/sdk-go"
)
func main() {
s := sdk.New(sdk.Config{
Tenant: "acme",
JWKSUrl: "https://your-idp.com/.well-known/jwks.json",
PolicyDir: "./policies",
})
r := chi.NewRouter()
// Standard Chi middleware
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
// CoreSDK JWT validation — applies to every route below
r.Use(s.Middleware())
// Public routes (JWT optional — user may be nil)
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
})
// Protected route group — require "orders:read" for all GET routes
r.Group(func(r chi.Router) {
r.Use(s.Require("orders:read"))
r.Get("/api/orders", listOrders)
r.Get("/api/orders/{id}", getOrder)
})
// Write routes require a different action
r.Group(func(r chi.Router) {
r.Use(s.Require("orders:write"))
r.Post("/api/orders", createOrder)
r.Delete("/api/orders/{id}", deleteOrder)
})
// Admin routes require an admin action
r.Group(func(r chi.Router) {
r.Use(s.Require("admin:users:read"))
r.Get("/api/admin/users", listUsers)
})
slog.Info("listening", "addr", ":3000")
http.ListenAndServe(":3000", r)
}Extracting user context
Use sdk.UserFromContext and sdk.TenantFromContext in any handler after s.Middleware() or s.Handler() has run:
import sdk "github.com/coresdk-dev/sdk-go"
func myHandler(w http.ResponseWriter, r *http.Request) {
user := sdk.UserFromContext(r.Context())
tenant := sdk.TenantFromContext(r.Context())
// User fields
_ = user.ID // stable user ID (JWT sub claim)
_ = user.Email // from the identity provider
_ = user.Role // resolved from tenant membership
_ = user.TenantID // matches tenant.ID
// Tenant fields
_ = tenant.ID // tenant slug
_ = tenant.Name // display name
_ = tenant.Plan // "free" | "team" | "business" | "enterprise"
// Raw JWT claims
claims := sdk.ClaimsFromContext(r.Context())
customVal, _ := claims["custom_claim"].(string)
slog.Info("handling request",
"user_id", user.ID,
"tenant", tenant.ID,
)
}UserFromContext returns nil if the middleware has not run or if no valid JWT was presented — check for nil on routes where authentication is optional.
Error responses
CoreSDK automatically returns RFC 9457 application/problem+json for all auth errors. No custom error handling is required:
{
"type": "https://coresdk.dev/errors/policy-denied",
"title": "Authorization Denied",
"status": 403,
"detail": "Action 'orders:read' denied for role 'guest'",
"trace_id": "01HX7KQMB4NWE9P6T2JS0RY3ZV",
"tenant": "acme"
}To customize the response body, provide a custom ErrorHandler in the config:
s := sdk.New(sdk.Config{
Tenant: "acme",
JWKSUrl: "https://your-idp.com/.well-known/jwks.json",
ErrorHandler: func(w http.ResponseWriter, r *http.Request, sdkErr sdk.Error) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(sdkErr.StatusCode())
json.NewEncoder(w).Encode(map[string]any{
"error": sdkErr.Code(),
"detail": sdkErr.Error(),
})
},
})Run locally
# Start the service
go run ./cmd/server
# Test with a valid JWT (replace with a real token from your IdP)
curl http://localhost:3000/api/orders \
-H "Authorization: Bearer <your-jwt>"
# Without a token — expect 401
curl http://localhost:3000/api/orders
# Health check (no token needed)
curl http://localhost:3000/healthNext steps
- Writing Rego policies — controlling what each role can do
- JWT configuration — audience, issuer, clock skew
- Multi-tenancy — tenant isolation and row-level security
- Observability — traces, metrics, and the CoreSDK dashboard
- Error reference — full list of error types and status codes