npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@workos/authkit-astro

v0.0.1

Published

Official WorkOS AuthKit SDK for Astro

Readme

@workos/authkit-astro

Official WorkOS AuthKit SDK for Astro. Session validation, automatic token refresh, PKCE sign-in, route protection, auth components, and a client-island auth store — built on @workos/authkit-session.

Requires an SSR / on-demand Astro app (an adapter such as @astrojs/node). Individual pages may still opt into prerendering — see Prerendered pages.

Quick start (integration)

npx astro add @workos/authkit-astro

astro add installs the package and adds the integration to your config. Or do it by hand:

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

export default defineConfig({
  output: 'server',
  adapter: node({ mode: 'standalone' }),
  integrations: [workos({ protectedRoutes: ['/dashboard'] })],
});

The integration auto-wires everything: the auth middleware, the /login · /signup · /callback · /logout routes, the client session endpoint, the astro:env secret schema, and the Astro.locals.auth types. You also need to install the WorkOS Node SDK peer (npx handles this for you with astro add):

npm add @workos-inc/node

Then provide the env vars (read at runtime via astro:env, never bundled):

WORKOS_CLIENT_ID=client_...
WORKOS_API_KEY=sk_test_...
WORKOS_REDIRECT_URI=http://localhost:4321/callback
WORKOS_COOKIE_PASSWORD=at-least-32-characters-long-secret   # openssl rand -base64 32

Add http://localhost:4321/callback as a redirect URI in the WorkOS dashboard. That's it — /dashboard now redirects anonymous visitors to sign-in, and Astro.locals.auth is populated (and typed) everywhere.

---
const { auth } = Astro.locals;
---
{
  auth.user ? (
    <span>Signed in as {auth.user.email} · <a href="/logout">Sign out</a></span>
  ) : (
    <a href="/login">Sign in</a>
  )
}

Integration options

workos({
  protectedRoutes: ['/dashboard(.*)'], // prefixes or path-to-regexp-style patterns
  signInPath: '/login', // where anonymous visitors are sent
  loginPath: '/login', // patterns for the injected routes
  signUpPath: '/signup',
  callbackPath: '/callback',
  logoutPath: '/logout',
  afterSignOutUrl: '/', // where /logout lands (also accepts ?returnTo=)
  errorRedirect: '/login', // redirect (with ?error=) on callback failure instead of a 400
  sessionEndpoint: '/_authkit/me', // client store hydration endpoint
  injectRoutes: true,
  injectEnvSchema: true, // declare WORKOS_* in astro:env
  hydrateClient: true, // session endpoint + client bootstrap script
});

protectedRoutes accepts plain prefixes (/dashboard also matches /dashboard/...) and path-to-regexp-style patterns (/dashboard(.*), /orgs/:slug). Anonymous browser navigations are redirected to signInPath; non-HTML requests (e.g. fetch() from an island) get a 401 JSON response instead.

Astro.locals.auth

A discriminated union on user — checking if (auth.user) narrows sessionId, accessToken, and claims to non-optional types.

auth.user; // User | null
auth.sessionId; // string  (signed in)
auth.accessToken; // string  (signed in; server only — never sent to the client)
auth.claims; // verified JWT claims, incl. custom claims (signed in)
auth.organizationId; // string | null
auth.role; // string | null
auth.roles; // string[]
auth.permissions; // string[]
auth.entitlements; // string[]
auth.featureFlags; // string[]
auth.impersonator; // Impersonator | null

auth.has({ role: 'admin' }); // boolean — also permission / entitlement / featureFlag; ANDs checks
auth.redirectToSignIn(); // Response — redirect to sign-in, returnTo = current URL

Guard a page or endpoint imperatively:

---
const { auth } = Astro.locals;
if (!auth.user) return auth.redirectToSignIn();
if (!auth.has({ permission: 'billing:manage' })) return new Response(null, { status: 403 });
---

Components

Server-rendered control and button components (the unmatched branch never reaches the browser on request-rendered pages):

