DEV Community

Saif
Saif

Posted on

Enterprise Auth in Astro without the pain

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.


Table of Contents

  1. Why Scalekit for B2B auth?
  2. Core concepts: OAuth 2.0, OIDC, and the token trio
  3. Project setup & environment
  4. Initializing the Scalekit client
  5. The three auth endpoints
  6. Session middleware — the right way
  7. Protecting pages and API routes
  8. Enterprise SSO: per-organization connections
  9. PKCE flow (no client secret)
  10. Production best practices
  11. Troubleshooting common gotchas

Why Scalekit for B2B auth?

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.

In 7 out of 10 enterprise deals, authentication requirements like SSO are deal-breakers. 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.

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.

A single Scalekit environment can serve multiple applications (e.g., app.yourcompany.com and docs.yourcompany.com), 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.

💡 Scalekit's philosophy: 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.


Core concepts: OAuth 2.0, OIDC, and the token trio

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.

The Authorization Code Flow

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 authorization code back to your callback URL. Your server exchanges that code for three tokens.

Browser → /api/auth/login → Scalekit (IdP / SSO) → /api/auth/callback → HttpOnly Cookies
Enter fullscreen mode Exit fullscreen mode

The token trio

Scalekit returns three tokens on a successful exchange. Understanding their purpose determines how you store and use them:

Token Purpose Typical lifetime Where to store
idToken Identifies the user (name, email, sub). Used for logout hint. 1 hour HttpOnly cookie
accessToken Authorizes API calls. Validated server-side on every request. 15–60 minutes HttpOnly cookie
refreshToken Obtains a new access token without re-authentication. Days to weeks HttpOnly cookie (secure)

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

SAML vs. OIDC — which does your enterprise customer need?

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:

Protocol Format Common use case Scalekit support
SAML 2.0 XML assertions Enterprise SSO (Okta, Entra ID, ADFS) ✅ Full
OIDC JWT tokens Modern SSO, social login, API auth ✅ Full
OAuth 2.0 Bearer tokens API authorization ✅ Full

Project setup & environment

Prerequisites

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

Create the Astro project

# Scaffold a new Astro project
npm create astro@latest my-app -- --template minimal
cd my-app

# Install Scalekit SDK and the Node.js adapter
npm install @scalekit-sdk/node @astrojs/node
Enter fullscreen mode Exit fullscreen mode

Configure server-side rendering

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

// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
});
Enter fullscreen mode Exit fullscreen mode

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

Environment variables

Add your Scalekit credentials to .env. Never commit this file — add it to .gitignore.

# .env
SCALEKIT_ENVIRONMENT_URL=https://<your-env>.scalekit.cloud
SCALEKIT_CLIENT_ID=skc_<your-client-id>
SCALEKIT_CLIENT_SECRET=sks_<your-secret>
SCALEKIT_REDIRECT_URI=http://localhost:4321/api/auth/callback
Enter fullscreen mode Exit fullscreen mode

TypeScript types and IntelliSense

Declare the env variables and the App.Locals shape so TypeScript can type-check your middleware and Astro pages throughout the project:

// src/env.d.ts
/// <reference types="astro/client" />

