Starter kit
An Astro starter, and what writing it taught us about our own SDK
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.

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
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. A missing variable then fails at boot, naming the variable, instead of turning into a 401 an hour later.
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.

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 other starters: Next.js with auth and billing, Astro, and the digital shop.
