How to Add Auth to a SolidStart App (Sessions, JWT, and OAuth)
A practical guide to adding authentication to a SolidStart app: server functions, encrypted session cookies, middleware, and OAuth — with code and trade-offs.
You picked SolidStart because it’s fast and the reactivity model is clean. Then you hit the part every app needs: someone has to log in. And now you’re staring at server functions, useSession, encrypted cookies, and middleware, trying to figure out how the pieces connect.
Adding auth to a SolidStart app is more straightforward than it looks, but the docs assume you already know which primitive does what. This guide walks through the realistic options — encrypted session cookies, JWT, and OAuth — shows real code, and is honest about where each one costs you time.
Why Auth in SolidStart Is Different
SolidStart is a full-stack meta-framework on top of SolidJS. The important detail for auth: session helpers only run on the server. useSession, getSession, and updateSession work inside server functions ("use server") and API routes because they need to read and write HTTP headers. You can’t call them from a component that runs in the browser.
That constraint is good. It pushes you toward server-validated sessions instead of the client-only JWT-in-localStorage pattern that keeps showing up in tutorials and keeps getting apps owned. If the server doesn’t verify identity on every request, you don’t have auth — you have a suggestion.
One more thing to know up front: SolidStart does not ship CSRF protection. If you authenticate with cookies, you handle CSRF yourself (SameSite cookies plus a token check on mutations).
Option 1: Encrypted Session Cookies (the built-in path)
SolidStart’s useSession gives you signed, encrypted cookies with no extra dependency. This is the path most indie apps should start with.
useSession needs a password at least 32 characters long for signing and encryption. Keep it in an environment variable, never in source.
// src/lib/session.ts
import { useSession } from "vinxi/http";
type SessionData = {
userId?: string;
};
export function getSession() {
return useSession<SessionData>({
password: process.env.SESSION_SECRET!, // 32+ chars
});
}
A login server function looks like this:
// src/lib/auth.ts
"use server";
import { getSession } from "./session";
import { verifyPassword, findUserByEmail } from "./users";
export async function login(email: string, password: string) {
const user = await findUserByEmail(email);
if (!user || !(await verifyPassword(password, user.passwordHash))) {
throw new Error("Invalid credentials");
}
const session = await getSession();
await session.update({ userId: user.id });
return { id: user.id, email: user.email };
}
export async function logout() {
const session = await getSession();
await session.update({ userId: undefined });
}
export async function getCurrentUser() {
const session = await getSession();
if (!session.data.userId) return null;
return findUserById(session.data.userId);
}
The session data is encrypted and signed inside the cookie, so the browser can’t read or tamper with it. For most MVPs that’s all you need. If you later want server-side revocation (kill a session immediately), store a session ID in the cookie and keep session records in your database instead of putting userId directly in the cookie.
Option 2: Middleware for Protected Routes
Checking the session in every server function gets repetitive. SolidStart middleware runs before requests and is the right place to load the user once and gate protected paths.
// src/middleware.ts
import { createMiddleware } from "@solidjs/start/middleware";
import { getSession } from "./lib/session";
export default createMiddleware({
onRequest: async (event) => {
const session = await getSession();
const userId = session.data.userId;
// Attach to locals so route handlers can read it
event.locals.userId = userId ?? null;
const url = new URL(event.request.url);
if (url.pathname.startsWith("/app") && !userId) {
return Response.redirect(new URL("/login", url), 302);
}
},
});
Register it in app.config.ts:
import { defineConfig } from "@solidjs/start/config";
export default defineConfig({
middleware: "./src/middleware.ts",
});
Now any route under /app requires a session, and your server functions can read event.locals.userId instead of re-parsing the cookie each time.
Option 3: OAuth and “Sign in with Google”
Most users would rather click “Continue with Google” than create another password. For OAuth in SolidStart you have two realistic choices:
- Auth.js (
@auth/solid-start) — has a SolidStart adapter and handles the OAuth dance plus session cookies. It’s the closest thing to a community standard, though the SolidStart integration has historically been rougher than the Next.js one. Test the cookie behavior before you commit. - Roll your own OAuth flow — redirect to the provider, handle the callback in an API route, exchange the code for a profile, then create your own session with
useSession. More code, but no framework coupling and you understand every line.
A minimal hand-rolled callback route:
// src/routes/auth/callback/google.ts
import type { APIEvent } from "@solidjs/start/server";
import { getSession } from "~/lib/session";
import { exchangeCodeForProfile, upsertUser } from "~/lib/oauth";
export async function GET(event: APIEvent) {
const url = new URL(event.request.url);
const code = url.searchParams.get("code");
if (!code) return new Response("Missing code", { status: 400 });
const profile = await exchangeCodeForProfile(code);
const user = await upsertUser(profile);
const session = await getSession();
await session.update({ userId: user.id });
return Response.redirect(new URL("/app", url), 302);
}
Whichever path you pick, validate the state parameter to prevent CSRF on the OAuth callback. If you’re weighing OAuth against email-based flows, the magic link authentication guide covers the trade-offs in more detail.
Sessions vs JWT: Which One Here
Quick version for SolidStart:
- Encrypted session cookies — simplest, server-validated, easy to revoke if you store session records. Best default for a web app.
- JWT — useful if a separate mobile app or third-party client hits the same API. The downside is real: a JWT stays valid until it expires unless you build a blacklist. For a single web app, JWT usually adds complexity without buying you anything.
Start with sessions. Reach for JWT only when you actually have a non-browser client.
Where the Time Actually Goes
Login and logout are the easy 20%. The other 80% is the stuff nobody demos: email verification, password reset tokens with expiry, rate limiting on the login endpoint, session revocation, CSRF on every mutation, and then — the moment you want revenue — wiring Stripe customers to your user records, handling webhooks, and gating features by plan.
This is where Beag fits. Instead of building auth and payments from scratch in SolidStart, you drop in Beag and get both: server-validated authentication and Stripe subscriptions already connected. There’s no auth backend to maintain and no webhook plumbing to write. After login, the user’s email, plan, and subscription status are available on the client, so feature gating is a plan check:
function canUsePro() {
const user = JSON.parse(localStorage.getItem("beag_user") || "{}");
return user.plan === "pro" || user.plan === "enterprise";
}
You keep SolidStart for what it’s good at — a fast UI — and skip the weeks of auth and billing infrastructure.
A Sensible Plan
If you’re shipping a SolidStart MVP this week:
- Use
useSessionfor encrypted session cookies. - Add middleware to gate
/appand load the user once. - Add OAuth only if your users expect “Sign in with Google.”
- The moment you need to charge money, don’t hand-roll auth-plus-Stripe — use an all-in-one so payments and identity stay in sync.
Auth is infrastructure, not a feature. Get it right early so every record, route, and screen you build afterward assumes a known user from day one.
Add auth and Stripe payments to your SolidStart app in minutes. Get started with Beag — no auth backend, no webhook code.
For more framework guides, see how to add auth to a Next.js app or browse the Beag blog.
Ready to Make Money From Your SaaS?
Turn your SaaS into cash with Beag.io. Get started now!
Start 7-day free trial →