interface ImportMetaEnv {
  readonly SCALEKIT_ENVIRONMENT_URL: string;
  readonly SCALEKIT_CLIENT_ID: string;
  readonly SCALEKIT_CLIENT_SECRET: string;
  readonly SCALEKIT_REDIRECT_URI: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

declare namespace App {
  interface Locals {
    user?: {
      sub: string;
      email?: string;
      name?: string;
      orgId?: string; // available when using enterprise SSO
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Initializing the Scalekit client

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:

// src/lib/scalekit.ts
import { ScalekitClient } from '@scalekit-sdk/node';

// Singleton — instantiated once, reused across requests
export const scalekit = new ScalekitClient(
  import.meta.env.SCALEKIT_ENVIRONMENT_URL,
  import.meta.env.SCALEKIT_CLIENT_ID,
  import.meta.env.SCALEKIT_CLIENT_SECRET,
);

export const REDIRECT_URI =
  import.meta.env.SCALEKIT_REDIRECT_URI
  ?? 'http://localhost:4321/api/auth/callback';
Enter fullscreen mode Exit fullscreen mode

💡 Validate at startup: In production, wrap the ScalekitClient 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.


The three auth endpoints

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.

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
Enter fullscreen mode Exit fullscreen mode

login.ts — initiating the flow

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:

// src/pages/api/auth/login.ts
import type { APIRoute } from 'astro';
import { scalekit, REDIRECT_URI } from '../../../lib/scalekit';

export const GET: APIRoute = async ({ request }) => {
  // Optional: pass organizationId for direct IdP routing (enterprise SSO)
  const url = new URL(request.url);
  const orgId = url.searchParams.get('orgId') ?? undefined;

  const authUrl = scalekit.getAuthorizationUrl(REDIRECT_URI, {
    scopes: ['openid', 'profile', 'email', 'offline_access'],
    // Route directly to an enterprise IdP by org:
    ...(orgId && { organizationId: orgId }),
  });

  return Response.redirect(authUrl);
};
Enter fullscreen mode Exit fullscreen mode

ℹ️ offline_access scope: Including offline_access 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.

callback.ts — exchanging the code for tokens

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 cannot happen in the browser because it requires the secret. Tokens are then stored as HttpOnly cookies:

// src/pages/api/auth/callback.ts
import type { APIRoute } from 'astro';
import { scalekit, REDIRECT_URI } from '../../../lib/scalekit';

export const GET: APIRoute = async ({ request, cookies }) => {
  const url = new URL(request.url);
  const code = url.searchParams.get('code');
  const error = url.searchParams.get('error');

  // Handle errors gracefully — Scalekit sends them as query params
  if (error) {
    const desc = url.searchParams.get('error_description') ?? error;
    return Response.redirect(new URL(`/?error=${encodeURIComponent(desc)}`, url.origin).href);
  }

  if (!code) {
    return Response.redirect(new URL('/', url.origin).href);
  }

  try {
    const { user, idToken, accessToken, refreshToken } =
      await scalekit.authenticateWithCode(code, REDIRECT_URI);

    const secure = url.protocol === 'https:';
    const cookieOptions = {
      httpOnly: true,
      path: '/',
      sameSite: 'lax' as const,
      secure,
      // Set expiry to match the refresh token lifetime
      maxAge: 60 * 60 * 24 * 7, // 7 days
    };

    cookies.set('sk-id-token', idToken, cookieOptions);
    cookies.set('sk-access-token', accessToken, cookieOptions);
    cookies.set('sk-refresh-token', refreshToken, cookieOptions);

    // Redirect to a post-login destination (support ?next= param)
    const next = url.searchParams.get('next') ?? '/';
    return Response.redirect(new URL(next, url.origin).href);
  } catch (err) {
    console.error('[scalekit] token exchange failed', err);
    return Response.redirect(new URL('/?error=auth_failed', url.origin).href);
  }
};
Enter fullscreen mode Exit fullscreen mode

logout.ts — ending the session correctly

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.

// src/pages/api/auth/logout.ts
import type { APIRoute } from 'astro';
import { scalekit } from '../../../lib/scalekit';

export const GET: APIRoute = async ({ request, cookies }) => {
  const idToken = cookies.get('sk-id-token')?.value;

  // Step 1: clear local session cookies
  const deleteOpts = { path: '/' };
  cookies.delete('sk-id-token', deleteOpts);
  cookies.delete('sk-access-token', deleteOpts);
  cookies.delete('sk-refresh-token', deleteOpts);

  // Step 2: redirect to Scalekit's logout endpoint
  const origin = new URL(request.url).origin;
  const logoutUrl = scalekit.getLogoutUrl({
    idTokenHint: idToken,
    postLogoutRedirectUri: origin,
  });

  return Response.redirect(logoutUrl);
};
Enter fullscreen mode Exit fullscreen mode

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


Session middleware — the right way

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 Astro.locals.user. Do it once, centrally, and every page in your app benefits automatically.

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:

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
import type { IdTokenClaim } from '@scalekit-sdk/node';
import { scalekit } from './lib/scalekit';

const clearSession = (cookies: any) => {
  cookies.delete('sk-id-token', { path: '/' });
  cookies.delete('sk-access-token', { path: '/' });
  cookies.delete('sk-refresh-token', { path: '/' });
};

export const onRequest = defineMiddleware(async (context, next) => {
  const { pathname } = context.url;

  // Skip auth check for public assets and auth routes
  if (
    pathname.startsWith('/api/auth') ||
    pathname.startsWith('/_astro') ||
    pathname.match(/\.(ico|png|jpg|svg|css|js|woff2?)$/)
  ) {
    return next();
  }

  const accessToken = context.cookies.get('sk-access-token')?.value;

  if (!accessToken) return next();

  try {
    // Fast path: access token is still valid
    const claims = await scalekit.validateToken<IdTokenClaim>(accessToken);
    context.locals.user = {
      sub: claims.sub,
      email: claims.email,
      name: claims.name,
    };
  } catch {
    // Slow path: token expired — try refresh
    const refreshToken = context.cookies.get('sk-refresh-token')?.value;

    if (!refreshToken) {
      clearSession(context.cookies);
      return next();
    }

    try {
      const { accessToken: newToken } =
        await scalekit.refreshAccessToken(refreshToken);

      const secure = new URL(context.request.url).protocol === 'https:';
      context.cookies.set('sk-access-token', newToken, {
        httpOnly: true,
        path: '/',
        sameSite: 'lax',
        secure,
        maxAge: 60 * 60, // new access token: 1 hour
      });

      const claims = await scalekit.validateToken<IdTokenClaim>(newToken);
      context.locals.user = {
        sub: claims.sub,
        email: claims.email,
        name: claims.name,
      };
    } catch {
      // Refresh also failed — wipe everything
      clearSession(context.cookies);
    }
  }

  return next();
});
Enter fullscreen mode Exit fullscreen mode

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


Protecting pages and API routes

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

Protected page pattern

---
// 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')}`);
}
---
<Layout title="Dashboard">
  <h1>Welcome, {user.name ?? user.email}</h1>
  <p>You're signed in. <a href="/api/auth/logout">Sign out</a></p>
</Layout>
Enter fullscreen mode Exit fullscreen mode

Protected API route pattern

// src/pages/api/data.ts
import type { APIRoute } from 'astro';

export const GET: APIRoute = ({ locals }) => {
  if (!locals.user) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  return new Response(JSON.stringify({
    user: locals.user,
    data: '… your data …',
  }), {
    headers: { 'Content-Type': 'application/json' },
  });
};
Enter fullscreen mode Exit fullscreen mode

Navigation component

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

---
// src/components/AuthNav.astro
const user = Astro.locals.user;
---
<nav>
  {user ? (
    <>
      <span>{user.name ?? user.email}</span>
      <a href="/api/auth/logout">Sign out</a>
    </>
  ) : (
    <a href="/api/auth/login">Sign in</a>
  )}
</nav>
Enter fullscreen mode Exit fullscreen mode

Enterprise SSO: per-organization connections

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.

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

// src/pages/api/auth/login.ts — enterprise routing
export const GET: APIRoute = async ({ request }) => {
  const url = new URL(request.url);

  // Option A: route by org ID (stored in your DB after org onboarding)
  const orgId = url.searchParams.get('orgId');

  // Option B: route by email domain (Scalekit resolves IdP from domain)
  const loginHint = url.searchParams.get('email');

  const authUrl = scalekit.getAuthorizationUrl(REDIRECT_URI, {
    scopes: ['openid', 'profile', 'email', 'offline_access'],
    ...(orgId && { organizationId: orgId }),
    ...(loginHint && { loginHint }),
  });

  return Response.redirect(authUrl);
};
Enter fullscreen mode Exit fullscreen mode

The org-aware login form

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:

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

<p><a href="/api/auth/login">Or sign in with Google / GitHub</a></p>
Enter fullscreen mode Exit fullscreen mode

💡 IdP-initiated SSO: 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 state parameter in the redirect, you're in an IdP-initiated flow. Validate carefully and redirect to a safe default post-login destination.

Reading org context from the token

After a successful SSO authentication, the decoded access token claims will include the user's orgId. Use this to scope database queries and enforce multi-tenant isolation:

// src/middleware.ts — extended claims
const claims = await scalekit.validateToken<IdTokenClaim>(accessToken);
context.locals.user = {
  sub: claims.sub,
  email: claims.email,
  name: claims.name,
  orgId: (claims as any).org_id, // available on enterprise SSO logins
};
Enter fullscreen mode Exit fullscreen mode

PKCE flow (no client secret)

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).

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.

Step Authorization Code + Secret PKCE (no secret)
Code generation SDK builds URL, secret on server SDK generates code_verifier + code_challenge
Verifier storage N/A Session cookie or server-side store
Token exchange code + secret code + code_verifier
Best for SSR Astro (server mode) Edge, static hybrid, public repos
// src/pages/api/auth/login.ts — PKCE variant
import { generatePkce } from '@scalekit-sdk/node'; // or your own crypto util

export const GET: APIRoute = async ({ request, cookies }) => {
  const { codeVerifier, codeChallenge } = await generatePkce();

  // Store verifier server-side — needed at callback
  cookies.set('pkce-verifier', codeVerifier, {
    httpOnly: true, path: '/', sameSite: 'lax', maxAge: 300,
  });

  const authUrl = scalekit.getAuthorizationUrl(REDIRECT_URI, {
    scopes: ['openid', 'profile', 'email', 'offline_access'],
    codeChallenge,
    codeChallengeMethod: 'S256',
  });

  return Response.redirect(authUrl);
};
Enter fullscreen mode Exit fullscreen mode

Production best practices

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

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

BP 03 — Rotate tokens silently with refresh
The middleware's fallback to refreshAccessToken() keeps users logged in seamlessly for the lifetime of the refresh token. Always store the refresh token in an HttpOnly cookie.

BP 04 — Implement post-login redirect with ?next=
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.

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

BP 06 — Register all redirect URIs before deploying
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.

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

BP 08 — Multi-tenant isolation via orgId
When enterprise SSO is used, the token includes org_id. 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 sub claim.

Security headers

Beyond token security, add HTTP security headers to your Astro middleware. These are complementary to auth, not a substitute:

// src/middleware.ts — security headers addition
// At the end of your middleware, before returning next()
const response = await next();
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set(
  'Content-Security-Policy',
  "default-src 'self'; script-src 'self'; connect-src 'self' https://*.scalekit.cloud;",
);
return response;
Enter fullscreen mode Exit fullscreen mode

Troubleshooting common gotchas

redirect_uri_mismatch

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:

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

Token validation fails after deployment

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

Refresh token not returned

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

Middleware runs on static assets

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.

Enterprise SSO user not found in your database

The first time a user logs in via enterprise SSO, they may not have a row in your database. The sub 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.


Putting it all together

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:

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
Enter fullscreen mode Exit fullscreen mode

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.

🚀 Next steps: 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.


Resources


Written for the Scalekit developer community · scalekit.com

Top comments (0)