---
import {
  AuthState,    // synchronous client-store hydration (put in <head>)
  Show,         // <Show when={{ role: 'admin' }}> ... <p slot="fallback">…</p> </Show>
  SignedIn,     // children render only when signed in
  SignedOut,    // children render only when signed out
  SignInButton, // <a> to the login route; props: path, returnTo, + anchor attrs
  SignUpButton,
  SignOutButton,
  Impersonation, // fixed banner while the session is impersonated
} from '@workos/authkit-astro/components';
---

<SignedIn>
  Welcome back! <SignOutButton class="btn" />
</SignedIn>
<SignedOut>
  <SignInButton returnTo="/dashboard">Log in</SignInButton>
</SignedOut>

<Show when={{ permission: 'invoices:create' }}>
  <a href="/invoices/new">New invoice</a>
  <span slot="fallback">Ask an admin for access.</span>
</Show>

when accepts 'signed-in', 'signed-out', an object of role/permission/entitlement/featureFlag checks (ANDed), or a predicate (auth) => boolean (server-rendered pages only). On prerendered pages the components defer to the client store via a tiny <authkit-gate> custom element instead.

Client-island auth store

Read the session reactively from any island (React, Vue, Svelte, Preact, Solid, or vanilla) via nanostores — the same store works across frameworks. The store holds a client-safe snapshot (no access token).

Hydrate it synchronously by dropping <AuthState /> in your layout <head>:

---
// src/layouts/Layout.astro
import { AuthState } from '@workos/authkit-astro/components';
---
<head>
  <AuthState />
</head>

