<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Saif </title>
    <description>The latest articles on DEV Community by Saif  (@saif_shines).</description>
    <link>https://dev.to/saif_shines</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F17823%2F91f9c351-1a4b-4849-a4b7-80a136b6be3f.jpg</url>
      <title>DEV Community: Saif </title>
      <link>https://dev.to/saif_shines</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saif_shines"/>
    <language>en</language>
    <item>
      <title>Enterprise Auth in Astro without the pain</title>
      <dc:creator>Saif </dc:creator>
      <pubDate>Wed, 25 Mar 2026 01:35:05 +0000</pubDate>
      <link>https://dev.to/saif_shines/enterprise-auth-in-astro-without-the-pain-1idh</link>
      <guid>https://dev.to/saif_shines/enterprise-auth-in-astro-without-the-pain-1idh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A complete, production-oriented walkthrough of adding SSO, social login, magic links, and session management to your Astro application using Scalekit — the auth platform built for B2B and AI apps.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why Scalekit for B2B auth?&lt;/li&gt;
&lt;li&gt;Core concepts: OAuth 2.0, OIDC, and the token trio&lt;/li&gt;
&lt;li&gt;Project setup &amp;amp; environment&lt;/li&gt;
&lt;li&gt;Initializing the Scalekit client&lt;/li&gt;
&lt;li&gt;The three auth endpoints&lt;/li&gt;
&lt;li&gt;Session middleware — the right way&lt;/li&gt;
&lt;li&gt;Protecting pages and API routes&lt;/li&gt;
&lt;li&gt;Enterprise SSO: per-organization connections&lt;/li&gt;
&lt;li&gt;PKCE flow (no client secret)&lt;/li&gt;
&lt;li&gt;Production best practices&lt;/li&gt;
&lt;li&gt;Troubleshooting common gotchas&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why Scalekit for B2B auth?
&lt;/h2&gt;

&lt;p&gt;Shipping authentication feels deceptively simple: throw in a social login button, store a JWT, call it done. That works fine for consumer apps. But the moment your first enterprise prospect lands, everything changes. They need to log in via their company's Okta or Entra ID. Their IT team will ask whether you support SCIM. Their security team wants to audit every login event. And their procurement team won't sign off until you can prove SOC 2 compliance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In 7 out of 10 enterprise deals, authentication requirements like SSO are deal-breakers.&lt;/strong&gt; Teams that deprioritize this until they're deep in a sales cycle often spend months scrambling to retrofit auth — all while the deal sits on ice.&lt;/p&gt;

&lt;p&gt;Scalekit is designed to short-circuit that. It is an authentication platform built specifically for the B2B and AI application layer: it handles the full OAuth 2.0 / OIDC handshake, supports SAML and OIDC enterprise SSO out of the box, includes SCIM provisioning, social logins, magic links, and MFA — and exposes all of it through a single, tightly typed SDK. You get back tokens and a user profile; the heavy lifting stays on Scalekit's side.&lt;/p&gt;

&lt;p&gt;A single Scalekit environment can serve multiple applications (e.g., &lt;code&gt;app.yourcompany.com&lt;/code&gt; and &lt;code&gt;docs.yourcompany.com&lt;/code&gt;), so users authenticate once and share the same session across all of your properties. This matters especially for teams building content sites, documentation portals, or companion apps alongside a primary SaaS dashboard — a very common Astro pattern.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Scalekit's philosophy:&lt;/strong&gt; You should not need to become an identity expert to ship enterprise-grade auth. Scalekit handles SAML assertion validation, OIDC discovery, token rotation, and IdP quirks so your team can stay focused on the product.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Core concepts: OAuth 2.0, OIDC, and the token trio
&lt;/h2&gt;

&lt;p&gt;Before touching code, grounding the concepts pays dividends when you're debugging at midnight before a launch. Scalekit's auth surface is built on standard protocols — nothing proprietary, nothing that locks you in.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Authorization Code Flow
&lt;/h3&gt;

&lt;p&gt;This is the flow you'll use for server-rendered Astro apps. Your server never exposes a client secret to the browser. The user is redirected to Scalekit, authenticates, and Scalekit sends a short-lived &lt;strong&gt;authorization code&lt;/strong&gt; back to your callback URL. Your server exchanges that code for three tokens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser → /api/auth/login → Scalekit (IdP / SSO) → /api/auth/callback → HttpOnly Cookies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The token trio
&lt;/h3&gt;

&lt;p&gt;Scalekit returns three tokens on a successful exchange. Understanding their purpose determines how you store and use them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Token&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Typical lifetime&lt;/th&gt;
&lt;th&gt;Where to store&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;idToken&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Identifies the user (name, email, sub). Used for logout hint.&lt;/td&gt;
&lt;td&gt;1 hour&lt;/td&gt;
&lt;td&gt;HttpOnly cookie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;accessToken&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Authorizes API calls. Validated server-side on every request.&lt;/td&gt;
&lt;td&gt;15–60 minutes&lt;/td&gt;
&lt;td&gt;HttpOnly cookie&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;refreshToken&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Obtains a new access token without re-authentication.&lt;/td&gt;
&lt;td&gt;Days to weeks&lt;/td&gt;
&lt;td&gt;HttpOnly cookie (secure)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Never store tokens in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;localStorage&lt;/code&gt; and &lt;code&gt;sessionStorage&lt;/code&gt; are accessible from any JavaScript running on your page, making them vulnerable to XSS attacks. Always use &lt;strong&gt;HttpOnly cookies&lt;/strong&gt; on the server side. Astro's cookie API makes this trivial.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  SAML vs. OIDC — which does your enterprise customer need?
&lt;/h3&gt;

