Stackure

Stackure Go SDK

Protect routes in a net/http app. Zero dependencies.

Install the module. Set the app ID and secret. Wrap your handlers. For how sign-in works, see the overview. For the HTTP calls the SDK makes, see the API reference.

Install

go get stackure.com/sdk-go

It needs Go 1.27 or later and uses only the standard library. See the package reference and the source.

Configure

export STACKURE_APP_ID=...
export STACKURE_APP_SECRET=...
export STACKURE_BASE_URL=https://stackure.com

The SDK reads all 3 from the environment, so no function takes them as arguments.

Protect a route

import "stackure.com/sdk-go"

http.Handle("/admin", stackure.Auth()(handler))

Read the signed-in user in your handler:

user := stackure.UserFromContext(r.Context())
fmt.Println(user.UserEmail, user.AccountID)

User has UserID, AccountID (the user's organization), UserEmail, UserFirstName, UserLastName, UserIsAppAdmin and UserTeams. UserFromContext returns nil outside Auth and MCP.

A request without a valid session doesn't reach your handler:

{"error":"Unauthorized","message":"Valid authentication required","sign_in_url":"https://stackure.com/sign-in/magic-link?app_id=YOUR_APP_ID"}

When the check can't be completed, for example because Stackure can't be reached or the app secret is wrong, the request gets a 500 with "message":"Authentication verification failed" and no redirect. The SDK logs the cause with the standard log package.

Every request with a session cookie is checked with Stackure, so signing out or losing access takes effect on the next request. A request without a well-formed token is turned away without a call to Stackure. Sessions aren't tied to the browser's user agent or IP address. The SDK forwards User-Agent and X-Forwarded-For, but Stackure doesn't use them to check the session.

Sign-in handoff

You don't write this part. After the user signs in, Stackure sends the browser to your app's registered URL with a form POST carrying session_token, a token that works only for your app. Auth checks it with Stackure, stores it in a cookie on your domain and redirects to the same URL as a GET.

People can open the app once an app admin gives it to everyone in the organization or to one of their teams. App admins and the owner can always open it. Anyone else who signs in sees "You don't have access to this app."

Protect an MCP endpoint

http.Handle("/mcp", stackure.MCP()(mcpHandler))

mcpHandler is your MCP server's http.Handler. AI clients such as Claude, Claude Code, VS Code and Cursor sign users in through Stackure and send the credential it issues as a bearer token. MCP checks every request with Stackure using the same app ID and secret. There's no extra setup.

Teams and app admins

Every User from Auth or MCP carries 2 facts from the user's organization:

Stackure defines no permissions inside your app. Your app decides what these facts mean.

List who can open the app

page, err := stackure.DirectoryUsers(r, stackure.DirectoryOptions{Query: "ada"})
// page.Items: UserID, UserEmail, UserFirstName, UserLastName
// page.NextCursor, page.Total

teams, err := stackure.DirectoryTeams(r, stackure.DirectoryOptions{Limit: 50})
// teams.Items: TeamID, TeamName

For pickers and sharing, DirectoryUsers lists the users in the caller's organization who can open your app and DirectoryTeams the teams that have it. Each call returns one page, sorted by name.

Verify without middleware

result := stackure.Verify(r)
if !result.Authenticated {
    // result.Error.Code, result.Error.Message, result.Error.SignInURL
    return
}
// result.User

Verify never returns an error. No valid session is a result with Code 401 and a SignInURL. A failed check is a result with Code 500. The SDK logs the cause. Verify doesn't take the sign-in handoff, so the registered URL still needs Auth.

ValidateSession(r) returns the raw result instead: a *Session with Authenticated, User and SignInURL, plus an error.

resp, err := stackure.SendMagicLink("user@example.com")
// resp.Message

Stackure emails the user a sign-in link for your app. It works for 15 minutes. Opening it asks the user to confirm with Continue on Stackure. It then signs them in and, if they can open the app, sends them to it. The response is the same whether or not the address belongs to someone. A malformed address is a validation error before any call. Sending is limited to 3 requests per minute and 5 requests per hour for each email and IP address. Past that, it fails with a network error whose StatusCode is 429.

Sign out

http.HandleFunc("/logout", stackure.Logout)
<form method="post" action="/logout"><button>Sign Out</button></form>

Errors

Every error is a *stackure.StackureError with Code, StatusCode (0 when no response arrived) and Message. Switch on Code:

var se *stackure.StackureError
if errors.As(err, &se) {
    switch se.Code {
    case "validation", "auth", "forbidden", "timeout", "network":
        // ...
    }
}
CodeMeaning
validationCaught before any request: a malformed email, an unset STACKURE_APP_SECRET or a STACKURE_APP_ID that's unset or isn't a UUID.
authStackure answered 401, for example for a wrong app secret. DirectoryUsers and DirectoryTeams also return it when there's no valid session.
forbiddenStackure answered 403.
timeoutThe call ran past its 2-second deadline.
networkEverything else: no connection, a redirect, a 429, another 4xx, a 5xx after the retry or a response that isn't valid JSON.

Auth, MCP, Verify and Logout don't return errors. Auth and Verify report a failed check as 500 and MCP as 503. Logout always redirects.

Timeouts and limits

Releases

Releases are cut from main automatically and tagged v1.YYYYMMDD.N. Each one is signed with cosign and carries a GitHub build-provenance attestation. Report a vulnerability through GitHub Security Advisories.