(The integration also exposes /_authkit/me, so islands hydrate even without <AuthState /> — just slightly later. With Astro's <ClientRouter /> view transitions, the store re-hydrates automatically after each navigation.)

Then read it in an island:

// React island
import { useStore } from '@nanostores/react';
import { $signedIn, $user } from '@workos/authkit-astro/client';

export function UserBadge() {
  const user = useStore($user);
  return <span>{user ? user.email : 'Signed out'}</span>;
}

The client entry exports the stores $auth, $user, $signedIn, $isLoaded, $organizationId, $role, $permissions and the helpers setAuthState() / hydrateAuth(). $isLoaded distinguishes "not hydrated yet" from "signed out" — gate loading UI on it. Use the matching @nanostores/{react,vue,svelte,...} binding for your island framework — or, for React, the zero-dependency hooks:

import { useAuth, useUser } from '@workos/authkit-astro/react';

export function UserBadge() {
  const { user, isLoaded } = useUser();
  if (!isLoaded) return null;
  return <span>{user ? user.email : 'Signed out'}</span>;
}

Manual setup (advanced)

Prefer to wire things yourself (custom paths, full middleware control, no integration)? The building blocks are exported directly.

// src/middleware.ts
import { authkitMiddleware, configureAuthKit, createRouteMatcher } from '@workos/authkit-astro';
import { WORKOS_API_KEY, WORKOS_CLIENT_ID, WORKOS_COOKIE_PASSWORD, WORKOS_REDIRECT_URI } from 'astro:env/server';

configureAuthKit({
  clientId: WORKOS_CLIENT_ID,
  apiKey: WORKOS_API_KEY,
  redirectUri: WORKOS_REDIRECT_URI,
  cookiePassword: WORKOS_COOKIE_PASSWORD,
});

// Options form — string prefix, pattern, RegExp, or (pathname) => boolean:
export const onRequest = authkitMiddleware({
  protectedRoutes: ['/dashboard(.*)', /^\/admin/],
});

Or take full per-request control with the handler form:

const isAdminRoute = createRouteMatcher(['/admin(.*)']);

export const onRequest = authkitMiddleware((auth, context) => {
  if (isAdminRoute(context.url) && !auth.has({ role: 'admin' })) {
    return auth.redirectToSignIn();
  }
});
// src/pages/login.ts   (and signup.ts, logout.ts)
export { handleSignIn as GET } from '@workos/authkit-astro';
// src/pages/callback.ts — drop-in, or customized:
import { createCallbackHandler } from '@workos/authkit-astro';
export const GET = createCallbackHandler({
  errorRedirect: '/login',
  onSuccess: (_ctx, { authResponse }) => console.log('signed in', authResponse.user.id),
});

Organizations

Switch the session's active organization (refreshes the access token scoped to the org and persists the new session cookie):

// src/pages/api/switch-org.ts
import { switchToOrganization } from '@workos/authkit-astro';

export const POST: APIRoute = async (context) => {
  const { organizationId } = await context.request.json();
  const auth = await switchToOrganization(context, organizationId);
  return Response.json({
    organizationId: auth.organizationId,
    role: auth.role,
  });
};

WorkOS API access

The full WorkOS Node client, sharing the SDK's configuration:

import { getWorkOS } from '@workos/authkit-astro';

const org = await getWorkOS().organizations.getOrganization(auth.organizationId);

Webhooks

Verify the workos-signature header and get a parsed event back:

// src/pages/api/webhooks.ts
import { verifyWebhook } from '@workos/authkit-astro';

export const POST: APIRoute = async (context) => {
  const event = await verifyWebhook(context); // secret from WORKOS_WEBHOOK_SECRET
  if (event.event === 'user.created') {
    // ...
  }
  return new Response(null, { status: 200 });
};

Prerendered pages

Pages with export const prerender = true build without a request, so the middleware skips session work there (locals.auth is signed-out and builds don't need the WORKOS_* secrets). On those pages:

  • <SignedIn> / <SignedOut> / <Show> defer to the client store and resolve after hydration (content is in the HTML — don't put secrets in it).
  • <AuthState /> emits nothing; islands hydrate from the session endpoint.

API

| Export | Type | Purpose | | -------------------------------------- | ------------------- | ---------------------------------------------------------------------- | | default (workos) | AstroIntegration | The astro add integration | | authkitMiddleware(options?) | MiddlewareHandler | Validate session → Astro.locals.auth, refresh tokens, gate routes | | authkitMiddleware(handler, options?) | MiddlewareHandler | Handler form: (auth, context, next) => for full per-request control | | createRouteMatcher(patterns) | (input) => bool | Reusable matcher for prefixes / patterns / RegExps / predicates | | configureAuthKit(config) | void | Provide config explicitly (e.g. from astro:env) | | getWorkOS() | WorkOS | The configured WorkOS Node client | | switchToOrganization(ctx, orgId) | Promise<auth> | Switch the session's active organization | | getSignInUrl(ctx, opts?) | Promise<string> | Sign-in URL + writes PKCE verifier cookie (for custom links) | | getSignUpUrl(ctx, opts?) | Promise<string> | Sign-up URL variant | | handleSignIn / handleSignUp | APIRoute | Drop-in GET handlers that redirect to AuthKit | | handleCallback | APIRoute | Drop-in GET handler for the OAuth callback | | handleSignOut | APIRoute | Drop-in GET handler that clears the session and logs out | | createCallbackHandler(opts?) | () => APIRoute | Callback with errorRedirect / onSuccess / onError | | createSignOutHandler(opts?) | () => APIRoute | Sign-out with a custom afterSignOutUrl | | verifyWebhook(ctx, opts?) | Promise<Event> | Verify + parse a WorkOS webhook request | | @workos/authkit-astro/components | Astro components | AuthState, Show, SignedIn, SignedOut, buttons, Impersonation | | @workos/authkit-astro/client | nanostores | $auth, $user, $signedIn, $isLoaded, …, hydrateAuth | | @workos/authkit-astro/react | hooks | useAuth(), useUser() (no extra deps) |

How it works

  • The middleware calls withAuth() once per request, exposes the result on Astro.locals.auth, and persists a refreshed token when one is issued.
  • Cookies (session + the short-lived PKCE verifier) are read and written through Astro's native context.cookies, which Astro flushes onto the response automatically — no manual Set-Cookie handling.
  • Sign-in is PKCE-bound: the login route writes a per-flow verifier cookie that the callback verifies before exchanging the code.
  • returnTo values are restricted to relative paths everywhere, so the login/logout flows can't be used as open redirects.
  • The client store is hydrated from a client-safe projection of the session (toClientAuth) — the access token is never serialized to the browser.

Example

A complete runnable app using the integration lives in example/.

License

MIT