There is an Astro starter now, at rekey-dev/astro-starter. Sessions, sign-in, plans, checkout, entitlements and credits, and the pages ship no JavaScript at all. Writing it turned up two things about our own SDK that are worth saying out loud.

The Astro starter's home page, with a heading about auth and billing wired up and a grid describing each integration file.
Same shape as the Next.js kit, none of the JavaScript.

There is no Astro package, so the kit is the adapter

We publish adapters for Node and Next. Astro users get the node SDK and write their own session layer, so the starter includes one, in full, at about ninety lines. Two cookies, a read that refreshes, a write. It is not hidden in a helper because the two lines that matter are easy to get wrong quietly.

src/lib/session.ts

export async function getSession(cookies, request) {
  const access = cookies.get(ACCESS_COOKIE)?.value;
  if (access) {
    try {
      return { user: await rekey.auth.getCurrentUser(access), accessToken: access };
    } catch (err) {
      // Only an invalid token falls through to a refresh. Anything else is a
      // real error and is rethrown, so an API blip does not present itself to
      // your users as a mass logout.
      if (!(err instanceof RekeyError) || err.code !== 'USER_TOKEN_INVALID') throw err;
    }
  }
  // ... refresh, or null ...
}

The other one is the Secure flag on the session cookie. It is decided per request, from x-forwarded-proto and the host, rather than from import.meta.env.PROD. That is a build-time answer to a request-time question, and it fails in the expensive direction: guessing wrong on a real host means the browser refuses the cookie, which is loud and takes one variable to fix, while guessing wrong the other way puts a session credential on the wire in cleartext and nothing anywhere looks broken.

The cookie names and lifetimes match @rekey.dev/nextjs deliberately, so an app that moves between the two frameworks does not sign everybody out.

This should be a package

Every Astro user is now going to write their own version of both rules. That is tracked as issue 14.

Secrets that are not baked into the build

The obvious way to read a secret in Astro is import.meta.env.REKEY_SECRET. Vite inlines that at build time, so the key ends up in dist/, and a container built once and run in two environments carries the wrong one with nothing in the source to show it.

The starter declares its variables in astro.config.mjs and imports them from astro:env/server, which resolves at runtime and names the variable when it is missing.

Two corrections to that, both found by running the built server rather than reasoning about it. The built server does not read .env at all, because Vite loads that for astro dev only. And a missing variable does not fail at boot: the server starts and listens perfectly happily, then returns 500 on the first request. A health check that only asks whether the port is open will call that deploy healthy.

Why there is no React in it

This was meant to be the neat part. <SignIn> from @rekey.dev/react is stateless, and given an actionUrl it renders a plain form. Rendering it in Astro with no client directive should have given a real form with no JavaScript.

What it actually gives you is correct markup with no styling at all, because the package injects its stylesheet from a client effect that never runs. Adding client:load fixes the look by shipping React to a page that otherwise needs none.

So the starter writes the form in Astro, in about forty lines you can restyle by editing them, and React left the project entirely. That is the right answer for a starter and the wrong answer for the SDK, which is filed as issue 18.

The Astro starter's sign-in page, a plain card with email and password fields and a create account link.
A form, posting to an endpoint. Zero islands on the page, verified against the built server.

What the form posts to

src/pages/api/sign-in.ts

export const POST: APIRoute = async ({ request, cookies, redirect, url }) => {
  const form = await request.formData();
  const outcome = await rekey.auth.signIn({
    email: String(form.get('email') ?? ''),
    password: String(form.get('password') ?? ''),
  });

  if (outcome.mfaRequired) return redirect('/sign-in?mfa=1');

  setSession(cookies, request, outcome);
  const next = url.searchParams.get('next') ?? '/dashboard';
  // Only ever a path on this site; an absolute URL would make this an open redirect.
  return redirect(next.startsWith('/') && !next.startsWith('//') ? next : '/dashboard');
};

Middleware puts the session on Astro.locals once per request, and pages guard themselves with two lines. Billing is the same API as everywhere else: read the plans, post a slug, redirect to the checkout URL.

The bug that only appears once you deploy

The first version of this kit passed every local test and was completely non-functional in production. Astro checks the Origin header on form posts, and it builds the URL it compares against from the socket. Behind a TLS-terminating proxy that is http://your-host while the browser sends Origin: https://your-host. They do not match, and every sign-in, sign-up, checkout and cancel returns a bare 403.

Astro trusts X-Forwarded-Proto only once you tell it which host is yours, via security.allowedDomains. The kit sets that from PUBLIC_APP_URL now, and the README says it has to be present at build time, because that config file runs at build.

Found by asking, not by assuming

Three reviewers went over these kits and then everything was run against a real API. This one came out of the second half: it is invisible to a type checker, invisible to a local run, and invisible to a reviewer who does not know that particular Astro default.
An Astro starter for auth and billing, with no JavaScript shipped | Rekey