Stackure

Stackure JavaScript SDK

Protect routes in Express, Connect or plain Node.js. Zero dependencies.

Install the package. Set the app ID and secret. Add the middleware. For how sign-in works, see the overview. For the HTTP calls the SDK makes, see the API reference.

Install

npm install stackure

It needs Node.js 22 or later. It's ESM only and ships its TypeScript types. See the package 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 at call time and never caches them, so no function takes them as arguments.

Protect a route

import { auth, userFromRequest } from 'stackure';

app.get('/admin', auth(), (req, res) => {
  const user = userFromRequest(req);
  res.json({ email: user.user_email, account: user.account_id });
});

userFromRequest(req) returns the signed-in user, or undefined 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 console.error, starting stackure: verification error:.

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 gets the sign-in URL 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.

The middleware writes with res.setHeader and res.writeHead, so it works on Express, Connect and a bare http.createServer. On Fastify, pass request.raw and reply.raw.

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 that the POST came from STACKURE_BASE_URL, validates the token with Stackure, stores it in a cookie on your domain and redirects to the same URL with a 303, so the browser loads it with 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

import { mcp, userFromRequest } from 'stackure';

app.all('/mcp', mcp(), (req, res) => {
  const user = userFromRequest(req);
  // serve the MCP request
});

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 signed-in user

auth() and mcp() attach the same User:

FieldValue
user_idThe user's ID
account_idThe ID of the user's organization
user_emailTheir email address
user_first_nameTheir first name
user_last_nameTheir last name
user_is_app_adminWhether they're an app admin or the owner, in charge of the organization's apps
user_teamsTheir teams, each { team_id, team_name }. 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

import { directoryTeams, directoryUsers } from 'stackure';

const { items, nextCursor, total } = await directoryUsers(req, { query: 'ada' });
// items: { user_id, user_email, user_first_name, user_last_name }[]

const teams = await directoryTeams(req, { limit: 50 });
// teams.items: { team_id, team_name }[]

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

import { verify } from 'stackure';

const result = await verify(req);
if (!result.authenticated) {
  // result.error.code, result.error.message, result.error.sign_in_url
  return res.status(result.error.code).json(result.error);
}
// result.user

verify never throws. No valid session is a result with code 401 and a sign_in_url. 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(req) returns the raw result instead, { authenticated, user, sign_in_url }, and throws StackureError when the check fails.

import { sendMagicLink } from 'stackure';

const resp = await 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 throws a network error whose statusCode is 429.

Sign out

import { logout } from 'stackure';

app.all('/logout', (req, res) => logout(req, res));
<form method="post" action="/logout"><button>Sign Out</button></form>

Errors

sendMagicLink, validateSession, directoryUsers and directoryTeams throw StackureError, with code, statusCode (undefined when no response arrived) and message. Switch on code:

import { StackureError } from 'stackure';

try {
  await sendMagicLink(email);
} catch (err) {
  if (err instanceof StackureError) {
    switch (err.code) {
      case 'validation':
      case 'auth':
      case 'forbidden':
      case 'timeout':
      case 'network':
        // ...
    }
  }
}
CodeMeaning
validationA missing or malformed email or STACKURE_APP_ID. An unset STACKURE_APP_SECRET. Caught before any request.
authStackure answered 401, for example for a wrong app secret. directoryUsers and directoryTeams also throw it without a valid session.
forbiddenStackure answered 403.
timeoutThe call ran past its 2-second deadline.
networkEverything else: no connection, a 429, another 4xx, a 5xx or a response that isn't valid JSON.

auth(), mcp(), verify and logout don't throw. auth() and verify report a failed check as 500 and mcp() as 503. logout always redirects.

Timeouts and limits

Releases

Releases are published to npm from main automatically and tagged v1.YYYYMMDD.N. Each one carries npm provenance and a GitHub build-provenance attestation. The SDK is MIT licensed. Report a vulnerability through GitHub Security Advisories.