Saltar al contenido

Sesiones en SolidStart con useSession

El protocolo HTTP es amnésico: cada petición nace sin memoria de la anterior. Una sesión reintroduce continuidad guardando datos del usuario, cifrados y firmados, dentro de una cookie que el navegador reenvía en cada petición. En SolidStart el helper `useSession` de vinxi (la capa de h3 que da potencia a Start) gestiona ese ciclo completo: sella la cookie con un `password` de al menos 32 caracteres, la desella y verifica su firma al llegar, y expone `session.data`, `session.update` y `session.clear`. Corre solo en el servidor, porque necesita las cabeceras HTTP y un secreto que ningún cliente debe ver.

useSession: una sesión sellada, no solo firmada

useSession recibe una configuración con un password y devuelve una promesa del objeto de sesión. Como el secreto no debe aparecer jamás en el código, lo lees de una variable de entorno privada, y como la sesión solo tiene sentido en el servidor, la envuelves en un helper marcado con "use server". Tipar el contenido con un genérico te da autocompletado y seguridad en cada acceso posterior.

TypeScript
// src/lib/sesion.tsimport { useSession } from "vinxi/http";export type DatosSesion = {  userId?: string;};export async function usarSesion() {  "use server";  return useSession<DatosSesion>({    password: process.env.SESSION_SECRET as string,    name: "sesion",    cookie: {      httpOnly: true,      // invisible para el JavaScript del cliente      secure: true,        // solo viaja por HTTPS      sameSite: "lax",     // mitiga CSRF      maxAge: 60 * 60 * 24 * 7, // una semana en segundos    },  });}

Fíjate en el verbo que la documentación elige: la sesión no se «firma», se sella. h3 cifra el contenido y además lo autentica, de modo que el cliente ni puede leer lo que guardas ni puede alterarlo sin que la verificación falle al volver. Es una diferencia de fondo respecto a una cookie meramente firmada, donde el valor viaja en claro y solo se detecta la manipulación.

Leer, actualizar y limpiar

El objeto que recibes expone tres operaciones. session.data es el contenido actual —tipado con tu genérico—; session.update reescribe ese contenido y programa una nueva cookie Set-Cookie en la respuesta; session.clear la vacía y caduca la cookie. Las tres viven detrás de funciones de servidor porque tocan cabeceras HTTP que no existen en el cliente.

TypeScript
// src/lib/sesion.ts (continuacion)export async function usuarioActual() {  "use server";  const sesion = await usarSesion();  return sesion.data.userId ?? null;   // lectura}export async function anotarUsuario(userId: string) {  "use server";  const sesion = await usarSesion();  await sesion.update({ userId });      // reescribe y re-sella la cookie}export async function olvidarSesion() {  "use server";  const sesion = await usarSesion();  await sesion.clear();                 // vacia y caduca la cookie}

session.update fusiona lo que le pasas con lo existente, así que puedes actualizar un campo sin perder los demás; y admite tanto un objeto como una función prev => nuevo cuando el valor nuevo depende del anterior. Cada update o clear emite una cabecera Set-Cookie, motivo por el que estas operaciones solo tienen sentido durante una respuesta viva del servidor.

Una precisión sobre la lectura: session.data refleja lo que llegó en la cookie de esta petición. Si actualizas la sesión, el cambio viaja en el Set-Cookie de la respuesta y lo verás resuelto en la siguiente petición, cuando el navegador reenvíe la cookie reescrita. Distinguir lo que hay en el cable de lo que hay en tu objeto en memoria evita sorpresas al encadenar operaciones dentro de una misma petición.

El password: qué cifra y qué firma

El password es la clave maestra de todo el esquema. Con él, h3 deriva las claves que cifran el contenido y que calculan la etiqueta de autenticación; sin él, la cookie sería ilegible o falsificable. Por eso la regla es estricta: mínimo 32 caracteres, generado al azar y almacenado en una variable de entorno privada, nunca incrustado en el repositorio.

Shell
# Genera un secreto fuerte y guárdalo en .env como SESSION_SECRETopenssl rand -base64 32

Si no pasas name, la cookie se llama h3 por defecto; conviene darle un nombre propio para no filtrar qué herramienta hay debajo. Y rotar el secreto tiene una consecuencia deliberada: invalida de golpe todas las sesiones vivas, porque las cookies viejas ya no desellan con la clave nueva. Ese es, de hecho, tu único botón global de cierre de sesión.

Server-only: la sesión no cruza al cliente

Los helpers de sesión corren solo en el servidor, y no es un capricho: necesitan las cabeceras HTTP —que en el navegador no existen— y el password, que jamás debe salir del servidor. Por eso los envuelves en "use server". La consecuencia práctica es tranquilizadora: el cliente nunca desella la cookie ni ve el secreto, solo recibe lo que una función de servidor decide devolverle.

Para llevar un dato de la sesión a la interfaz, expones una función de servidor y la lees con el patrón canónico query más createAsync del nivel de datos. El cliente pregunta «¿quién soy?» y el servidor responde con lo mínimo —un userId, un nombre—, nunca con la cookie cruda.

TypeScript
// El cliente nunca desella la cookie: pide el dato a una funcion de servidorimport { query, createAsync } from "@solidjs/router";import { usuarioActual } from "~/lib/sesion";export const getUserId = query(() => usuarioActual(), "user-id");// En un componente cliente:// const id = createAsync(() => getUserId());

Es la misma frontera que gobierna todo SolidStart: el secreto y la lógica sensible se quedan en el servidor, y por el cable solo viaja el resultado serializado. La sesión sellada encaja en ese modelo sin fricción, porque su contenido nace y muere del lado servidor y el cliente solo conoce su sombra.