&lt;p&gt;Scalekit abstracts this decision for you: it accepts SAML assertions from enterprise IdPs like Okta and Entra ID and converts them into standard OIDC tokens before returning them to your app. You write OIDC code once; Scalekit handles the protocol translation. That said, knowing the landscape helps when a customer's IT team comes calling:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Common use case&lt;/th&gt;
&lt;th&gt;Scalekit support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SAML 2.0&lt;/td&gt;
&lt;td&gt;XML assertions&lt;/td&gt;
&lt;td&gt;Enterprise SSO (Okta, Entra ID, ADFS)&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OIDC&lt;/td&gt;
&lt;td&gt;JWT tokens&lt;/td&gt;
&lt;td&gt;Modern SSO, social login, API auth&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth 2.0&lt;/td&gt;
&lt;td&gt;Bearer tokens&lt;/td&gt;
&lt;td&gt;API authorization&lt;/td&gt;
&lt;td&gt;✅ Full&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Project setup &amp;amp; environment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A Scalekit account — free tier includes 1M MAUs, 100 organizations, 1 SSO + 1 SCIM connection&lt;/li&gt;
&lt;li&gt;An Astro project (v4 or v5) with &lt;code&gt;output: 'server'&lt;/code&gt; configured&lt;/li&gt;
&lt;li&gt;Node.js ≥ 18 (the SDK uses native &lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;crypto&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Your Scalekit environment URL, client ID, and client secret from the dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Create the Astro project
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Scaffold a new Astro project&lt;/span&gt;
npm create astro@latest my-app &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--template&lt;/span&gt; minimal
&lt;span class="nb"&gt;cd &lt;/span&gt;my-app

&lt;span class="c"&gt;# Install Scalekit SDK and the Node.js adapter&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; @scalekit-sdk/node @astrojs/node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure server-side rendering
&lt;/h3&gt;

&lt;p&gt;Scalekit's auth flow requires server-side code to exchange tokens and set cookies. Astro's default &lt;code&gt;output: 'static'&lt;/code&gt; mode won't work for auth endpoints. You need &lt;code&gt;output: 'server'&lt;/code&gt; with the Node adapter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// astro.config.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@astrojs/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;node&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;standalone&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;strong&gt;Hybrid rendering:&lt;/strong&gt; If you're using &lt;code&gt;output: 'static'&lt;/code&gt; for most of your site but want SSR for auth, you can use hybrid mode: set &lt;code&gt;output: 'static'&lt;/code&gt; globally and add &lt;code&gt;export const prerender = false&lt;/code&gt; to each auth endpoint file. The behavior is identical.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Environment variables
&lt;/h3&gt;

&lt;p&gt;Add your Scalekit credentials to &lt;code&gt;.env&lt;/code&gt;. Never commit this file — add it to &lt;code&gt;.gitignore&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env
SCALEKIT_ENVIRONMENT_URL=https://&amp;lt;your-env&amp;gt;.scalekit.cloud
SCALEKIT_CLIENT_ID=skc_&amp;lt;your-client-id&amp;gt;
SCALEKIT_CLIENT_SECRET=sks_&amp;lt;your-secret&amp;gt;
SCALEKIT_REDIRECT_URI=http://localhost:4321/api/auth/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  TypeScript types and IntelliSense
&lt;/h3&gt;

&lt;p&gt;Declare the env variables and the &lt;code&gt;App.Locals&lt;/code&gt; shape so TypeScript can type-check your middleware and Astro pages throughout the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/env.d.ts&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;reference types="astro/client" /&amp;gt;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ImportMetaEnv&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;SCALEKIT_ENVIRONMENT_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;SCALEKIT_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;SCALEKIT_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;SCALEKIT_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ImportMeta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ImportMetaEnv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// available when using enterprise SSO&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Initializing the Scalekit client
&lt;/h2&gt;

&lt;p&gt;Create a singleton client so the SDK doesn't re-initialize on every request. Astro's server modules are cached between requests, so a module-level export is the right pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/scalekit.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ScalekitClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@scalekit-sdk/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Singleton — instantiated once, reused across requests&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ScalekitClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SCALEKIT_ENVIRONMENT_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SCALEKIT_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SCALEKIT_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;REDIRECT_URI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SCALEKIT_REDIRECT_URI&lt;/span&gt;
  &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:4321/api/auth/callback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Validate at startup:&lt;/strong&gt; In production, wrap the &lt;code&gt;ScalekitClient&lt;/code&gt; constructor in a guard that throws if any of the three required env vars are missing. Missing credentials fail silently in some configurations and produce confusing 401 errors at runtime.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The three auth endpoints
&lt;/h2&gt;

&lt;p&gt;The entire auth flow is orchestrated through three API routes. Think of them as a clean boundary: the browser never touches tokens, and your Astro pages never call Scalekit directly. All sensitive work stays server-side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/pages/api/auth/
  login.ts     ← generates the authorization URL, redirects user
  callback.ts  ← exchanges code for tokens, sets cookies
  logout.ts    ← clears cookies, ends Scalekit session
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  login.ts — initiating the flow
&lt;/h3&gt;

&lt;p&gt;The login route's only job is to redirect the user to Scalekit's authorization endpoint. The SDK builds the URL — including the correct state parameter, code challenge (if using PKCE), and any login hints you want to pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/pages/api/auth/login.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;REDIRECT_URI&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../../lib/scalekit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Optional: pass organizationId for direct IdP routing (enterprise SSO)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orgId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthorizationUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offline_access&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;// Route directly to an enterprise IdP by org:&lt;/span&gt;
    &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;strong&gt;&lt;code&gt;offline_access&lt;/code&gt; scope:&lt;/strong&gt; Including &lt;code&gt;offline_access&lt;/code&gt; in the scopes array instructs Scalekit to return a refresh token. Without it, users will need to re-authenticate every time the access token expires. Always include it for production applications.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  callback.ts — exchanging the code for tokens
&lt;/h3&gt;

&lt;p&gt;After the user authenticates, Scalekit redirects back to this endpoint with a one-time authorization code. The server exchanges it for tokens using the client secret — a step that &lt;em&gt;cannot&lt;/em&gt; happen in the browser because it requires the secret. Tokens are then stored as HttpOnly cookies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/pages/api/auth/callback.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;REDIRECT_URI&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../../lib/scalekit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cookies&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Handle errors gracefully — Scalekit sends them as query params&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;desc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error_description&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/?error=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refreshToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticateWithCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cookieOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;httpOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sameSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// Set expiry to match the refresh token lifetime&lt;/span&gt;
      &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 7 days&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk-id-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cookieOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk-access-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cookieOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk-refresh-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cookieOptions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Redirect to a post-login destination (support ?next= param)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[scalekit] token exchange failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/?error=auth_failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  logout.ts — ending the session correctly
&lt;/h3&gt;

&lt;p&gt;Logout is a two-step process: clear your local cookies, then redirect to Scalekit's logout endpoint so it terminates the session on its side too. Skipping the second step means users can return to Scalekit and silently re-authenticate without entering credentials again — not what you want.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/pages/api/auth/logout.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../../lib/scalekit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cookies&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk-id-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 1: clear local session cookies&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deleteOpts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk-id-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteOpts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk-access-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteOpts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk-refresh-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;deleteOpts&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 2: redirect to Scalekit's logout endpoint&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logoutUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogoutUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;idTokenHint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;postLogoutRedirectUri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logoutUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Register your post-logout redirect URI.&lt;/strong&gt; The &lt;code&gt;postLogoutRedirectUri&lt;/code&gt; must be registered in your Scalekit dashboard under &lt;strong&gt;Settings → Redirects&lt;/strong&gt;. If it's not allowlisted, Scalekit will reject the logout request and the user will land on an error page.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Session middleware — the right way
&lt;/h2&gt;

&lt;p&gt;Astro middleware runs on every incoming request before any page or API route handler fires. This is the right place to validate the session and populate &lt;code&gt;Astro.locals.user&lt;/code&gt;. Do it once, centrally, and every page in your app benefits automatically.&lt;/p&gt;

&lt;p&gt;The middleware below implements a two-stage validation: try the access token first, fall back to the refresh token if it's expired, and clear the session if both fail. This gives users seamless silent sessions without constant re-authentication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/middleware.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineMiddleware&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro:middleware&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IdTokenClaim&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@scalekit-sdk/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./lib/scalekit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clearSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk-id-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk-access-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk-refresh-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Skip auth check for public assets and auth routes&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/_astro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.(&lt;/span&gt;&lt;span class="sr"&gt;ico|png|jpg|svg|css|js|woff2&lt;/span&gt;&lt;span class="se"&gt;?)&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk-access-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Fast path: access token is still valid&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validateToken&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IdTokenClaim&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Slow path: token expired — try refresh&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refreshToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk-refresh-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newToken&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refreshAccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sk-access-token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;httpOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;sameSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// new access token: 1 hour&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validateToken&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IdTokenClaim&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Refresh also failed — wipe everything&lt;/span&gt;
      &lt;span class="nf"&gt;clearSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Skip middleware on auth routes.&lt;/strong&gt; Notice the early return at the top for &lt;code&gt;/api/auth/*&lt;/code&gt;, &lt;code&gt;/_astro&lt;/code&gt;, and static file extensions. Without this, the middleware runs on the callback endpoint before cookies are set, causing unnecessary validation overhead on unauthenticated requests.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Protecting pages and API routes
&lt;/h2&gt;

&lt;p&gt;Once the middleware populates &lt;code&gt;Astro.locals.user&lt;/code&gt;, protecting a page is just a conditional redirect in the frontmatter. No wrapper components, no higher-order functions — just plain server logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Protected page pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// src/pages/dashboard.astro
import Layout from '../layouts/Layout.astro';

const user = Astro.locals.user;
// Redirect unauthenticated users back through login, preserving the destination
if (!user) {
  return Astro.redirect(`/api/auth/login?next=${encodeURIComponent('/dashboard')}`);
}
---
&amp;lt;Layout title="Dashboard"&amp;gt;
  &amp;lt;h1&amp;gt;Welcome, {user.name ?? user.email}&amp;lt;/h1&amp;gt;
  &amp;lt;p&amp;gt;You're signed in. &amp;lt;a href="/api/auth/logout"&amp;gt;Sign out&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
&amp;lt;/Layout&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Protected API route pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/pages/api/data.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;… your data …&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Navigation component
&lt;/h3&gt;

&lt;p&gt;A nav component that adapts to auth state is a common need. Since it's an Astro component (server-rendered), you can read &lt;code&gt;Astro.locals.user&lt;/code&gt; directly — no client-side JS required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// src/components/AuthNav.astro
const user = Astro.locals.user;
---
&amp;lt;nav&amp;gt;
  {user ? (
    &amp;lt;&amp;gt;
      &amp;lt;span&amp;gt;{user.name ?? user.email}&amp;lt;/span&amp;gt;
      &amp;lt;a href="/api/auth/logout"&amp;gt;Sign out&amp;lt;/a&amp;gt;
    &amp;lt;/&amp;gt;
  ) : (
    &amp;lt;a href="/api/auth/login"&amp;gt;Sign in&amp;lt;/a&amp;gt;
  )}
&amp;lt;/nav&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Enterprise SSO: per-organization connections
&lt;/h2&gt;

&lt;p&gt;One of Scalekit's headline capabilities is letting each of your enterprise customers connect their own IdP — Okta, Microsoft Entra ID, Google Workspace, PingFederate, ADFS — without you writing any IdP-specific code. Scalekit stores the per-org SSO configuration and does the SAML/OIDC handshake transparently.&lt;/p&gt;

&lt;p&gt;From your application's perspective, you simply pass an &lt;code&gt;organizationId&lt;/code&gt; (or a &lt;code&gt;loginHint&lt;/code&gt; email domain) when generating the authorization URL, and Scalekit routes the user to the correct IdP automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/pages/api/auth/login.ts — enterprise routing&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Option A: route by org ID (stored in your DB after org onboarding)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orgId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Option B: route by email domain (Scalekit resolves IdP from domain)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loginHint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthorizationUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offline_access&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;organizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;loginHint&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;loginHint&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The org-aware login form
&lt;/h3&gt;

&lt;p&gt;A typical enterprise login UX asks for a work email, resolves the org from the domain, and redirects silently into SSO. No custom IdP UI needed on your end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
// src/pages/login.astro
const error = Astro.url.searchParams.get('error');
---
&amp;lt;form action="/api/auth/login" method="GET"&amp;gt;
  &amp;lt;label&amp;gt;
    Work email
    &amp;lt;input type="email" name="email" required placeholder="you@company.com" /&amp;gt;
  &amp;lt;/label&amp;gt;
  &amp;lt;button type="submit"&amp;gt;Continue with SSO&amp;lt;/button&amp;gt;
  {error &amp;amp;&amp;amp; &amp;lt;p class="error"&amp;gt;{decodeURIComponent(error)}&amp;lt;/p&amp;gt;}
&amp;lt;/form&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;a href="/api/auth/login"&amp;gt;Or sign in with Google / GitHub&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;IdP-initiated SSO:&lt;/strong&gt; Some enterprise IdPs can initiate SSO by pushing an assertion to your app without the user visiting your login page first (common in Okta app launchers). Scalekit handles this scenario. In your callback handler, if the code exchange succeeds but there's no &lt;code&gt;state&lt;/code&gt; parameter in the redirect, you're in an IdP-initiated flow. Validate carefully and redirect to a safe default post-login destination.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Reading org context from the token
&lt;/h3&gt;

&lt;p&gt;After a successful SSO authentication, the decoded access token claims will include the user's &lt;code&gt;orgId&lt;/code&gt;. Use this to scope database queries and enforce multi-tenant isolation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/middleware.ts — extended claims&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validateToken&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;IdTokenClaim&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;org_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// available on enterprise SSO logins&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  PKCE flow (no client secret)
&lt;/h2&gt;

&lt;p&gt;The Authorization Code flow with a client secret is ideal for traditional SSR apps like Astro in server mode. But there are scenarios where you won't have — or don't want — a secret: Astro's hybrid/static mode with edge functions, or an open-source site where the server code is public. In those cases, use PKCE (Proof Key for Code Exchange).&lt;/p&gt;

&lt;p&gt;PKCE replaces the client secret with a cryptographic challenge/verifier pair generated fresh per-request. Scalekit's developer docs site itself is an open-source Astro project that uses PKCE — it's a real-world reference you can study.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Authorization Code + Secret&lt;/th&gt;
&lt;th&gt;PKCE (no secret)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Code generation&lt;/td&gt;
&lt;td&gt;SDK builds URL, secret on server&lt;/td&gt;
&lt;td&gt;SDK generates &lt;code&gt;code_verifier&lt;/code&gt; + &lt;code&gt;code_challenge&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verifier storage&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Session cookie or server-side store&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token exchange&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;code&lt;/code&gt; + &lt;code&gt;secret&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;code&lt;/code&gt; + &lt;code&gt;code_verifier&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;SSR Astro (server mode)&lt;/td&gt;
&lt;td&gt;Edge, static hybrid, public repos&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/pages/api/auth/login.ts — PKCE variant&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generatePkce&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@scalekit-sdk/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// or your own crypto util&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cookies&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;codeVerifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;codeChallenge&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generatePkce&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Store verifier server-side — needed at callback&lt;/span&gt;
  &lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pkce-verifier&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;codeVerifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;httpOnly&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;sameSite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthorizationUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offline_access&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;codeChallenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;codeChallengeMethod&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;S256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Production best practices
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BP 01 — Always set Secure + HttpOnly + SameSite on cookies&lt;/strong&gt;&lt;br&gt;
All three attributes together block the main vectors of session hijacking. &lt;code&gt;SameSite: 'lax'&lt;/code&gt; is the right default — it allows top-level navigations but blocks cross-site POSTs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BP 02 — Validate the access token on every request&lt;/strong&gt;&lt;br&gt;
Never trust a cookie value without cryptographic validation. The SDK's &lt;code&gt;validateToken()&lt;/code&gt; verifies the signature and expiry. This check is fast — it's local JWT verification with no network call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BP 03 — Rotate tokens silently with refresh&lt;/strong&gt;&lt;br&gt;
The middleware's fallback to &lt;code&gt;refreshAccessToken()&lt;/code&gt; keeps users logged in seamlessly for the lifetime of the refresh token. Always store the refresh token in an HttpOnly cookie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BP 04 — Implement post-login redirect with &lt;code&gt;?next=&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
When a user hits a protected page unauthenticated, store their intended URL and restore it after login. Otherwise you always land on the home page, frustrating deep-link sharing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BP 05 — Handle errors in the callback, not with crashes&lt;/strong&gt;&lt;br&gt;
Scalekit sends &lt;code&gt;?error=&amp;amp;error_description=&lt;/code&gt; to your callback URL on failure (cancelled login, misconfigured IdP). Always check for these and redirect gracefully rather than letting the exchange throw.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BP 06 — Register all redirect URIs before deploying&lt;/strong&gt;&lt;br&gt;
Scalekit validates every redirect URI against a strict allowlist. Add your production, staging, and preview URLs in the dashboard before deploying. Mismatched URIs are the #1 cause of auth failures at launch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BP 07 — Use environment-specific Scalekit environments&lt;/strong&gt;&lt;br&gt;
Scalekit supports multiple environments (dev, staging, prod) with separate credentials and SSO connections. Never use the same environment URL for local development and production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BP 08 — Multi-tenant isolation via &lt;code&gt;orgId&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
When enterprise SSO is used, the token includes &lt;code&gt;org_id&lt;/code&gt;. Always scope database queries and API calls to this value. Never let a user from Org A read data from Org B by trusting only the &lt;code&gt;sub&lt;/code&gt; claim.&lt;/p&gt;
&lt;h3&gt;
  
  
  Security headers
&lt;/h3&gt;

&lt;p&gt;Beyond token security, add HTTP security headers to your Astro middleware. These are complementary to auth, not a substitute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/middleware.ts — security headers addition&lt;/span&gt;
&lt;span class="c1"&gt;// At the end of your middleware, before returning next()&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Frame-Options&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DENY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Content-Type-Options&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nosniff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Referrer-Policy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;strict-origin-when-cross-origin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Security-Policy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;default-src 'self'; script-src 'self'; connect-src 'self' https://*.scalekit.cloud;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Troubleshooting common gotchas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;redirect_uri_mismatch&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the single most common error. Scalekit requires the redirect URI in the authorization request to exactly match a registered URI — including trailing slashes, protocol, and port. Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The URI in your &lt;code&gt;.env&lt;/code&gt; matches what's registered in the Scalekit dashboard&lt;/li&gt;
&lt;li&gt;You're not accidentally including or omitting a trailing slash&lt;/li&gt;
&lt;li&gt;In development, the port (&lt;code&gt;4321&lt;/code&gt;) is correct and not being remapped&lt;/li&gt;
&lt;li&gt;In production, you've added the &lt;code&gt;https://&lt;/code&gt; version, not just &lt;code&gt;http://&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Token validation fails after deployment
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;validateToken()&lt;/code&gt; checks the token's &lt;code&gt;iss&lt;/code&gt; claim against your environment URL. If your &lt;code&gt;SCALEKIT_ENVIRONMENT_URL&lt;/code&gt; has a trailing slash in one place and not another, validation will fail. Normalize it: strip trailing slashes before passing to the client constructor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Refresh token not returned
&lt;/h3&gt;

&lt;p&gt;If &lt;code&gt;refreshToken&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt; after the code exchange, you forgot to include &lt;code&gt;offline_access&lt;/code&gt; in your scopes. Update your login endpoint's scopes array and test with a fresh login (existing tokens won't gain &lt;code&gt;offline_access&lt;/code&gt; retroactively).&lt;/p&gt;

&lt;h3&gt;
  
  
  Middleware runs on static assets
&lt;/h3&gt;

&lt;p&gt;Astro middleware runs before static file serving in some adapter configurations. Add an early return for paths you know are static (the example in Section 6 already includes this). If you're still seeing performance issues, verify your adapter version.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enterprise SSO user not found in your database
&lt;/h3&gt;

&lt;p&gt;The first time a user logs in via enterprise SSO, they may not have a row in your database. The &lt;code&gt;sub&lt;/code&gt; claim is the stable identifier — it won't change even if the user changes their email. On first login, use it as the primary key to create a new user record. Treat this as a just-in-time (JIT) provisioning pattern — a common B2B requirement when SCIM is not yet configured.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;Here's the final file structure for a complete Scalekit + Astro authentication setup. Everything fits in a handful of files — no auth framework configuration DSLs, no provider callback soup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  lib/
    scalekit.ts          ← SDK singleton + REDIRECT_URI
  pages/
    api/auth/
      login.ts           ← GET → redirect to Scalekit
      callback.ts        ← GET → exchange code, set cookies
      logout.ts          ← GET → clear cookies, end session
    login.astro          ← Login page with email input
    dashboard.astro      ← Protected page
  components/
    AuthNav.astro        ← Conditional sign in/out nav
  middleware.ts          ← Token validation + silent refresh
  env.d.ts               ← TypeScript env + App.Locals types
.env                     ← SCALEKIT_* credentials (gitignored)
astro.config.mjs         ← output: 'server', adapter: node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scalekit's value proposition is exactly this compactness. You've added enterprise SSO, social login, magic link support, SCIM-ready organization management, token rotation, and production-grade session handling — and you haven't written a single line of SAML parsing, JWT signing, or PKCE crypto yourself. The auth complexity lives inside Scalekit's infrastructure; your codebase stays readable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚀 &lt;strong&gt;Next steps:&lt;/strong&gt; Once auth is running, the natural next steps in a B2B application are SCIM provisioning (so your enterprise customers can auto-provision and deprovision users from their IdP), organization management APIs (invite flows, role assignment), and Auth Logs for audit trails. All are available in Scalekit — the same SDK, same environment, no new dependencies.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Scalekit developer docs — &lt;a href="https://docs.scalekit.com" rel="noopener noreferrer"&gt;docs.scalekit.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Node.js SDK source — &lt;a href="https://github.com/scalekit-inc/scalekit-sdk-node" rel="noopener noreferrer"&gt;github.com/scalekit-inc/scalekit-sdk-node&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Astro example with Authorization Code flow — &lt;a href="https://github.com/scalekit-developers/astro-scalekit-auth-example" rel="noopener noreferrer"&gt;github.com/scalekit-developers/astro-scalekit-auth-example&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Scalekit developer docs site (PKCE, no SDK) — &lt;a href="https://github.com/scalekit-inc/developer-docs" rel="noopener noreferrer"&gt;github.com/scalekit-inc/developer-docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Astro on-demand rendering guide — &lt;a href="https://docs.astro.build/en/guides/on-demand-rendering" rel="noopener noreferrer"&gt;docs.astro.build/en/guides/on-demand-rendering&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written for the Scalekit developer community · &lt;a href="https://scalekit.com" rel="noopener noreferrer"&gt;scalekit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>astro</category>
      <category>oauth</category>
      <category>typescript</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Add Enterprise SSO to Your Next.js App in Minutes Using Claude Code &amp; Scalekit</title>
      <dc:creator>Saif </dc:creator>
      <pubDate>Tue, 24 Mar 2026 04:13:07 +0000</pubDate>
      <link>https://dev.to/scalekit-inc/add-enterprise-sso-to-your-nextjs-app-in-minutes-using-claude-code-scalekit-5edg</link>
      <guid>https://dev.to/scalekit-inc/add-enterprise-sso-to-your-nextjs-app-in-minutes-using-claude-code-scalekit-5edg</guid>
      <description>&lt;p&gt;&lt;strong&gt;The old way&lt;/strong&gt;: Spend hours reading OAuth documentation, configuring SAML endpoints, building login flows, handling token validation, and creating admin portals for your enterprise customers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The new way&lt;/strong&gt;: Describe what you want in plain English. Let Claude Code and Scalekit's plugin do the rest.&lt;/p&gt;

&lt;p&gt;This guide shows you how to add production-ready enterprise Single Sign-On to a Next.js application using Claude Code's plugin ecosystem—going from zero to a working SSO flow with customer self-service portal in under 10 minutes.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/6KxtXKbMvHQ"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What You're Building&lt;/li&gt;
&lt;li&gt;Understanding the Plugin Architecture&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Step-by-Step Implementation&lt;/li&gt;
&lt;li&gt;Testing with the IDP Simulator&lt;/li&gt;
&lt;li&gt;How It Actually Works&lt;/li&gt;
&lt;li&gt;Production Considerations&lt;/li&gt;
&lt;li&gt;Alternative: Using Other AI Coding Agents&lt;/li&gt;
&lt;li&gt;Why This Matters&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What You're Building
&lt;/h2&gt;

&lt;p&gt;By the end of this guide, you'll have:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Enterprise SSO Authentication Flow&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users enter their work email (&lt;code&gt;user@company.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Automatic redirect to their organization's identity provider (Microsoft Entra ID, Okta, Google Workspace, etc.)&lt;/li&gt;
&lt;li&gt;Secure callback handling with token validation&lt;/li&gt;
&lt;li&gt;Session creation with verified user identity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Self-Service Admin Portal&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your B2B customers configure their own IDP connections&lt;/li&gt;
&lt;li&gt;No engineering involvement needed for enterprise onboarding&lt;/li&gt;
&lt;li&gt;Support for SAML and OIDC protocols&lt;/li&gt;
&lt;li&gt;Real-time connection status monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Production-Ready Code&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Environment-based configuration&lt;/li&gt;
&lt;li&gt;Proper error handling&lt;/li&gt;
&lt;li&gt;Security best practices baked in&lt;/li&gt;
&lt;li&gt;Next.js App Router patterns&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Understanding the Plugin Architecture
&lt;/h2&gt;

&lt;p&gt;Before we dive in, let's understand what makes this possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Are Claude Code Plugins?
&lt;/h3&gt;

&lt;p&gt;Claude Code plugins are &lt;strong&gt;packaged bundles of capabilities&lt;/strong&gt; that extend what Claude can do. Think of them as specialized skill packs that teach Claude how to work with specific services, frameworks, or patterns.&lt;/p&gt;

&lt;p&gt;A plugin can contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skills&lt;/strong&gt;: Context-aware instruction sets Claude automatically uses when relevant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Servers&lt;/strong&gt;: Connections to external APIs and services via Model Context Protocol&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commands&lt;/strong&gt;: Slash commands you invoke manually (e.g., &lt;code&gt;/deploy&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subagents&lt;/strong&gt;: Specialized AI agents for specific tasks (e.g., security review)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hooks&lt;/strong&gt;: Scripts that run on specific events (pre-commit, post-deployment)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How Scalekit's Plugin Works
&lt;/h3&gt;

&lt;p&gt;The Scalekit Auth Stack plugin gives Claude Code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Documentation Access&lt;/strong&gt;: Real-time access to Scalekit's implementation guides via MCP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Integration&lt;/strong&gt;: Ability to query your Scalekit environment, retrieve config, manage connections&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implementation Patterns&lt;/strong&gt;: Battle-tested code patterns for SSO flows, session management, and security&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework Knowledge&lt;/strong&gt;: How to properly integrate with Next.js, Express, FastAPI, and other frameworks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you ask Claude to "add SSO," it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads your existing codebase structure&lt;/li&gt;
&lt;li&gt;Pulls relevant Scalekit documentation&lt;/li&gt;
&lt;li&gt;Queries your Scalekit environment via MCP&lt;/li&gt;
&lt;li&gt;Generates complete, working code that follows your project's patterns&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Required:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Next.js application (even a fresh &lt;code&gt;create-next-app&lt;/code&gt; works)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://code.claude.com/docs/en/quickstart" rel="noopener noreferrer"&gt;Claude Code installed&lt;/a&gt; and configured&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://scalekit.com" rel="noopener noreferrer"&gt;Scalekit account&lt;/a&gt; with dashboard access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You'll need from Scalekit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Environment URL (e.g., &lt;code&gt;https://yourenv.scalekit.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Client ID (from your Scalekit dashboard)&lt;/li&gt;
&lt;li&gt;Client Secret (generate in dashboard, keep secure)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Time investment:&lt;/strong&gt; ~10 minutes&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-Step Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Install the Scalekit Plugin
&lt;/h3&gt;

&lt;p&gt;Open your terminal and start the Claude Code REPL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the Scalekit Auth Stack marketplace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add scalekit-inc/claude-code-authstack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see confirmation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;❯ /plugin marketplace add scalekit-inc/claude-code-authstack

⎿  Successfully added marketplace: scalekit-auth-stack
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now install the Modular SSO plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin &lt;span class="nb"&gt;install &lt;/span&gt;modular-sso@scalekit-auth-stack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What just happened?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code now has access to Scalekit's SSO implementation knowledge&lt;/li&gt;
&lt;li&gt;The plugin includes MCP servers that can query your Scalekit environment&lt;/li&gt;
&lt;li&gt;Skills are loaded that guide code generation for auth flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Verify installation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;modular-sso&lt;/code&gt; in your installed plugins list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Navigate to Your Project
&lt;/h3&gt;

&lt;p&gt;Exit the REPL (&lt;code&gt;Ctrl+D&lt;/code&gt; or &lt;code&gt;exit&lt;/code&gt;) and navigate to your Next.js project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/your-nextjs-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start Claude Code in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Give Claude the Objective
&lt;/h3&gt;

&lt;p&gt;Here's where the magic happens. Paste this prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;My goal is to add SSO to my application and also let my customers 
configure SSO connections with their IDP using an admin portal.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Watch Claude work:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Skill Detection&lt;/strong&gt;: Claude automatically identifies the Modular SSO skill is relevant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codebase Analysis&lt;/strong&gt;: Reads your &lt;code&gt;package.json&lt;/code&gt;, route structure, and existing patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation Retrieval&lt;/strong&gt;: Fetches Scalekit's implementation guide via MCP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment Discovery&lt;/strong&gt;: Queries your Scalekit account to get environment details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Generation&lt;/strong&gt;: Creates all necessary files following Next.js conventions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You'll see output like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Loading skill: Modular SSO
Analyzing project structure...
Reading Next.js configuration...
Fetching Scalekit environment configuration...
Found environment: https://yourenv.scalekit.com
Retrieved Client ID: sk_live_...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Approve the Redirect URI
&lt;/h3&gt;

&lt;p&gt;Claude will ask permission to configure a &lt;strong&gt;redirect URI&lt;/strong&gt; in your Scalekit environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;I need to add a redirect URI to your Scalekit environment:
http://localhost:3000/api/auth/callback

Allow? [y/n]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type &lt;code&gt;y&lt;/code&gt; and press Enter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt;&lt;br&gt;
The redirect URI is where Scalekit sends users after they authenticate with their IDP. It must be registered for security—preventing malicious sites from receiving your auth callbacks.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5: Add Your Client Secret
&lt;/h3&gt;

&lt;p&gt;Claude will generate a &lt;code&gt;.env.local&lt;/code&gt; file with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SCALEKIT_ENV_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://yourenv.scalekit.com
&lt;span class="nv"&gt;SCALEKIT_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk_live_abc123...
&lt;span class="nv"&gt;SCALEKIT_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open your Scalekit dashboard, navigate to &lt;strong&gt;Settings → API Keys&lt;/strong&gt;, and generate a new Client Secret.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copy it and add to &lt;code&gt;.env.local&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SCALEKIT_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk_secret_xyz789...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security note&lt;/strong&gt;: Never commit this file. Claude should have added it to &lt;code&gt;.gitignore&lt;/code&gt; automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Review Generated Files
&lt;/h3&gt;

&lt;p&gt;Claude will have created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your-app/
├── .env.local                          # Environment configuration
├── app/
│   ├── api/
│   │   └── auth/
│   │       ├── [org]/
│   │       │   └── route.ts           # SSO initiation
│   │       └── callback/
│   │           └── route.ts           # OAuth callback handler
│   ├── login/
│   │   └── page.tsx                   # Login page with email input
│   └── sso-settings/
│       └── page.tsx                   # Admin portal for IDP config
├── lib/
│   └── scalekit.ts                    # Scalekit client initialization
└── middleware.ts                       # Session validation (optional)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key file: &lt;code&gt;lib/scalekit.ts&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ScalekitClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@scalekit-sdk/node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ScalekitClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SCALEKIT_ENV_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SCALEKIT_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SCALEKIT_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key file: &lt;code&gt;app/api/auth/callback/route.ts&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/scalekit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login?error=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login?error=no_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Exchange code for user identity&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;scalekit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticateWithCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/auth/callback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Create session with user data&lt;/span&gt;
    &lt;span class="c1"&gt;// (Implementation depends on your session strategy)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Auth error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login?error=auth_failed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 7: Install Dependencies
&lt;/h3&gt;

&lt;p&gt;Claude should have updated &lt;code&gt;package.json&lt;/code&gt;, but verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="c"&gt;# or npm install, or yarn install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing with the IDP Simulator
&lt;/h2&gt;

&lt;p&gt;You don't need a real enterprise IDP to test this. Scalekit provides a built-in simulator.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start Your Dev Server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to &lt;code&gt;http://localhost:3000&lt;/code&gt;—you should be redirected to &lt;code&gt;/login&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use the Test Organization
&lt;/h3&gt;

&lt;p&gt;In your Scalekit dashboard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Organizations&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Find the &lt;strong&gt;Test Organization&lt;/strong&gt; (created by default)&lt;/li&gt;
&lt;li&gt;Note it's configured with &lt;code&gt;example.com&lt;/code&gt; as an organization domain&lt;/li&gt;
&lt;li&gt;The IDP is set to &lt;strong&gt;IDP Simulator&lt;/strong&gt; (simulates Okta/Microsoft/Google)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Test the Flow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;On the login page, enter: &lt;code&gt;yourname@example.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Continue&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You'll be redirected to the IDP Simulator (looks like a real enterprise login page)&lt;/li&gt;
&lt;li&gt;Enter any name in the form&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Simulate Login&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What happens:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scalekit matches &lt;code&gt;example.com&lt;/code&gt; to the Test Organization&lt;/li&gt;
&lt;li&gt;Redirects to the configured IDP (Simulator)&lt;/li&gt;
&lt;li&gt;Simulator "authenticates" the user&lt;/li&gt;
&lt;li&gt;Redirects back to your app at &lt;code&gt;/api/auth/callback?code=...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Your app exchanges the code for user identity&lt;/li&gt;
&lt;li&gt;Session is created, user is logged in&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Check the Admin Portal
&lt;/h3&gt;

&lt;p&gt;Navigate to &lt;code&gt;http://localhost:3000/sso-settings&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You should see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connected Identity Providers&lt;/strong&gt; section&lt;/li&gt;
&lt;li&gt;Status of each IDP (Enabled/Disabled)&lt;/li&gt;
&lt;li&gt;Available IDPs to configure (Microsoft Entra ID, Okta, Google, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what your enterprise customers will use to self-serve their SSO configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Actually Works
&lt;/h2&gt;

&lt;p&gt;Let's demystify what just happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  The SSO Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. User visits app
   ↓
2. Redirected to /login
   ↓
3. User enters work email: user@acme.com
   ↓
4. App calls Scalekit: "Get SSO URL for acme.com"
   ↓
5. Scalekit checks: Is there an org with domain acme.com?
   ↓
6. If yes: Return IDP login URL for that org
   ↓
7. User redirected to their company's IDP (Okta/Microsoft/etc)
   ↓
8. User authenticates at IDP
   ↓
9. IDP redirects back to: yourapp.com/api/auth/callback?code=xyz
   ↓
10. App exchanges code with Scalekit for user identity
    ↓
11. Scalekit returns: { email, name, organization, ... }
    ↓
12. App creates session, user is logged in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Organization-Based Routing
&lt;/h3&gt;

&lt;p&gt;The key insight: &lt;strong&gt;Scalekit routes authentication based on email domain&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a user enters &lt;code&gt;jane@acme.com&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scalekit looks up which organization owns &lt;code&gt;acme.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Retrieves that organization's configured IDP&lt;/li&gt;
&lt;li&gt;Returns the appropriate login URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why your B2B customers can each use different IDPs—routing happens automatically per organization.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Admin Portal
&lt;/h3&gt;

&lt;p&gt;The self-service portal uses Scalekit's management APIs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;List Organizations&lt;/strong&gt;: Show the customer their organization details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure Connections&lt;/strong&gt;: Let them set up SAML/OIDC with their IDP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Connections&lt;/strong&gt;: Validate the setup before going live&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manage Domains&lt;/strong&gt;: Add/remove email domains for their org&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All without your engineering team touching anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Considerations Built In
&lt;/h3&gt;

&lt;p&gt;The generated code includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PKCE (Proof Key for Code Exchange)&lt;/strong&gt;: Protects against authorization code interception&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State Parameter&lt;/strong&gt;: Prevents CSRF attacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Validation&lt;/strong&gt;: Ensures codes haven't been tampered with&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment-Based Config&lt;/strong&gt;: Secrets never in version control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS Enforcement&lt;/strong&gt;: Redirect URIs must use HTTPS in production&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Environment Variables
&lt;/h3&gt;

&lt;p&gt;For production, set these in your hosting platform (Vercel, AWS, etc.):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SCALEKIT_ENV_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://yourenv.scalekit.com
&lt;span class="nv"&gt;SCALEKIT_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk_live_...
&lt;span class="nv"&gt;SCALEKIT_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk_secret_...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Update Redirect URIs
&lt;/h3&gt;

&lt;p&gt;In your Scalekit dashboard, add production callback URLs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://yourdomain.com/api/auth/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Session Management
&lt;/h3&gt;

&lt;p&gt;Claude generates a basic session setup. For production, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session Storage&lt;/strong&gt;: Redis, database, or encrypted cookies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session Lifetime&lt;/strong&gt;: How long until re-authentication required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Refresh&lt;/strong&gt;: For long-lived sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-Device&lt;/strong&gt;: Handling concurrent sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Error Handling
&lt;/h3&gt;

&lt;p&gt;Add user-friendly error messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/login/page.tsx&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errorMessages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;no_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authentication failed. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;auth_failed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Could not verify your identity.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;invalid_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please use your work email address.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;no_org&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your organization has not set up SSO yet.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Monitoring
&lt;/h3&gt;

&lt;p&gt;Track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Failed authentication attempts&lt;/li&gt;
&lt;li&gt;Organization-wise SSO usage&lt;/li&gt;
&lt;li&gt;IDP connection health&lt;/li&gt;
&lt;li&gt;Session creation/destruction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scalekit provides webhooks for these events.&lt;/p&gt;




&lt;h2&gt;
  
  
  Alternative: Using Other AI Coding Agents
&lt;/h2&gt;

&lt;p&gt;Scalekit's Auth Stack works with 40+ AI coding agents, not just Claude Code.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Copilot CLI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install marketplace&lt;/span&gt;
copilot plugin marketplace add scalekit-inc/github-copilot-authstack

&lt;span class="c"&gt;# Install plugin&lt;/span&gt;
copilot plugin &lt;span class="nb"&gt;install &lt;/span&gt;modular-sso@scalekit-auth-stack

&lt;span class="c"&gt;# Generate implementation&lt;/span&gt;
copilot &lt;span class="s2"&gt;"Add Scalekit SSO to my app with admin portal for customer IDP configuration"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cursor
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Option&lt;/strong&gt;: Use Claude Code marketplace (works now)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In Claude Code REPL&lt;/span&gt;
/plugin marketplace add scalekit-inc/claude-code-authstack

&lt;span class="c"&gt;# Cursor automatically picks up Claude Code plugins&lt;/span&gt;
&lt;span class="c"&gt;# Open Cursor → Settings → Plugins → Enable Modular SSO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then use Cursor's chat with &lt;code&gt;Cmd+L&lt;/code&gt; / &lt;code&gt;Ctrl+L&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add Scalekit SSO with customer admin portal for IDP configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Any Agent via Vercel Skills
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Interactive install&lt;/span&gt;
npx skills add scalekit-inc/skills

&lt;span class="c"&gt;# Or list and pick&lt;/span&gt;
npx skills add scalekit-inc/skills &lt;span class="nt"&gt;--list&lt;/span&gt;

&lt;span class="c"&gt;# Install specific skill&lt;/span&gt;
npx skills add scalekit-inc/skills &lt;span class="nt"&gt;--skill&lt;/span&gt; modular-sso

&lt;span class="c"&gt;# Global install for all projects&lt;/span&gt;
npx skills add scalekit-inc/skills &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supported agents: Windsurf, Cline, Gemini CLI, OpenCode, Codex, and 30+ others.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Traditional SSO implementation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read OAuth 2.0 / SAML spec (hours)&lt;/li&gt;
&lt;li&gt;Study IDP-specific quirks (hours)&lt;/li&gt;
&lt;li&gt;Write authorization URL generation (30 min)&lt;/li&gt;
&lt;li&gt;Handle callback and token exchange (1 hour)&lt;/li&gt;
&lt;li&gt;Build session management (2+ hours)&lt;/li&gt;
&lt;li&gt;Create admin UI for IDP config (3+ hours)&lt;/li&gt;
&lt;li&gt;Test with multiple IDPs (hours)&lt;/li&gt;
&lt;li&gt;Debug edge cases (days)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total: 1-2 weeks for an experienced developer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With Claude Code + Scalekit:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install plugin (1 minute)&lt;/li&gt;
&lt;li&gt;Describe what you want (30 seconds)&lt;/li&gt;
&lt;li&gt;Review generated code (5 minutes)&lt;/li&gt;
&lt;li&gt;Test with simulator (2 minutes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total: ~10 minutes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The code quality is production-ready because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The plugin encodes battle-tested patterns from thousands of implementations&lt;/li&gt;
&lt;li&gt;It follows your framework's conventions automatically&lt;/li&gt;
&lt;li&gt;Security best practices are baked in&lt;/li&gt;
&lt;li&gt;Scalekit handles the complex parts (SAML parsing, token validation, IDP integrations)&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Enterprise SSO used to be a feature that delayed product launches. Between understanding OAuth flows, handling SAML complexity, and building customer onboarding portals, it was weeks of work.&lt;/p&gt;

&lt;p&gt;AI coding agents with specialized plugins collapse that timeline to minutes. But this isn't about AI writing code faster—it's about &lt;strong&gt;encoding expert knowledge&lt;/strong&gt; into reusable skills that handle entire feature implementations.&lt;/p&gt;

&lt;p&gt;The Scalekit plugin doesn't just generate boilerplate. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understands your codebase structure&lt;/li&gt;
&lt;li&gt;Queries your live environment&lt;/li&gt;
&lt;li&gt;Follows security best practices&lt;/li&gt;
&lt;li&gt;Creates complete, working flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the new paradigm: describe the outcome, let specialized agents handle the implementation details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.scalekit.com" rel="noopener noreferrer"&gt;Scalekit Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://code.claude.com/docs/en/plugins" rel="noopener noreferrer"&gt;Claude Code Plugin Development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/scalekit-inc/claude-code-authstack" rel="noopener noreferrer"&gt;Scalekit Auth Stack GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/scalekit-inc/scalekit-nextjs-example" rel="noopener noreferrer"&gt;Example Implementation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Try it yourself&lt;/strong&gt;: The fastest way to understand this isn't reading—it's doing. Spin up a fresh Next.js app, install the plugin, and watch Claude build your SSO flow in real-time.&lt;/p&gt;

&lt;p&gt;The future of development isn't writing less code. It's describing what you want and having specialized agents that actually understand your domain build it for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Have questions? Join the &lt;a href="https://scalekit-community.slack.com/" rel="noopener noreferrer"&gt;Scalekit's Slack&lt;/a&gt; or the &lt;a href="https://discord.gg/claude-code" rel="noopener noreferrer"&gt;Claude Code community&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sso</category>
      <category>claudecode</category>
      <category>nextjs</category>
      <category>fullstack</category>
    </item>
    <item>
      <title>Multi-Connector OAuth: Meeting Scheduler Agent using Google Calendar, Gmail, Scalekit</title>
      <dc:creator>Saif </dc:creator>
      <pubDate>Thu, 12 Mar 2026 10:46:15 +0000</pubDate>
      <link>https://dev.to/saif_shines/multi-connector-oauth-meeting-scheduler-agent-using-google-calendar-gmail-scalekit-89e</link>
      <guid>https://dev.to/saif_shines/multi-connector-oauth-meeting-scheduler-agent-using-google-calendar-gmail-scalekit-89e</guid>
      <description>&lt;p&gt;Scheduling a meeting takes three API calls: check availability, create the event, and draft the confirmation. &lt;/p&gt;

&lt;p&gt;In a human workflow, those three steps share a session. &lt;/p&gt;

&lt;p&gt;In an agent, &lt;a href="https://docs.scalekit.com/agent-auth/tools/execute/" rel="noopener noreferrer"&gt;every tool calling&lt;/a&gt; crosses an independent OAuth boundary - and each boundary requires its own token, its own refresh logic, its own error handling.&lt;/p&gt;

&lt;p&gt;This is the part of agentic development that doesn't show up in agent demos. The demo shows the agent booking a meeting. The production reality is that you've written token-fetching code three times, your refresh logic has a race condition you'll discover at 2 am, and your error messages could be indistinguishable between "token expired" and "wrong scope."&lt;/p&gt;

&lt;p&gt;This &lt;em&gt;cookbook&lt;/em&gt; solves that by &lt;a href="https://scalekit.webflow.io/agent-auth" rel="noopener noreferrer"&gt;&lt;strong&gt;delegating OAuth lifecycle management to Scalekit&lt;/strong&gt;&lt;/a&gt; entirely, so the agent code is just workflow logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What You're Actually Building&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A Python agent that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authorizes against &lt;em&gt;Google Calendar&lt;/em&gt; and &lt;em&gt;Gmail&lt;/em&gt; independently
&lt;/li&gt;
&lt;li&gt;Queries the &lt;em&gt;freeBusy&lt;/em&gt; endpoint to find open slots
&lt;/li&gt;
&lt;li&gt;Creates a calendar event with attendee(s)
&lt;/li&gt;
&lt;li&gt;Drafts (not sends - more on that), a follow-up email with the event link&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete source code is in &lt;code&gt;python/meeting_scheduler_agent.py&lt;/code&gt; in the &lt;a href="https://github.com/scalekit-developers/agent-auth-examples/blob/main/python/meeting_scheduler_agent.py" rel="noopener noreferrer"&gt;agent-auth-examples repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6iwswkfwbdxuu4e1cnra.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6iwswkfwbdxuu4e1cnra.jpg" alt="Flowchart of Meeting Scheduler Agent" width="800" height="977"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why OAuth Gets Complicated in Multi-Step Agents&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Single-API agents&lt;/strong&gt; are manageable: You fetch a token, store it, and refresh it when it expires. It's boilerplate, but contained boilerplate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-API agents&lt;/strong&gt; have a compounding problem. Google Calendar and Gmail use separate OAuth scopes and issue separate access tokens. Your agent must manage both independently, and the failure modes interact in non-obvious ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  The four OAuth problems that make this harder than it looks:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One token per connector.&lt;/strong&gt; Calendar and Gmail have different scope requirements and different token lifetimes. You can't share a token between them, and the refresh logic for each has to be independent.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-run authorization is blocking.&lt;/strong&gt; If the user hasn't authorized a connector yet, your agent can't proceed until they complete the OAuth flow in a browser. That blocking behavior needs to be handled explicitly - not just as an error case, but as a designed state in the workflow.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token expiry is silent.&lt;/strong&gt; A token that worked yesterday fails today, and the failure looks identical to a permissions error. Without explicit expiry tracking, your agent degrades silently.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chaining tool outputs is fragile.&lt;/strong&gt; The Calendar API returns an event link. That link needs to appear in the Gmail draft. If the Calendar call succeeds but the Gmail call fails, you have an orphaned calendar event with no follow-up email. The compensation logic is real work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't edge cases. They're the normal operating conditions of any agent that touches more than one external system.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Architecture: Delegated OAuth Lifecycle&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Scalekit exposes a &lt;code&gt;connected_accounts&lt;/code&gt; abstraction: a mapping from a user identifier to an authorized OAuth session per connector. When your agent calls &lt;code&gt;get_or_create_connected_account&lt;/code&gt;, Scalekit either returns an existing active session with a valid token or creates a new one and returns an authorization URL. After the user authorizes, &lt;code&gt;get_connected_account&lt;/code&gt; returns the token. From that point, Scalekit handles refresh automatically - including rotation, expiry detection, and re-issuance.&lt;/p&gt;

&lt;p&gt;The consequence: your agent's authorization step is a single function regardless of which connector you're targeting. Calendar, Gmail, Slack, Jira - the pattern is identical. The connector-specific OAuth complexity lives in Scalekit's infrastructure, not in your agent code.&lt;/p&gt;

&lt;p&gt;This is the same principle behind why enterprises centralize OAuth in a gateway rather than distributing token management across individual services. When credentials live in one place, you get consistent refresh handling, consistent revocation, and a coherent audit trail. For an agent, Scalekit is that centralized layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Setup&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file at the project root with your Scalekit credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;SCALEKIT_ENVIRONMENT_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://your-env.scalekit.com&lt;/span&gt;
&lt;span class="py"&gt;SCALEKIT_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-client-id&lt;/span&gt;
&lt;span class="py"&gt;SCALEKIT_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;your-client-secret&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install dependencies:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pip install scalekit-sdk python-dotenv requests&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In the Scalekit Dashboard, create 2 connections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;googlecalendar&lt;/code&gt; - &lt;a href="https://docs.scalekit.com/reference/agent-connectors/googlecalendar/" rel="noopener noreferrer"&gt;Google Calendar OAuth connection&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gmail&lt;/code&gt; - &lt;a href="https://docs.scalekit.com/reference/agent-connectors/gmail/" rel="noopener noreferrer"&gt;Gmail OAuth connection&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The connection names must match exactly what you pass to &lt;code&gt;authorize()&lt;/code&gt;. A mismatch returns an error, not a helpful message.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Code for Building Meeting Scheduler Agent using Google Calendar, Gmail, and Scalekit&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Initialize the Scalekit client&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.mime.text&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MIMEText&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scalekit&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ScalekitClient&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Never hard-code credentials - they would be exposed in source control
# and CI logs. Pull them from environment variables instead.
&lt;/span&gt;&lt;span class="n"&gt;scalekit_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ScalekitClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;environment_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCALEKIT_ENVIRONMENT_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCALEKIT_CLIENT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCALEKIT_CLIENT_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;actions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scalekit_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;

&lt;span class="c1"&gt;# Replace with a real user identifier from your application's session
&lt;/span&gt;&lt;span class="n"&gt;USER_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;ATTENDEE_EMAIL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attendee@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;MEETING_TITLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Quick Sync&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;DURATION_MINUTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;span class="n"&gt;SEARCH_DAYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;WORK_START_HOUR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;   &lt;span class="c1"&gt;# UTC
&lt;/span&gt;&lt;span class="n"&gt;WORK_END_HOUR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;    &lt;span class="c1"&gt;# UTC
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scalekit_client.actions&lt;/code&gt; is the entry point for all connected-account operations. Initialize it once and pass &lt;code&gt;actions&lt;/code&gt; to the functions below.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Authorize each connector&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The &lt;em&gt;authorize&lt;/em&gt; function handles the first-run prompt and returns a valid access token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Ensure the user has an active connected account and return its access token.

    On first run, this prints an authorization URL and waits for the user
    to complete the browser OAuth flow before continuing.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_or_create_connected_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;USER_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;auth_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_authorization_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;USER_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Open this link to authorize &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;auth_link&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Press Enter after completing authorization in your browser...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_connected_account&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;USER_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authorization_details&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oauth_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call this once per connector before any API calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;calendar_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;googlecalendar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gmail_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gmail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the first successful authorization, &lt;code&gt;get_or_create_connected_account&lt;/code&gt; returns &lt;code&gt;status == "active"&lt;/code&gt; on subsequent runs and the &lt;code&gt;if&lt;/code&gt; block is skipped. Scalekit refreshes expired tokens automatically - you never write refresh logic.&lt;/p&gt;

&lt;p&gt;The critical property: this function is identical regardless of which connector you're authorizing. Swap &lt;code&gt;"googlecalendar"&lt;/code&gt; for &lt;code&gt;"slack"&lt;/code&gt; and the same code handles Slack's OAuth flow, token storage, and refresh lifecycle. The connector-specific behavior lives in Scalekit's configuration, not in your agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Query calendar availability&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;With a valid Calendar token, query the freeBusy endpoint to get the user’s busy intervals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_busy_slots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetch busy intervals for the user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s primary calendar.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;window_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SEARCH_DAYS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.googleapis.com/calendar/v3/freeBusy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeMin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeMax&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;window_end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;primary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calendars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;primary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;busy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;raise_for_status()&lt;/code&gt; converts 4xx and 5xx responses into exceptions. Without it, a 403 from a missing scope fails silently and returns empty busy slots - which means your agent will try to book into a slot that doesn't exist. Always raising an error.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;busy&lt;/em&gt; list contains &lt;code&gt;{"start": "...", "end": "..."}&lt;/code&gt; dicts in ISO 8601 format.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Find the first open slot&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Walk forward in one-hour increments from now and return the first candidate that falls within working hours and does not overlap a busy interval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_free_slot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;busy_slots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the first open one-hour slot during working hours in UTC.

    Returns None if no slot is available in the search window.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Round up to the next whole hour so the candidate is always in the future
&lt;/span&gt;    &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;microsecond&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;window_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SEARCH_DAYS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;window_end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;slot_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DURATION_MINUTES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;WORK_START_HOUR&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;WORK_END_HOUR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;overlap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;slot_end&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;busy_slots&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slot_end&lt;/span&gt;

        &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a deliberate first-draft implementation: one-hour granularity, UTC-only, primary calendar. The limits are real and addressed in the production notes below - but starting simple makes the logic auditable. You can see exactly what the agent will do before it does it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Create the calendar event&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Post the event to the Google Calendar API and return its HTML link, which you’ll include in the email draft:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create a calendar event and return its HTML link.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.googleapis.com/calendar/v3/calendars/primary/events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MEETING_TITLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Scheduled by agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dateTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeZone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dateTime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeZone&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attendees&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ATTENDEE_EMAIL&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;htmlLink&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;htmlLink&lt;/code&gt; in the response is the calendar event URL. Google sends an invitation to each attendee automatically - the draft in the next step is a separate follow-up, not a duplicate of the invitation.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Draft the confirmation email&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_draft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create a Gmail draft with the meeting details.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hi,&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ve scheduled &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MEETING_TITLE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; for &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%A, %B %d at %H&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="n"&gt;UTC&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DURATION_MINUTES&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; min).&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Calendar link: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event_link&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Looking forward to it!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MIMEText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ATTENDEE_EMAIL&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invitation: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MEETING_TITLE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Gmail's API requires the raw RFC 2822 message encoded as URL-safe base64
&lt;/span&gt;    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_bytes&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://gmail.googleapis.com/gmail/v1/users/me/drafts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Draft created in Gmail.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a draft, not a sent message. The user reviews it before sending. For an agent taking actions on behalf of a user, draft-then-review is the right default - it keeps a human in the loop for outbound communication while still eliminating the scheduling work. When you're confident in the agent's output quality, switching to &lt;code&gt;/messages/send&lt;/code&gt; is a one-line change.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Wire it together&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorizing Google Calendar...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;calendar_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;googlecalendar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorizing Gmail...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;gmail_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gmail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Checking calendar availability...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;busy_slots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_busy_slots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calendar_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;slot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_free_slot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;busy_slots&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No free slot found in the next &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SEARCH_DAYS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; days.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Found slot: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%A %B %d, %H&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; UTC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Creating calendar event...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;event_link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calendar_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Event created: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event_link&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Creating Gmail draft...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;create_draft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gmail_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event_link&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Testing Your Agent&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Run the agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;python meeting_scheduler_agent.py
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On first run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Authorizing Google Calendar...

Open this link to authorize googlecalendar:
https://accounts.google.com/o/oauth2/auth?...

Press Enter after completing authorization in your browser...

Authorizing Gmail...

Open this link to authorize gmail:
https://accounts.google.com/o/oauth2/auth?...

Press Enter after completing authorization in your browser...

Checking calendar availability...
Found slot: Wednesday March 11, 10:00 UTC
Creating calendar event...
Event created: https://calendar.google.com/calendar/event?eid=...
Creating Gmail draft...
Draft created in Gmail.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On subsequent runs, authorization prompts are skipped. The agent goes straight to availability checking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open Google Calendar&lt;/strong&gt; - the event should appear on the chosen date
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Gmail Drafts&lt;/strong&gt; - the draft should contain the event link&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Common Mistakes while Building Your Agent&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connection name mismatch.&lt;/strong&gt;&lt;br&gt;
If you name the Scalekit connection &lt;code&gt;google-calendar&lt;/code&gt; instead of &lt;code&gt;googlecalendar&lt;/code&gt;, &lt;code&gt;get_or_create_connected_account&lt;/code&gt; returns an error. The name in the Dashboard must exactly match the string you pass to &lt;code&gt;authorize()&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Missing OAuth scopes.&lt;/strong&gt;&lt;br&gt;
A 403 when calling the Calendar or Gmail API means the OAuth app in Google Cloud Console is missing required scopes. Calendar needs &lt;code&gt;https://www.googleapis.com/auth/calendar&lt;/code&gt;, Gmail needs &lt;a href="https://www.googleapis.com/auth/gmail.compose" rel="noopener noreferrer"&gt;&lt;code&gt;https://www.googleapis.com/auth/gmail.compose&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;raise_for_status()&lt;/code&gt; swallowing context.&lt;/strong&gt;&lt;br&gt;
The default exception message from requests truncates the response body. In development, add &lt;code&gt;print(response.text)&lt;/code&gt; before &lt;code&gt;raise_for_status()&lt;/code&gt; to see the full error from Google.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UTC times without timezone info.&lt;/strong&gt;&lt;br&gt;
Passing a naive datetime (without &lt;code&gt;timezone.utc&lt;/code&gt;) to &lt;code&gt;isoformat()&lt;/code&gt; produces a string without a Z suffix. Google Calendar rejects this with a 400. Always construct datetimes with &lt;code&gt;timezone.utc&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;USER_ID&lt;/code&gt; not matching your session.&lt;/strong&gt;&lt;br&gt;
The script uses a hardcoded &lt;code&gt;"user_123"&lt;/code&gt;. In production, replace this with the actual user ID from your application's session. A mismatch means the connected account query returns the wrong user's tokens - and you'll get valid tokens for the wrong person.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Production Notes&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Timezone handling.&lt;/strong&gt;&lt;br&gt;
The working-hours check is UTC-only. In production, convert the user's local timezone and the attendee's timezone before searching. The &lt;code&gt;zoneinfo&lt;/code&gt; module (Python 3.9+) handles this without third-party dependencies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Slot granularity.&lt;/strong&gt;&lt;br&gt;
One-hour increments miss 30- and 15-minute openings. Use the busy intervals directly to calculate gaps between events, then filter by minimum duration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multiple calendars.&lt;/strong&gt;&lt;br&gt;
The freeBusy query checks only &lt;code&gt;primary&lt;/code&gt;. Users who split work and personal calendars will show false availability. Expand the &lt;code&gt;items&lt;/code&gt; list to include all calendars they've shared access to.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Error recovery.&lt;/strong&gt;&lt;br&gt;
If &lt;code&gt;create_event&lt;/code&gt; succeeds but &lt;code&gt;create_draft&lt;/code&gt; fails, you have an orphaned event with no follow-up email. Wrap the two calls in a compensation pattern: store the event ID and delete it if draft creation fails.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Credentials in multi-tenant deployments.&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;USER_ID&lt;/code&gt; here is a single hardcoded identifier. In a real multi-tenant system, each user needs their own connected account - and those accounts need to be isolated from each other. Scalekit's connected account model handles this by design: &lt;code&gt;get_or_create_connected_account(connector, user_id)&lt;/code&gt; creates a separate authorized session per user, per connector. No token sharing, no cross-user access.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rate limits.&lt;/strong&gt;&lt;br&gt;
Google Calendar and Gmail have per-user quotas. If your agent runs frequently for the same user, add exponential backoff around the &lt;code&gt;requests.post&lt;/code&gt; calls.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What This Pattern Generalizes To&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;authorize()&lt;/code&gt; function in this recipe is connector-agnostic. The same four lines handle authorization for any &lt;a href="https://docs.scalekit.com/reference/agent-connectors/airtable/" rel="noopener noreferrer"&gt;Scalekit-supported connector&lt;/a&gt;: swap &lt;code&gt;"googlecalendar"&lt;/code&gt; for &lt;code&gt;"slack"&lt;/code&gt;, &lt;code&gt;"notion"&lt;/code&gt;, or &lt;code&gt;"jira"&lt;/code&gt; and the OAuth lifecycle - initial authorization, token storage, automatic refresh, revocation handling - is managed identically.&lt;/p&gt;

&lt;p&gt;This matters as agents get more capable. An agent that starts by booking meetings will eventually need to create Notion docs summarizing those meetings, post Slack updates to the relevant channel, and open Jira tickets for action items. Each of those integrations would traditionally mean writing OAuth boilerplate again. With the connected account model, it means adding one &lt;code&gt;authorize()&lt;/code&gt; call per connector and focusing the rest of the code on the workflow.&lt;/p&gt;

&lt;p&gt;The engineering leverage is the point. OAuth is infrastructure. Agents are products. The less time you spend on the former, the more you can invest in the latter.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Next Steps&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add natural language input&lt;/strong&gt; - Replace hardcoded &lt;code&gt;ATTENDEE_EMAIL&lt;/code&gt;, &lt;code&gt;MEETING_TITLE&lt;/code&gt;, and &lt;code&gt;DURATION_MINUTES&lt;/code&gt; with parameters parsed from an LLM tool call
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build the JavaScript equivalent&lt;/strong&gt; - The agent-auth-examples repo includes a JavaScript track with the same pattern
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle re-authorization&lt;/strong&gt; - If a user revokes access, &lt;code&gt;get_connected_account&lt;/code&gt; returns an inactive account; add a re-authorization path instead of crashing
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add more connectors&lt;/strong&gt; - The &lt;code&gt;authorize()&lt;/code&gt; pattern works for Slack, Notion, Jira - swap the connector name and replace the Google API calls with the target service's API
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review the &lt;a href="https://docs.scalekit.com/agent-auth/overview/" rel="noopener noreferrer"&gt;Scalekit agent auth quickstart&lt;/a&gt;&lt;/strong&gt; - For a broader overview of the connected-accounts model and how it handles multi-tenant deployments&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>authentication</category>
      <category>oauth</category>
    </item>
    <item>
      <title>Automate Slack workflows with LangGraph</title>
      <dc:creator>Saif </dc:creator>
      <pubDate>Wed, 05 Nov 2025 12:38:08 +0000</pubDate>
      <link>https://dev.to/saif_shines/automate-slack-workflows-with-langgraph-4e62</link>
      <guid>https://dev.to/saif_shines/automate-slack-workflows-with-langgraph-4e62</guid>
      <description>&lt;p&gt;&lt;em&gt;How to turn messy #product-support Slack messages into auditable tasks&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We’ve been experimenting with AI workflows (like most of the dev world lately — and of course, using Scalekit for the auth layer. Full disclosure: I work for Scalekit. Our offering is always free for devs tinkering with stuff).&lt;/p&gt;

&lt;p&gt;For our first one, we tried automating Slack workflows with LangGraph and Scalekit. Here’s what we built 👇&lt;/p&gt;

&lt;p&gt;🎫 Auto-create GitHub issues or Zendesk tickets directly from Slack messages&lt;br&gt;
🔐 Simplify OAuth, token management, and API calls using Scalekit&lt;br&gt;
🔁 Use LangGraph to design modular, observable workflows&lt;/p&gt;
&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Slack is great for fast communication, but terrible as a system of record. Important messages about bugs or support requests get buried almost instantly.&lt;/p&gt;

&lt;p&gt;So we built an agent that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Watches specific Slack channels&lt;/li&gt;
&lt;li&gt;Classifies each message (bug, support, ignore)&lt;/li&gt;
&lt;li&gt;Creates a GitHub issue or Zendesk ticket based on that classification&lt;/li&gt;
&lt;li&gt;Posts a confirmation reply in Slack&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Why LangGraph + Scalekit
&lt;/h2&gt;

&lt;p&gt;Scalekit takes care of all the auth plumbing, so no need to manually store Slack or GitHub tokens, refresh them, or juggle SDKs. You just call &lt;code&gt;execute_tool&lt;/code&gt; with the right parameters, and Scalekit securely handles the rest.&lt;/p&gt;

&lt;p&gt;LangGraph gives you a clean, node-based workflow where each step is independent and observable. Instead of a big procedural script, you have a graph that shows exactly how data moves, which is perfect when you want to debug or extend it later.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Here’s the basic flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# triage.py
from routing import classify_route
from actions import create_github_issue, post_slack_confirmation, handle_zendesk_ticket

def triage_message(slack_user, channel, ts, text):
    route = classify_route(text)

    if route == "github":
        issue = create_github_issue(slack_user, channel, ts, text)
        post_slack_confirmation(slack_user, channel, ts, f"Created GitHub issue → {issue['url']}")
    elif route == "zendesk":
        handle_zendesk_ticket(slack_user, channel, ts)
    else:
        # ignored message
        pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each message gets classified, turned into a structured task, and acknowledged back in Slack — no human in the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving to LangGraph
&lt;/h2&gt;

&lt;p&gt;Once we had the script working, we turned it into a LangGraph workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from langgraph import StateGraph
graph = (
    StateGraph()
      .add("parse", parse_node)
      .add("classify", classify_node)
      .add("execute", execute_node)
      .add("confirm", confirm_node)
      .edge("parse", "classify")
      .edge("classify", "execute")
      .edge("execute", "confirm")
      .entry("parse")
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each node handles one job, and you can easily replace or expand them later, like plugging in an LLM classifier, adding retries, or branching into multiple integrations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is this useful?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Keeps your Slack channels clean and actionable&lt;/li&gt;
&lt;li&gt;Saves hours of manual triage work&lt;/li&gt;
&lt;li&gt;Makes tasks traceable and auditable&lt;/li&gt;
&lt;li&gt;Easy to extend to Notion, Jira, or any API Scalekit supports&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Connect Slack + GitHub via Scalekit&lt;/li&gt;
&lt;li&gt;Drop in the triage script&lt;/li&gt;
&lt;li&gt;Run your loop or use webhooks for real-time updates&lt;/li&gt;
&lt;li&gt;Watch your Slack messages turn into issues and tickets automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you'd like a more comprehensive tutorial, head to this blog (It has working code, screenshots, and links to Git repos).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.scalekit.com/blog/automate-slack-workflows-langgraph-scalekit" rel="noopener noreferrer"&gt;Automating Slack workflows with LangGraph and Scalekit&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

</description>
      <category>langchain</category>
      <category>aiagents</category>
      <category>webdev</category>
      <category>oauth</category>
    </item>
    <item>
      <title>Building passwordless authentication in React</title>
      <dc:creator>Saif </dc:creator>
      <pubDate>Wed, 10 Sep 2025 10:21:14 +0000</pubDate>
      <link>https://dev.to/saif_shines/building-passwordless-authentication-in-react-1cm4</link>
      <guid>https://dev.to/saif_shines/building-passwordless-authentication-in-react-1cm4</guid>
      <description>&lt;p&gt;Imagine this: your SaaS product has grown quickly, and users are constantly forgetting their passwords. Support tickets pile up, password resets consume engineering time, and the login flow feels more like a barrier than an entry point. Your team decides to modernize by going passwordless, replacing passwords with short codes or magic links. Simple in theory, but the implementation? Not so much.&lt;/p&gt;

&lt;p&gt;If you're building with React, here's what you have to tackle: Multiple forms, async calls colliding with redirects, state that doesn’t survive reloads, and code that breaks when users switch devices. Suddenly, what seemed like a straightforward UX improvement feels like a maintenance nightmare.&lt;/p&gt;

&lt;p&gt;The real problem isn’t the idea of magic links or OTPs — those are well understood. The problem lies in managing state and security guarantees in a way that keeps your React code clean and your backend trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a passwordless flow actually looks like
&lt;/h2&gt;

&lt;p&gt;At a high level, passwordless authentication follows a simple sequence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collect an email from the user.&lt;/li&gt;
&lt;li&gt;Issue a credential — either a one-time passcode or a magic link.&lt;/li&gt;
&lt;li&gt;Verify the code or token when the user comes back.&lt;/li&gt;
&lt;li&gt;Mark the session as authenticated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s it. No password resets, no strength meters, no secret storage. Just short-lived credentials that expire quickly and can’t be reused.&lt;/p&gt;

&lt;p&gt;The tricky part is not the flow itself, but handling all the edge cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1. Users clicking a link twice.&lt;/li&gt;
&lt;li&gt;2. Codes expiring while the form is still open.&lt;/li&gt;
&lt;li&gt;3. Redirects landing in a different tab.&lt;/li&gt;
&lt;li&gt;4. Sessions being dropped after a page reload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without a structured approach, developers end up duct-taping state machines across components and hoping everything holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why security has to be server-first
&lt;/h2&gt;

&lt;p&gt;Even if you nail the React side, passwordless only works if the server enforces the rules. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generating cryptographically signed tokens.&lt;/li&gt;
&lt;li&gt;Enforcing expiry windows.&lt;/li&gt;
&lt;li&gt;Preventing replay across devices.&lt;/li&gt;
&lt;li&gt;Making sure resends invalidate old codes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn’t something you want scattered through frontend code. The backend should own these guarantees so the client can stay focused on state and UX.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Scalekit simplifies it
&lt;/h2&gt;

&lt;p&gt;Scalekit takes on the server-side heavy lifting. It issues and validates short-lived tokens, handles resends safely, and ensures credentials are bound to the right context. You never expose secrets to the client, and you don’t need to reinvent cryptography or expiry logic.&lt;/p&gt;

&lt;p&gt;On the React side, you just consume that flow declaratively. Using Scalekit’s SDK, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collect an email and request a magic link or OTP.&lt;/li&gt;
&lt;li&gt;Handle redirects automatically and verify tokens.&lt;/li&gt;
&lt;li&gt;Track session state across reloads.&lt;/li&gt;
&lt;li&gt;Show clear UI states for sending, verifying, success, or error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of juggling timers and redirects by hand, your components stay focused: one handles email input, another handles code entry, and global context keeps track of whether the user is authenticated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Passwordless authentication removes one of the biggest sources of friction for users, but only if it’s implemented with both security and developer sanity in mind. By splitting responsibilities — React for state and UX, Scalekit for token security — you get a system that’s secure by default and reusable across projects.&lt;/p&gt;

&lt;p&gt;If you’re ready to ditch fragile password resets and move to something users actually enjoy, check out the full guide and working example here: &lt;a href="https://www.scalekit.com/blog/passwordless-authentication-react-js" rel="noopener noreferrer"&gt;Passwordless authentication in React with Scalekit&lt;/a&gt;&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>react</category>
      <category>reactjsdevelopment</category>
    </item>
    <item>
      <title>Passwordless logins with magic links using Next.js 15 &amp; Scalekit</title>
      <dc:creator>Saif </dc:creator>
      <pubDate>Tue, 02 Sep 2025 17:00:18 +0000</pubDate>
      <link>https://dev.to/saif_shines/passwordless-logins-with-magic-links-using-nextjs-15-scalekit-1i0i</link>
      <guid>https://dev.to/saif_shines/passwordless-logins-with-magic-links-using-nextjs-15-scalekit-1i0i</guid>
      <description>&lt;p&gt;Passwords are messy. Users forget them, reset flows break, and security teams keep telling us to add more rules (uppercase, symbols, no reuse). For developers, this means complexity. For users, it means frustration.&lt;/p&gt;

&lt;p&gt;There’s a cleaner way: magic links.&lt;/p&gt;

&lt;p&gt;With Scalekit, you can implement passwordless login in Next.js 15 using nothing more than API routes and middleware. Let’s see how.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why magic links?
&lt;/h2&gt;

&lt;p&gt;At Scalekit, we’ve seen teams run into the same problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Password resets flood support.&lt;/li&gt;
&lt;li&gt;SMS-based codes lag or fail at scale.&lt;/li&gt;
&lt;li&gt;Session state spreads across multiple services, making incidents hard to debug.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Magic links fix this by collapsing login into three simple server-side steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Issue a link&lt;/li&gt;
&lt;li&gt;Verify it&lt;/li&gt;
&lt;li&gt;Create a session&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s it. No passwords, no SMS gateways, no half-baked tokens in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Sending a link
&lt;/h2&gt;

&lt;p&gt;In Next.js, create an API route &lt;code&gt;/api/send-magic-link&lt;/code&gt; that accepts an email and calls Scalekit to generate a passwordless request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// /api/send-magic-link/route.ts
export async function POST(req: NextRequest) {
  const { email } = await req.json()
  const resp = await client.passwordless.createAuthRequest({
    email,
    passwordlessType: 'MAGIC_LINK',
    expiresIn: 600,
  })

  const res = NextResponse.json({ ok: true })
  res.cookies.set('sk_auth_request_id', resp.authRequestId, { httpOnly: true, secure: true })
  return res
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cookie (&lt;code&gt;sk_auth_request_id&lt;/code&gt;) ties the link back to this origin so clients can’t fake it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Verifying
&lt;/h2&gt;

&lt;p&gt;When the user clicks the link, your &lt;code&gt;/api/verify-magic-link&lt;/code&gt; route checks the token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export async function POST(req: NextRequest) {
  const { link_token, auth_request_id } = await req.json()
  const result = await client.passwordless.verifyAuthRequest({ linkToken: link_token, authRequestId: auth_request_id })
  return NextResponse.json({ email: result.email })
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: creating a session
&lt;/h2&gt;

&lt;p&gt;Finally, issue a short-lived JWT stored in an &lt;code&gt;HttpOnly&lt;/code&gt; cookie:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const token = jwt.sign({ email }, process.env.SESSION_JWT_SECRET, { expiresIn: '30m' })
res.cookies.set('sk_session', token, { 
httpOnly: true, 
secure: true, 
sameSite: 'lax' 
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Middleware can now enforce that any protected route requires a valid JWT before it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Server-first&lt;/strong&gt;: all sensitive logic (link creation, verification, session minting) happens in the backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client-agnostic&lt;/strong&gt;: the same API works for web apps, mobile apps, even CLI tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observable&lt;/strong&gt;: logs tell you who requested, who verified, and when the session was issued.&lt;/p&gt;

&lt;p&gt;This design trims moving parts and makes login flows something you can trust and monitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Full guide
&lt;/h2&gt;

&lt;p&gt;This post only scratches the surface. The full tutorial covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate limiting (stop bots from spamming send/verify).&lt;/li&gt;
&lt;li&gt;Security headers (CSP, HSTS, etc).&lt;/li&gt;
&lt;li&gt;Structured logging with correlation IDs.&lt;/li&gt;
&lt;li&gt;Redis/SQL persistence for production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;a href="https://www.scalekit.com/blog/backend-magic-link-implementation-next-js-15" rel="noopener noreferrer"&gt;Read the complete step-by-step guide here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;Have you implemented passwordless login in your projects? Magic links, OTPs, or something else?&lt;/p&gt;

&lt;p&gt;Share your setup, lessons, or gotchas in the comments: Other devs (and future you) will thank you.&lt;/p&gt;

</description>
      <category>authentication</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to safely let AI agents act on behalf of users</title>
      <dc:creator>Saif </dc:creator>
      <pubDate>Tue, 26 Aug 2025 15:54:09 +0000</pubDate>
      <link>https://dev.to/saif_shines/how-to-safely-let-ai-agents-act-on-behalf-of-users-4ngh</link>
      <guid>https://dev.to/saif_shines/how-to-safely-let-ai-agents-act-on-behalf-of-users-4ngh</guid>
      <description>&lt;p&gt;When you introduce AI agents into production systems, they don’t just run in isolation. They often act on behalf of real users—querying dashboards, triggering deploys, posting comments, or raising incidents.&lt;/p&gt;

&lt;p&gt;The problem? Traditional OAuth and identity systems were built for single-user sessions, not autonomous agents. If you let agents impersonate users directly, you end up with over-permissioned, untraceable access.&lt;/p&gt;

&lt;p&gt;That’s where On-Behalf-Of (OBO) authentication comes in. It creates a secure way for agents to operate with scoped, revocable permissions, while maintaining a clear audit trail that says exactly who acted, on whose behalf, and within which scopes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The challenge: Delegation in AI workflows
&lt;/h2&gt;

&lt;p&gt;Imagine your DevOps team builds an AI agent called InfraBot. Its job is to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query metrics from Datadog&lt;/li&gt;
&lt;li&gt;File alerts in PagerDuty&lt;/li&gt;
&lt;li&gt;Post comments on GitHub pull requests&lt;/li&gt;
&lt;li&gt;Trigger rollbacks in ArgoCD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;InfraBot should act only with the permissions of the engineer who initiated the action. But today’s identity systems don’t support this dual identity (user + agent). Without OBO, you end up with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tokens tied only to agents means no attribution to the human who delegated authority&lt;/li&gt;
&lt;li&gt;Over-permissioning means agents get more access than they need&lt;/li&gt;
&lt;li&gt;Weak audit trails mean logs just say “InfraBot did X”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How OBO works: dual identity in tokens
&lt;/h2&gt;

&lt;p&gt;OBO authentication encodes both the agent and the user inside a single token. This is done using structured JWT claims.&lt;/p&gt;

&lt;p&gt;Here’s a simple example of a decoded JWT payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "sub": "agent:infrabot",
  "act": {
    "sub": "user:eng123",
    "scope": ["incident:read", "monitoring:query"]
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells downstream systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The agent making the request is &lt;code&gt;agent:infrabot&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The user who delegated authority is &lt;code&gt;user:eng123&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The scopes permitted are &lt;code&gt;incident:read&lt;/code&gt;, &lt;code&gt;monitoring:query&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, when InfraBot calls GitHub or PagerDuty, those systems know both who executed the request and who authorized it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoped, explicit, and revocable permissions
&lt;/h2&gt;

&lt;p&gt;Delegation should never mean “full access.” Tokens must be:&lt;br&gt;
&lt;strong&gt;Explicit&lt;/strong&gt;: Only the required scopes (e.g., deploy:create)&lt;br&gt;
&lt;strong&gt;Scoped&lt;/strong&gt;: Limited to specific resources or environments&lt;br&gt;
&lt;strong&gt;Temporary&lt;/strong&gt;: Short-lived (5–15 minutes)&lt;br&gt;
&lt;strong&gt;Revocable&lt;/strong&gt;: Tied to user state so that if Sam loses deploy access, InfraBot loses it too&lt;/p&gt;

&lt;p&gt;Here’s an example of a structured, short-lived delegation token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "sub": "agent:infrabot",
  "act": {
    "sub": "user:sam",
    "scope": ["urn:infra:monitoring:read"],
    "context": {
      "trigger": "ci_pipeline",
      "environment": "prod"
    }
  },
  "exp": 1722000000,
  "revocation_url": "https://auth.company.com/revoke/token123",
  "log_url": "https://audit.company.com/logs/token123"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Multi-level delegation: Chaining identities
&lt;/h2&gt;

&lt;p&gt;Things get more complex when agents call other agents. For example:&lt;br&gt;
Sam → InfraBot → ArgoCD → AWS&lt;/p&gt;

&lt;p&gt;Nested claims in the token can represent that chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "sub": "agent:argocd",
  "act": {
    "sub": "agent:infrabot",
    "act": {
      "sub": "user:sam",
      "scope": ["deploy:create"]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Downstream services can then parse the chain and know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sam triggered the workflow&lt;/li&gt;
&lt;li&gt;InfraBot delegated to ArgoCD&lt;/li&gt;
&lt;li&gt;ArgoCD is the final executor&lt;/li&gt;
&lt;li&gt;Enforcing delegation in your services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Issuing tokens is only half the work. Every service receiving the token must:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validate the chain of actors (sub + act.sub)&lt;/li&gt;
&lt;li&gt;Enforce scopes against allowed operations&lt;/li&gt;
&lt;li&gt;Log both the agent and the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a simple Node.js middleware example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function delegationMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];
  const payload = jwt.verify(token, publicKey);

  req.agent = payload.sub;
  req.user = payload.act?.sub;
  req.scope = payload.act?.scope || [];

  if (!req.user) return res.status(401).send("Missing delegation context");
  next();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;Are your AI agents still using service accounts or static tokens? Start by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Issuing structured tokens with both agent + user identities&lt;/li&gt;
&lt;li&gt;Enforcing scopes at runtime in your APIs&lt;/li&gt;
&lt;li&gt;Shortening token lifetimes and adding revocation checks&lt;/li&gt;
&lt;li&gt;Logging both the actor and delegator in every request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building trustworthy delegation is the difference between uncontrolled automation and secure, enterprise-ready AI workflows.&lt;/p&gt;

&lt;p&gt;Want to go deeper? Check out the full write-up here: &lt;a href="https://www.scalekit.com/blog/delegated-agent-access" rel="noopener noreferrer"&gt;Understanding On-Behalf-Of in AI agent authentication&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>oauth</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Will your existing API work with AI agents?</title>
      <dc:creator>Saif </dc:creator>
      <pubDate>Wed, 20 Aug 2025 15:57:41 +0000</pubDate>
      <link>https://dev.to/saif_shines/will-your-existing-api-work-with-ai-agents-4ai6</link>
      <guid>https://dev.to/saif_shines/will-your-existing-api-work-with-ai-agents-4ai6</guid>
      <description>&lt;p&gt;If you've built a solid REST API and you're starting to integrate AI agents, MCP wrapping might be your fast track to compatibility. Wrapping doesn't mean rebuilding your backend. It means adding a machine-callable layer around it so AI models can treat those endpoints as structured tools.&lt;/p&gt;

&lt;p&gt;Let’s break down when wrapping makes sense, and walk through four practical patterns to help your agents interact cleanly with your system.&lt;/p&gt;

&lt;h2&gt;
  
  
  When wrapping MCP around your existing API makes sense
&lt;/h2&gt;

&lt;p&gt;Wrapping is a smart shortcut when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your API is &lt;strong&gt;stable, versioned, and well-documented&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You’re short on time or backend bandwidth&lt;/li&gt;
&lt;li&gt;You're prototyping AI integrations but don't want to overhaul your stack&lt;/li&gt;
&lt;li&gt;You need a &lt;strong&gt;buffer for legacy systems&lt;/strong&gt;, adding structure, auth, and formatting without touching internal logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of rewriting endpoints, you make them agent-ready with minimal effort, complete with typed inputs, response templates, and security layers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four wrapper architecture patterns
&lt;/h2&gt;

&lt;p&gt;Here are four proven approaches to MCP wrapping. Choose one based on your API design and integration goals:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Direct translation: Each endpoint becomes one tool
&lt;/h3&gt;

&lt;p&gt;Simply map each REST route to an MCP tool (e.g., &lt;code&gt;get_user_profile&lt;/code&gt;). Easy to automate using OpenAPI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Minimal logic, fast to generate&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: Too granular, potentially overwhelming&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Capability aggregation: Bundle related endpoints into intelligent tools
&lt;/h3&gt;

&lt;p&gt;For example, wrap &lt;code&gt;create_invoice&lt;/code&gt;, &lt;code&gt;update_invoice&lt;/code&gt;, &lt;code&gt;cancel_invoice&lt;/code&gt; into a single &lt;code&gt;InvoiceManager&lt;/code&gt; tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Cleaner for agents, allows orchestration or retries&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: Needs more design and docs effort&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Context-aware wrapping: Add short-term memory to tools
&lt;/h3&gt;

&lt;p&gt;Useful for flows like “search → select → update.” Maintain light session state or context to avoid repetition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Natural conversations; richer UX&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: Adds complexity: reset, expire, manage state, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Hybrid: Mix tool exposure and direct API calls
&lt;/h3&gt;

&lt;p&gt;Expose only core actions as tools, while letting internal services or legacy parts remain untouched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;: Flexible, low-risk for prototypes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;: Requires clear documentation and boundaries&lt;/p&gt;

&lt;h2&gt;
  
  
  Speed things up with OpenAPI-to-MCP tooling
&lt;/h2&gt;

&lt;p&gt;If your API already follows OpenAPI, you can generate MCP tool definitions automatically. The &lt;code&gt;openapi-to-mcp-converter&lt;/code&gt; lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turn an OpenAPI spec into MCP-ready tool configs&lt;/li&gt;
&lt;li&gt;Generate typed arguments + response templates effortlessly&lt;/li&gt;
&lt;li&gt;Add optional prompts or formatting hints to enrich outputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you a jump-start on wrapping with minimal manual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping vs. rebuilding: A quick comparison
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rebuilding as native MCP tools&lt;/strong&gt; can be more structured, but it takes time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrapping&lt;/strong&gt; gives you a fast way to support agents using your existing stack&lt;/li&gt;
&lt;li&gt;Most teams start with wrapping and refactor toward full tools only when needed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your next steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Choose a wrapper pattern&lt;/strong&gt; based on your API and agent needs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run OpenAPI-to-MCP tools&lt;/strong&gt; if your spec supports it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add structured prompts and validation&lt;/strong&gt; to make tools intuitive for agents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document and test&lt;/strong&gt; using an agent client like Claude, ChatGPT, or your own agent test harness&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Want a full walkthrough and examples on wrapping MCP around your API for agents? Check out the detailed guide: &lt;a href="https://www.scalekit.com/blog/wrap-mcp-around-existing-api" rel="noopener noreferrer"&gt;Wrap MCP around your existing API&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>api</category>
      <category>ai</category>
      <category>oauth</category>
    </item>
    <item>
      <title>Mapping an existing API to MCP tools</title>
      <dc:creator>Saif </dc:creator>
      <pubDate>Tue, 12 Aug 2025 16:19:07 +0000</pubDate>
      <link>https://dev.to/saif_shines/mapping-an-existing-api-to-mcp-tools-57l1</link>
      <guid>https://dev.to/saif_shines/mapping-an-existing-api-to-mcp-tools-57l1</guid>
      <description>&lt;p&gt;Imagine your team has an internal productivity API, built over the years, that powers projects, tasks, comments, roles, notifications, and access control. It’s stable and well‑used, but it’s not yet ready for AI agents.&lt;/p&gt;

&lt;p&gt;The challenge is to expose this system as a clean, composable set of MCP tools, not just wrappers, but schema‑driven, predictable, and reusable.&lt;/p&gt;

&lt;p&gt;Here’s how you can do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1️⃣ Understand your API surface
&lt;/h2&gt;

&lt;p&gt;Group endpoints into logical domains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users&lt;/li&gt;
&lt;li&gt;Tasks&lt;/li&gt;
&lt;li&gt;Comments&lt;/li&gt;
&lt;li&gt;Roles&lt;/li&gt;
&lt;li&gt;Projects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each domain, list the operations: &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;. This mapping leads to tool names like &lt;code&gt;get_user&lt;/code&gt;, &lt;code&gt;list_projects&lt;/code&gt;, &lt;code&gt;create_task&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2️⃣ MCP tools represent capabilities, not routes
&lt;/h2&gt;

&lt;p&gt;Each tool is a single, self‑contained capability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: "get_user"
description: "Retrieve a user by ID"
inputSchema: { userId: string }
outputSchema: { name: string; email: string }
handler: async ({ userId }) =&amp;gt; { /* ... */ }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flatten all request inputs into inputSchema and define predictable JSON outputs. Remove HTTP status codes from the interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  3️⃣ Transform OpenAPI into Agent‑Friendly Schemas
&lt;/h2&gt;

