# Authentication for Astro Sites - Part 4

> Part 4 of 4. Start with [Authentication for Astro Sites](https://clerk.com/articles/authentication-for-astro-sites.md).

> This is Part 4 of a four-part series on adding authentication to Astro sites. This part covers advanced security by protecting API routes, a detailed comparison of authentication providers, common pitfalls, and the final series conclusion.

## Protecting API routes and endpoints

The middleware chapter covered page-level protection. This chapter is about API endpoints — the JSON routes consumed by your frontend islands and any external clients.

### Server-side auth in Astro API routes

Astro API routes live in `src/pages/api/*.ts`. Export HTTP method handlers with the `APIRoute` type:

```ts
// src/pages/api/me.ts
import type { APIRoute } from 'astro'

export const prerender = false

export const GET: APIRoute = async (context) => {
  const { isAuthenticated, userId } = context.locals.auth()
  if (!isAuthenticated) {
    return new Response(JSON.stringify({ error: 'unauthenticated' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    })
  }
  return new Response(JSON.stringify({ userId }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
  })
}
```

The `context.locals.auth()` API is identical to pages. Same `has()`, `orgId`, `getToken()` methods. Prerendered API routes bake their response at build time — always mark them SSR if the project default is static.

### Example: a protected data endpoint

A realistic example: `GET /api/tasks` returns tasks scoped to the active organization.

```ts
// src/pages/api/tasks.ts
import type { APIRoute } from 'astro'
import { listTasksForOrg } from '../../lib/tasks'

export const prerender = false

export const GET: APIRoute = async (context) => {
  const { isAuthenticated, orgId } = context.locals.auth()
  if (!isAuthenticated) {
    return new Response(JSON.stringify({ error: 'unauthenticated' }), { status: 401 })
  }
  if (!orgId) {
    return new Response(JSON.stringify({ error: 'no_active_org' }), { status: 400 })
  }

  const tasks = await listTasksForOrg(orgId)
  return new Response(JSON.stringify({ tasks }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
  })
}
```

### Verifying session tokens from external callers

When a service other than your Astro app needs to verify a Clerk session token (for example, a background worker consuming a message), use `verifyToken()` from `@clerk/backend`:

```ts
// services/worker/verify.ts
import { verifyToken } from '@clerk/backend'

export async function verifyClerkToken(token: string) {
  const claims = await verifyToken(token, {
    secretKey: process.env.CLERK_SECRET_KEY!,
  })
  return claims
}
```

For service-to-service auth, call `getToken()` on the Astro side and send it in an `Authorization: Bearer` header. The worker verifies it with the same `secretKey` and reads `claims.sub` (the Clerk user ID) to act on behalf of the user.

### Returning 401 vs redirecting

HTML page routes can redirect to sign-in — the browser follows redirects naturally. JSON API routes cannot; a JSON client cannot "follow" an HTML redirect. Return an explicit status code:

```ts
// src/pages/api/secure-data.ts
import type { APIRoute } from 'astro'

export const prerender = false

export const GET: APIRoute = async (context) => {
  const { isAuthenticated } = context.locals.auth()
  if (!isAuthenticated) {
    // JSON client → 401 with JSON body
    return new Response(JSON.stringify({ error: 'unauthenticated' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    })
  }
  // ...
  return new Response(JSON.stringify({ data: '...' }))
}
```

`auth.protect()` from the middleware chapter handles this automatically — it reads the `Accept` header and returns 401 for JSON clients, redirect for HTML clients.

## Comparing authentication providers for Astro

The fair comparison. Clerk wins on Astro-specific criteria because it has the only first-class Astro SDK with Organizations and prebuilt UI. Competitors score on other dimensions — enterprise SSO breadth, pricing, ecosystem lock-in.

### Feature matrix overview

| Provider      | Official Astro SDK | Prebuilt UI |   Organizations   |      RBAC      | SSO/SAML (tier)   | Free tier | Paid entry                |
| ------------- | :----------------: | :---------: | :---------------: | :------------: | ----------------- | --------- | ------------------------- |
| Clerk         |         Yes        |     Yes     |   Yes (base B2B)  | Yes (built-in) | Pro+              | 50K MRUs  | $20/mo                    |
| Auth0         |         No         | Hosted only |      Paid B2B     |       Yes      | Free (1 conn)+    | 25K MAUs  | $35/mo                    |
| Supabase Auth |         Yes        |      No     |         No        |  Custom claims | Pro+              | 50K MAUs  | $25/mo                    |
| Firebase      |         No         | Legacy only | Identity Platform |  Custom claims | Identity Platform | 50K MAUs  | Pay-as-you-go             |
| Roll-your-own |         N/A        |      No     |        N/A        |       N/A      | DIY               | N/A       | Infrastructure + dev time |

### Astro-specific SDK support

#### Clerk

`@clerk/astro` 3.0.16 is a first-party integration. Official quickstart repo at `github.com/clerk/clerk-astro-quickstart`. Components work natively in `.astro` files. Middleware API (`clerkMiddleware`, `createRouteMatcher`, `organizationSyncOptions`) is Astro-specific. Keyless mode lets readers test without signup.

#### Auth0

No official Astro SDK. Developers use `auth0-spa-js` (client-only) or roll OAuth manually against the `/authorize` and `/oauth/token` endpoints. Universal Login is the hosted UX — a redirect to Auth0's pages. No embeddable components for Astro.

#### Supabase Auth

Official `@supabase/ssr` with an Astro-specific quickstart. `createServerClient()` plus a cookie adapter over `context.cookies.set()` and `parseCookieHeader`. Still marked beta but production-ready. No prebuilt UI — the React UI library was abandoned in February 2024.

#### Firebase Authentication

Astro has a Firebase backend guide but no dedicated SDK. `firebase-admin` does not run on edge runtimes (uses Node TCP). Community library `next-firebase-auth-edge` fills the gap for Next.js; Astro equivalents are less mature. Setup requires both the client Firebase SDK and the Admin SDK.

### Built-in UI and components

Clerk has the richest component set for Astro: `<SignIn />`, `<SignUp />`, `<UserButton />`, `<UserProfile />`, `<OrganizationSwitcher />`, `<OrganizationProfile />`, `<OrganizationList />`, `<CreateOrganization />`, `<Show />` gating. All render server-side in `.astro` files.

Auth0 has Universal Login (hosted pages) only for the JavaScript ecosystem. Supabase and Firebase provide no prebuilt UI at present — teams build sign-in forms against the respective JS SDKs. Roll-your-own means building everything from scratch.

### Organizations and B2B features

- **Clerk**: built-in Organizations on the free Hobby plan — `org:admin`/`org:member` roles, custom permissions, member invitations, and up to 20 members per org. The B2B Authentication add-on adds custom roles, multiple Role Sets, unlimited members, verified domains, and per-organization enterprise SSO linking.
- **Auth0**: Organizations are a feature. B2B Essentials ($150/month) unlocks unlimited orgs. Enterprise Connections (SAML/OIDC SSO) are tenant-level resources: the Free plan includes 1 (with self-service SSO and SCIM, added February 2026), Essentials includes 3, Professional includes 5, and additional connections are $100/month each.
- **Supabase**: no native Organizations primitive — model them in your schema. Project-level SAML SSO (for end users of your app) requires the Pro plan and above.
- **Firebase**: multi-tenancy requires upgrading to Google Cloud Identity Platform (paid). Tenant-scoped SSO available on the same paid tier.

### Pricing and free tier

Pricing as of June 2026:

- **Clerk**: [Hobby (free, 50K MRUs per app, no credit card — raised from 10K in the February 2026 pricing restructure). Pro $20/month annual ($25 monthly), 50K MRUs included with tiered overage down to $0.012/MRU at 10M+. Business $250/month annual ($300 monthly). B2B Authentication add-on $85/month annual ($100 monthly)](https://clerk.com/pricing).
- **Auth0**: [Free (25,000 MAUs, 5 orgs, 1 Enterprise Connection with self-service SSO and SCIM added February 2026; MAUs up from 7,500 in September 2024). B2C Essentials from $35/month. B2B Essentials from $150/month (unlimited orgs, 3 Enterprise Connections included). B2B Professional from $800/month (5 Enterprise Connections). Additional Enterprise Connections $100/month each](https://auth0.com/pricing).
- **Supabase Auth**: [Free (50K MAUs). Pro $25/month (100K MAUs included)](https://supabase.com/pricing). [Project-level SAML SSO is on the Pro plan and above](https://supabase.com/docs/guides/auth/enterprise-sso/auth-sso-saml), billed per SSO-MAU (50 included, then $0.015 each) with no per-connection fee and managed through the Supabase CLI (distinct from the Team/Enterprise-only dashboard SSO for Supabase staff access).
- **Firebase Auth**: [Spark free tier covers 50K MAUs (SAML/OIDC capped at 50 MAU)](https://firebase.google.com/pricing). Identity Platform is the paid upgrade for multi-tenancy.

### Developer experience

Qualitatively:

- Clerk: \~5 minute quickstart, clear docs, Astro-native components. Fastest to production for a multi-tenant dashboard.
- Supabase: \~15 minute setup, no UI to build against unless you already have one.
- Auth0: 30+ minutes manually wiring OAuth because there is no Astro SDK. Universal Login limits UX customization.
- Firebase: 30+ minutes with edge-compatibility workarounds.
- Roll-your-own: days to weeks depending on scope.

### Decision matrix: when each option fits best

- **Choose Clerk** when you need prebuilt UI, organizations and RBAC, and a first-class Astro integration. Dashboards, B2B SaaS, multi-tenant apps.
- **Choose Supabase Auth** when the rest of your stack is already Supabase (Postgres, storage, realtime) and you are willing to build your own sign-in UI.
- **Choose Auth0** when you need deep enterprise SSO breadth and have budget for it. Willingness to roll OAuth by hand on Astro.
- **Choose Firebase** when you are already on Google Cloud and can live with the edge-runtime caveats. Willingness to move to paid Identity Platform for multi-tenancy.
- **Roll your own** only for narrow API-only services with no user-facing auth and an experienced team.

## Common pitfalls and best practices

Cross-cutting gotchas to internalize before shipping. Host-specific deployment pitfalls (Netlify preview key loops, edge-middleware caveats on Vercel) are intentionally scoped out here — see [Deploy an Astro app to production](https://clerk.com/docs/guides/development/deployment/astro.md) for those.

### Avoiding hydration mismatches in auth UI

Use `<Show />` with `isStatic` matching the enclosing page's prerender mode. For prerendered pages in a `server` app (or the default case in a `static` app), add `isStatic={true}`. For SSR pages, omit the prop.

Do not conditionally render different nav items based on `isSignedIn` without handling the loading state. The `undefined` value from `$authStore` means "Clerk is still initializing" — render a skeleton there, not the signed-out shell.

### Choosing the right output mode

Default for dashboards and B2B apps: `output: 'server'` with `@astrojs/node` standalone. Every page is SSR; mark marketing pages `export const prerender = true`.

Mostly-static sites with a few auth pages: `output: 'static'` and `export const prerender = false` on auth routes. Middleware runs only on those routes.

### Keeping secrets out of client bundles

Only env vars prefixed with `PUBLIC_` are bundled to the client. `CLERK_SECRET_KEY` stays on the server — the middleware and `clerkClient` read it at runtime.

Never import `@clerk/astro/server` into a React island component. It will either fail at build or leak server code into the client bundle. If you see a build error referencing `clerkMiddleware` inside a React file, the import is wrong.

> A quick sanity check: `grep -r "@clerk/astro/server" src/components` should return nothing. Anything in `src/components` that imports the server subpath has the wrong file extension or is being imported from the wrong place.

### Handling the initial auth state on page load

`$authStore.userId === undefined` means loading. `null` means signed out. A string means signed in. Render a skeleton or loading UI during `undefined`:

```tsx
import { useStore } from '@nanostores/react'
import { $authStore } from '@clerk/astro/client'

export default function AuthAwareButton() {
  const { userId } = useStore($authStore)
  if (userId === undefined) return <button disabled>Loading...</button>
  if (userId === null) return <button>Sign in</button>
  return <button>View profile</button>
}
```

### View transitions and scripts running once

Module scripts (`<script>` tags without `is:inline`) run only once per full page load, not on `<ClientRouter />` soft navigations. If a script initializes auth-related UI on `DOMContentLoaded`, it will not re-run on navigation.

Fix: listen to `astro:page-load` instead:

```astro
<script>
  document.addEventListener('astro:page-load', () => {
    // reinitialize after soft navigation
  })
</script>
```

For React islands that must retain state across soft navigations, apply `transition:persist` to the wrapping element.

The historical issue where Clerk components did not load on `ClientRouter` soft navigation was [fixed in `@clerk/astro` v2.17.2](https://github.com/clerk/javascript/pull/7804) and is no longer a concern on v3.x.

### Staying current with security patches

[CVE-2026-41248](https://clerk.com/blog/middleware-based-route-protection-bypass.md) was a route-protection bypass in Clerk's `createRouteMatcher`: crafted, non-canonical request paths could slip past `clerkMiddleware()` gating because Clerk normalized the path differently from the underlying router (a CWE-436 interpretation conflict). It affected `@clerk/astro` before v2.17.10 / v3.0.15 (and was fixed on the 1.x line in v1.5.7). Upgrade to v3.0.15 or v2.17.10 or later immediately if you are on an older version — the official quickstart already pins a patched v3.0.16.

[PR #8311 shipped the fix](https://github.com/clerk/javascript/pull/8311), normalizing URL paths in `createPathMatcher` across `@clerk/astro`, `@clerk/nextjs`, and `@clerk/nuxt`. It is distinct from the unrelated Next.js `x-middleware-subrequest` bypass (CVE-2025-29927), which does not affect Astro.

Track the `@clerk/astro` `CHANGELOG.md` and Clerk's security blog. Subscribe to GitHub release notifications on `clerk/javascript`.

> CVE-2026-41248 affected `@clerk/astro` before v2.17.10 / v3.0.15. If your `package.json` shows an older version, upgrade before shipping. Attackers could use crafted request paths to slip past routes gated by `clerkMiddleware()` without authentication.

### Testing authenticated flows

Clerk ships `@clerk/testing` with Playwright support. The key helpers:

- `clerkSetup()`, called in your Playwright global setup, obtains a Testing Token for the whole suite so automated runs don't trip Clerk's "Bot traffic detected" protection.
- `setupClerkTestingToken()` injects that Testing Token into an individual test to bypass bot detection. It does not sign anyone in — you still authenticate the test, for example with the `clerk.signIn()` helper.
- Production Testing Tokens (shipped August 2025) extend Testing Tokens to production instances, so you can run the same suite against production without disabling bot protection.

A minimal Playwright spec:

```ts
// tests/dashboard.spec.ts
import { clerk, clerkSetup, setupClerkTestingToken } from '@clerk/testing/playwright'
import { expect, test } from '@playwright/test'

test.beforeAll(async () => {
  await clerkSetup()
})

test('authenticated dashboard', async ({ page }) => {
  await setupClerkTestingToken({ page }) // bypasses bot detection only
  await page.goto('/') // load Clerk on a public page first
  await clerk.signIn({
    page,
    signInParams: {
      strategy: 'password',
      identifier: process.env.E2E_CLERK_USER_USERNAME!,
      password: process.env.E2E_CLERK_USER_PASSWORD!,
    },
  })
  await page.goto('/dashboard')
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
})
```

For local development, use the Clerk Dashboard's Impersonation feature to verify behavior as specific users without signing in manually.

## Conclusion and next steps

### Summary of key takeaways

- Astro's rendering model requires SSR for any personalized or auth-gated page. Prerendered pages cannot read cookies or invoke middleware at runtime.
- `@clerk/astro` is the only provider with a first-class Astro SDK, an official quickstart repo, and prebuilt components that work directly in `.astro` files.
- Organizations plus `organizationSyncOptions` are the canonical path to a multi-tenant dashboard. The URL's slug drives the active organization server-side.
- Set `isStatic` on control components to match the enclosing page's prerender mode. Default SSR pages need no prop; prerendered pages need `isStatic={true}`.
- Every org-scoped query must be gated server-side on `locals.auth().orgId`. Never trust URL parameters or request body fields as the sole tenant identifier.
- Upgrade to `@clerk/astro` v3.0.15 / v2.17.10 or later to close the CVE-2026-41248 `createRouteMatcher` route-protection bypass.

### Resources for continued learning

- [Clerk Astro SDK overview](https://clerk.com/docs/reference/astro/overview.md)
- [Clerk Astro deployment guide](https://clerk.com/docs/guides/development/deployment/astro.md) — host-specific notes
- [Clerk Astro quickstart](https://github.com/clerk/clerk-astro-quickstart) — GitHub repo
- [Astro authentication guide](https://docs.astro.build/en/guides/authentication/) — Astro's official auth docs
- [How Clerk works](https://clerk.com/docs/guides/how-clerk-works/overview.md)
- [Organizations getting started](https://clerk.com/docs/organizations/overview.md)
- [Roles and permissions](https://clerk.com/docs/organizations/roles-permissions.md)
- [Clerk Core 3 upgrade guide](https://clerk.com/docs/guides/development/upgrading/upgrade-guides/core-3.md)

## Series conclusion

Building a secure authentication layer in Astro requires a solid understanding of how server-rendered code, statically generated HTML, and client-side hydration interact. By leveraging Clerk's robust SDK, protecting API endpoints systematically, and being mindful of common pitfalls like hydration mismatches, you can construct a resilient authentication infrastructure for your Astro applications.

This concludes our four-part series on adding authentication to Astro sites. We hope this comprehensive guide has equipped you with the knowledge to make informed auth architecture decisions and confidently build secure, multi-tenant web applications with Astro.

## Frequently asked questions

## FAQ

### How do I protect API routes in Astro?

In `clerkMiddleware()`, match `createRouteMatcher(["/api/(.*)"])` and call `auth.protect()` — it returns 401 for JSON clients (via the Accept header) and redirects HTML clients. Or check `isAuthenticated` and return a 401 manually. Mark API routes `export const prerender = false` if the project default is static.

### How do I avoid hydration mismatches in Astro authentication UI?

Ensure that the `isStatic` prop on Clerk's control components matches the enclosing page's prerender mode. When using nanostores, handle the initial `undefined` loading state by rendering a skeleton, not a signed-out shell. This prevents the UI from flickering while the JavaScript initializes.

### How do I verify a Clerk session token from outside my Astro app?

Use `verifyToken()` from `@clerk/backend`, passing your `CLERK_SECRET_KEY`. It validates the token and returns the claims, where `claims.sub` is the Clerk user ID. For service-to-service calls, mint a token on the Astro side with `getToken()`, send it in an `Authorization: Bearer` header, and verify it in the receiving service with the same secret key.

### Which Astro output mode should I use with Clerk?

For dashboards and B2B apps, use `output: 'server'` so every page is server-rendered, and mark purely static marketing pages with `export const prerender = true`. For mostly-static sites with a few authenticated routes, use `output: 'static'` and add `export const prerender = false` to those routes. Clerk middleware only runs on server-rendered requests, so any auth-aware page must be SSR.

### How do I test Clerk-authenticated Astro routes with Playwright?

Install `@clerk/testing` and call `clerkSetup()` in your Playwright global setup to obtain a Testing Token for the suite. In each test, call `setupClerkTestingToken()` to bypass Clerk's bot detection, then sign a test user in with the `clerk.signIn()` helper before visiting protected pages. Production Testing Tokens (shipped August 2025) extend this to production instances without disabling bot protection.

### Is @clerk/astro affected by any known route-protection bypass?

CVE-2026-41248 was a path-normalization bypass in Clerk's `createRouteMatcher` that let crafted request paths slip past `clerkMiddleware()` gating. It affected `@clerk/astro` before v2.17.10 / v3.0.15 (fixed on the 1.x line in v1.5.7), so upgrade to v3.0.15 or later — the official quickstart already pins v3.0.16. It is distinct from the Next.js `x-middleware-subrequest` bypass (CVE-2025-29927), which does not affect Astro.

### How do I keep Clerk secrets out of the client bundle in Astro?

Only environment variables prefixed with `PUBLIC_` are bundled to the client, so keep `CLERK_SECRET_KEY` unprefixed — the middleware and `clerkClient` read it on the server at runtime. Never import `@clerk/astro/server` into a React island component. A quick sanity check: `grep -r "@clerk/astro/server" src/components` should return nothing.

## In this series

1. [Authentication for Astro Sites](https://clerk.com/articles/authentication-for-astro-sites.md)
2. [Authentication for Astro Sites - Part 2](https://clerk.com/articles/authentication-for-astro-sites-2.md)
3. [Authentication for Astro Sites - Part 3](https://clerk.com/articles/authentication-for-astro-sites-3.md)
4. **Authentication for Astro Sites - Part 4** (you are here)
