Skip to main content
CoreSDK
Getting Started

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@latest

Initialize 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
})
FieldDescription
TenantYour tenant slug — stamped on every span and error response
JWKSUrlJWKS endpoint used to verify incoming JWTs
PolicyDirDirectory of .rego policy files (watched for live reload)
OTELEndpointOTLP 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/health

Next steps

On this page