&lt;p&gt;From OpenAPI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /users/{id}
- path param: id: string
- 200 response: { name, email }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To MCP tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: "get_user",
inputSchema: { userId: z.string() },
outputSchema: { name: z.string(), email: z.string() }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes tools self‑documenting and composable.&lt;/p&gt;

&lt;h2&gt;
  
  
  4️⃣ Handle real‑world patterns
&lt;/h2&gt;

&lt;p&gt;We applied consistent schemas for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CRUD (&lt;code&gt;get_user&lt;/code&gt;, &lt;code&gt;create_task&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Filtering/Search (&lt;code&gt;search_tasks&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Batch Operations (&lt;code&gt;create_tasks_batch&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;File Uploads/Downloads (signed URLs)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inputSchema: {
  status: z.enum(["open", "in_progress", "closed"]),
  assignedTo: z.string().optional()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5️⃣ Keep tools predictable and reusable
&lt;/h2&gt;

&lt;p&gt;Stick to conventions (&lt;code&gt;get_&lt;/code&gt;, &lt;code&gt;list_&lt;/code&gt;, &lt;code&gt;create_&lt;/code&gt;, &lt;code&gt;update_&lt;/code&gt;, &lt;code&gt;delete_&lt;/code&gt;), share object schemas, and avoid mixing unrelated actions in a single tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  6️⃣ Clean, testable implementation
&lt;/h2&gt;

&lt;p&gt;Modular structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  tools/
    user-tools.ts
    task-tools.ts
  schemas/
    shared.ts
  server.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const getUserTool: MCPTool = {
  name: "get_user",
  description: "Retrieve a user by ID",
  inputSchema: z.object({ userId: z.string() }),
  outputSchema: UserSchema,
  handler: async ({ userId }) =&amp;gt; {
    const user = getUserById(userId);
    if (!user) throw { code: "NOT_FOUND", message: "User not found" };
    return user;
  }
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7️⃣ Test with a mock backend
&lt;/h2&gt;

&lt;p&gt;With mockDB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;it("get_user: throws on missing user", async () =&amp;gt; {
  await expect(getUserTool.handler({ userId: "nope" }))
    .rejects.toMatchObject({ code: "NOT_FOUND" });
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: A predictable, composable MCP tool layer with structured errors and maintainable code, ready for AI agents to use reliably.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.scalekit.com/blog/map-api-into-mcp-tool-definitions" rel="noopener noreferrer"&gt;📖 Read the deep dive&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your turn. Let me know your experiences about mapping API to MCP in the comments section below.&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>api</category>
      <category>mcp</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Implementing OAuth for MCP servers: a developer's guide</title>
      <dc:creator>Saif </dc:creator>
      <pubDate>Tue, 29 Jul 2025 16:47:05 +0000</pubDate>
      <link>https://dev.to/saif_shines/implementing-oauth-for-mcp-servers-a-developers-guide-2obc</link>
      <guid>https://dev.to/saif_shines/implementing-oauth-for-mcp-servers-a-developers-guide-2obc</guid>
      <description>&lt;p&gt;Imagine you've built an AI-driven sales analytics tool used by enterprise SaaS businesses. It aggregates sales data, integrates with CRMs, and helps companies forecast revenue accurately.&lt;/p&gt;

&lt;p&gt;In today’s AI-centric ecosystem, you would need to have the capability to use conversational prompts to surface the right insights. How do you securely handle authentication and manage API requests at scale without exposing sensitive customer data?&lt;/p&gt;

&lt;p&gt;MCP servers act as a secure layer that enables your AI application to interact safely and efficiently with external systems. With an MCP server in place, your sales analytics tool can securely handle auth, manage access to sensitive data, and control interactions between your app and client systems, all without exposing direct access credentials or data endpoints.&lt;/p&gt;

&lt;p&gt;As your MCP server moves from prototype to production, OAuth 2.1 authentication becomes critical. Here’s a step-by-step guide on securely implementing OAuth 2.1, with code examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  OAuth implementation overview
&lt;/h2&gt;

&lt;p&gt;Implementing OAuth for MCP involves four core steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Register your MCP server and define OAuth scopes&lt;/li&gt;
&lt;li&gt;Expose OAuth protected resource metadata&lt;/li&gt;
&lt;li&gt;Validate JWT tokens&lt;/li&gt;
&lt;li&gt;Implement scope-based authorization&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's dive right in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Register your MCP server
&lt;/h2&gt;

&lt;p&gt;First, register your MCP server in your &lt;a href="https://docs.scalekit.com/guides/mcp/overview/" rel="noopener noreferrer"&gt;Scalekit dashboard&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server name:&lt;/strong&gt; e.g., "Sales Analytics API"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource identifier:&lt;/strong&gt; Unique URL (e.g., &lt;code&gt;https://api.sales-analytics.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic client registration:&lt;/strong&gt; Enabled for automatic onboarding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token lifetime:&lt;/strong&gt; Access tokens (5 min–1 hr), refresh tokens (up to 24 hrs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Define your OAuth scopes
&lt;/h3&gt;

&lt;p&gt;Scopes should clearly represent permissions. Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mcp:tools:sales-data:read&lt;/code&gt; - Read sales data&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mcp:tools:crm:write&lt;/code&gt; - Update CRM records&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mcp:resources:customer-insights:read&lt;/code&gt; - Access customer insights&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 2: OAuth protected resource metadata
&lt;/h2&gt;

&lt;p&gt;Your MCP server exposes metadata for clients to auto-discover OAuth endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/.well-known/oauth-protected-resource&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.sales-analytics.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;authorization_servers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-org.scalekit.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;bearer_methods_supported&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;resource_documentation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.sales-analytics.com/docs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scopes_supported&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp:tools:sales-data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp:tools:crm:read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp:tools:crm:write&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp:tools:notifications:send&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp:resources:*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Validate JWT tokens
&lt;/h2&gt;

&lt;p&gt;Each request to your MCP endpoints must validate a JWT token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;jwtVerify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createRemoteJWKSet&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;JWKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRemoteJWKSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-org.scalekit.com/.well-known/jwks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validateToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer token required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;jwtVerify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JWKS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-org.scalekit.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;audience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.sales-analytics.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
      &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exp&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid or expired token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;validateToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Scope-based authorization
&lt;/h2&gt;

&lt;p&gt;Scopes provide granular permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requireScope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requiredScope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userScopes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasScope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userScopes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;requiredScope&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;requiredScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasScope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;insufficient_scope&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;required_scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requiredScope&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/mcp/tools/sales-data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;requireScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp:tools:sales-data:read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;handleSalesDataRequest&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing your implementation
&lt;/h2&gt;

&lt;p&gt;Thoroughly test OAuth integration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testMCPAuth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-org.scalekit.com/oauth2/token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;grant_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client_credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-client-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-client-secret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp:tools:sales-data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;access_token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tokenResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.sales-analytics.com/mcp/tools/sales-data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sales/get_forecast&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;North America&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For further details, explore our comprehensive guide: &lt;a href="https://scalekit.webflow.io/blog/implementing-oauth-mcp-servers" rel="noopener noreferrer"&gt;Implement OAuth authentication for MCP servers using Scalekit&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>mcp</category>
      <category>authorization</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why you need token vaults for AI agent workflows</title>
      <dc:creator>Saif </dc:creator>
      <pubDate>Mon, 21 Jul 2025 12:24:01 +0000</pubDate>
      <link>https://dev.to/saif_shines/why-you-need-token-vaults-for-ai-agent-workflows-16i5</link>
      <guid>https://dev.to/saif_shines/why-you-need-token-vaults-for-ai-agent-workflows-16i5</guid>
      <description>&lt;p&gt;Managing credentials for AI agents often means scattering secrets across containers and services. Hard-coded tokens and custom refresh logic quickly become security risks.&lt;/p&gt;

&lt;p&gt;A token vault centralizes credential storage, handles automatic rotation, and dispenses short-lived access tokens on demand. Agents simply request the tokens they need. No long-lived secrets in their code.&lt;/p&gt;

&lt;p&gt;Below, we’ll cover why a token vault is essential for modern workflows, walk through a real-world productivity tracker example, and share vetted code snippets to help you get started.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is a token vault?
&lt;/h2&gt;

&lt;p&gt;A token vault is a dedicated service that holds and manages all your API credentials. Instead of embedding &lt;a href="https://www.scalekit.com/blog/oauth-tokens-m2m-authentication" rel="noopener noreferrer"&gt;OAuth tokens&lt;/a&gt; or service-account secrets inside each agent, you give every agent a single vault credential. &lt;/p&gt;

&lt;p&gt;When an agent needs to call an API (Say, to create a calendar entry or fetch analytics data), it asks the vault for a scoped access token. The vault handles refreshing tokens, auditing access, and enforcing least-privilege policies.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical use case: AI productivity tracker
&lt;/h2&gt;

&lt;p&gt;Imagine you’re building a productivity tracker that uses AI to summarize a user’s meetings, log tasks to a time-tracking tool, and post daily digests to Slack. Each of these integrations requires separate OAuth tokens, and each agent instance, running per user session, needs its own credentials.&lt;/p&gt;

&lt;p&gt;Without a vault, you’d end up hard-coding refresh tokens in every container, writing duplicate logic to detect expiration and refresh tokens, and struggling to audit which agent accessed which service.&lt;/p&gt;

&lt;p&gt;With a token vault, your tracker agents become credential-agnostic. They simply request “give me a Slack token” or “give me a time-tracker token” and get back a valid bearer token, scoped exactly to the API they need.&lt;/p&gt;




&lt;h2&gt;
  
  
  Core vault workflow
&lt;/h2&gt;

&lt;p&gt;At a high level, the vault flow works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The vault operator performs an initial &lt;a href="https://www.scalekit.com/blog/client-credentials-flow-oauth" rel="noopener noreferrer"&gt;OAuth flow&lt;/a&gt; (or service-account exchange) to provision long-lived refresh tokens for each external API.&lt;/li&gt;
&lt;li&gt;Agents authenticate to the vault using their own short-lived vault token.&lt;/li&gt;
&lt;li&gt;When an agent needs to call an API, it requests a fresh token from the vault, specifying which service it needs (e.g., &lt;code&gt;slack-post&lt;/code&gt; or &lt;code&gt;time-tracker&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The vault returns a scoped access token. Under the hood, the vault uses the stored refresh token to fetch a new access token if necessary, and logs the event.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Example: Fetching a time-tracker token
&lt;/h2&gt;

&lt;p&gt;Here’s how a productivity tracker agent fetches a token for the time tracking API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getTimeTrackerToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agentId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vaultUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VAULT_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vaultToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VAULT_AGENT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// short-lived&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vaultUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/agents/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;agentId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/credentials/time-tracker`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;vaultToken&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Vault error (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;): &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;access_token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this helper, your agent code stays focused on business logic: summarizing meetings or logging hours, without ever touching the underlying refresh tokens or client secrets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Automated rotation and cleanup
&lt;/h2&gt;

&lt;p&gt;A background job in the vault takes care of refreshing tokens behind the scenes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# refresh_time_tracker.sh&lt;/span&gt;

&lt;span class="nv"&gt;new_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://oauth.time-tracker.com/token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$TT_CLIENT_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;client_secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$TT_CLIENT_SECRET&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;grant_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;refresh_token &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nv"&gt;refresh_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$TT_REFRESH_TOKEN&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Store the new token in the vault&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VAULT_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/v1/credentials/time-tracker"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VAULT_ADMIN_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;refresh_token&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$new_token&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; .refresh_token&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script can run on a schedule (e.g., cron or a Kubernetes CronJob), ensuring your agents always receive valid tokens without extra code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;A token vault streamlines secure credential management by centralizing storage, rotation, and access control. Agent code remains lean, secrets are protected in one place, and audit logs give you full visibility into who requested which token and when. &lt;/p&gt;

&lt;p&gt;For platforms with dozens or hundreds of AI workflows, a vault is the only sustainable path to secure, scalable integrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you’d like to get a comprehensive, step-by-step flow of how you can use a token vault, &lt;a href="https://www.scalekit.com/blog/token-vault-ai-agent-workflows" rel="noopener noreferrer"&gt;check out our blog post&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;Have you implemented a token vault or secrets manager for your AI agents or microservices? What tooling did you choose, and what challenges did you face? Share your experiences below. 👇&lt;/p&gt;

</description>
      <category>oauth</category>
      <category>tokenvault</category>
      <category>authentication</category>
      <category>ai</category>
    </item>
    <item>
      <title>Mapping the MCP Ecosystem</title>
      <dc:creator>Saif </dc:creator>
      <pubDate>Thu, 10 Jul 2025 15:52:28 +0000</pubDate>
      <link>https://dev.to/saif_shines/mapping-the-mcp-ecosystem-1l48</link>
      <guid>https://dev.to/saif_shines/mapping-the-mcp-ecosystem-1l48</guid>
      <description>&lt;p&gt;Every week feels like a new chapter in the AI world. And if you’ve been playing around with MCP servers, you know exactly what I mean.&lt;/p&gt;

&lt;p&gt;MCP is opening up something I’ve personally never seen at this scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI agents securely calling into SaaS apps as if they were just another user.&lt;/li&gt;
&lt;li&gt;SaaS platforms exposing their capabilities as discoverable, authenticated tools for agents to use.&lt;/li&gt;
&lt;li&gt;Devs like us building something mindblowing every other week.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What excites me even more is just how fast this ecosystem is growing.&lt;br&gt;
While building, we kept asking: who else is here? what tools are emerging? where does my work fit in?&lt;/p&gt;

&lt;p&gt;So we decided to map it. Across categories: servers, clients, hosting, testing, registries, marketplaces, and more.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.scalekit.com/blog/mcp-stack" rel="noopener noreferrer"&gt;Here’s our snapshot&lt;/a&gt; of the MCP ecosystem today.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkzxuqs6hgdgquikbe2f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhkzxuqs6hgdgquikbe2f.png" alt=" " width="800" height="1462"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>oauth</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
