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.
STACKURE_APP_IDis the app ID from the app's page in Stackure. Every call exceptLogoutreads it on each request and fails with avalidationerror when it's unset or isn't a UUID.STACKURE_APP_SECRETis the app secret. Stackure shows it once, when you register the app and each time you rotate it. It's sent asX-App-Secreton every call to Stackure except the sign-out call, which carries only the session token. A call that needs it fails with avalidationerror when it's unset. After a rotation, checks fail until you update it.STACKURE_BASE_URLis the Stackure address and defaults tohttps://stackure.com. Each region has its own address. This page is for https://stackure.com. The SDK sends every call there and accepts the sign-in handoff only from there.
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:
- A browser request, one whose
Acceptheader includestext/htmlbut notapplication/json, is redirected to Stackure sign-in. - Any other request gets a
401with this body:
{"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.
- Serve the registered URL behind
Auth, or the handoff never reaches the middleware. - The cookie is named
session. It'sHttpOnly,SameSite=LaxandPath=/. It'sSecurewhen the request arrived over HTTPS or withX-Forwarded-Proto: https. It lasts 7 days. - The session ends 1 day after the user signed in to Stackure, or sooner when they sign out or sign in again.
- Bodies over 4 KB and tokens that don't check out are ignored. The request goes on as usual with its body intact.
- Someone already signed in to Stackure is handed in without signing in again.
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.
- The user is attached as with
Auth, soUserFromContextworks inmcpHandler. - A request that isn't signed in gets a
401with{"error":"unauthorized"}and aWWW-Authenticateheader that tells the AI client where to sign in. - A check that can't be completed gets a
503with{"error":"unavailable"}. The SDK logs the cause. MCPreads onlyAuthorization: Bearer. It never redirects and never reads or sets a cookie, so mount it on the MCP route alone, not behindAuth.- The endpoint must use HTTPS on the same host and port as the app's registered URL, unless an MCP URL is set for the app in Stackure. The SDK works out the endpoint's address from the request's scheme,
Hostheader and path, so behind a proxy pass the originalHostandX-Forwarded-Protothrough. - Access ends when the user signs out, disconnects the client or loses access to the app. A connection also ends after 30 days unused or 90 days after it was made.
Teams and app admins
Every User from Auth or MCP carries 2 facts from the user's organization:
UserIsAppAdmin: the user is an app admin or the owner, in charge of the organization's apps.UserTeams: the teams they belong to, each aTeamwithTeamIDandTeamName. Empty when there are none.
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.
Querymatches a user's name, email and job title, or a team's name.Limitis the page size: 100 by default, 200 at most.IDsreturns only those IDs that can open the app, up to 200, to check or name people and teams you already store.- Pass
NextCursorasCursorfor the next page. It's empty on the last one. Totalis set on the first page when there are 10,000 matches or fewer.- Both use the request's session cookie, so call them from a route behind
Auth. MCP bearer tokens aren't accepted. A request without a valid session gets anautherror.
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.
Send a sign-in link
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>
- Mount
Logouton the path alone, with no method in the pattern ("/logout", not"POST /logout"), so every request to it reaches the SDK. - Trigger it with a form or button that POSTs from your app's own page. That POST signs the user out of Stackure everywhere, on every app, device and AI client. It clears your cookie and redirects to Stackure. If the call fails, the redirect goes to the Stackure sign-out page, where the user can finish signing out.
- A link or any other request goes to the Stackure sign-out page, where the user confirms. Nothing is called and the cookie stays.
- A POST counts as from your own page when
Sec-Fetch-Siteissame-originor, without that header, whenOriginmatches theHostheader. Requests that sendSec-Fetch-Site,OriginorHostmore than once don't count. This keeps a link on another site from signing your users out.
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":
// ...
}
}
| Code | Meaning |
|---|---|
validation | Caught before any request: a malformed email, an unset STACKURE_APP_SECRET or a STACKURE_APP_ID that's unset or isn't a UUID. |
auth | Stackure answered 401, for example for a wrong app secret. DirectoryUsers and DirectoryTeams also return it when there's no valid session. |
forbidden | Stackure answered 403. |
timeout | The call ran past its 2-second deadline. |
network | Everything 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
- Every call has one 2-second deadline covering the connection, headers, body and retry.
- A 5xx response or a connection failure (refused, reset, DNS or TLS) is retried once after 500 ms, only when more than 500 ms of the deadline is left.
- Timeouts are never retried. They're
timeouterrors. - Redirects aren't followed. A 3xx response is a
networkerror. - Stackure accepts 1,000 requests per minute per app for session checks, as many again for MCP checks and as many again for
DirectoryUsersandDirectoryTeamstogether. Past that it answers 429, whichAuthturns into a 500,MCPinto a 503 andDirectoryinto anetworkerror.
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.