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.
HTTP es un protocolo sin memoria: cada petición llega huérfana, sin saber nada de las que la precedieron. Para que una aplicación reconozca al mismo usuario entre una petición y la siguiente hay que reintroducir estado, y el vehículo clásico es la sesión: un puñado de datos que viajan, cifrados y firmados, dentro de una cookie que el navegador reenvía en cada llamada. En SolidStart no montas ese mecanismo a mano —lo provee useSession, el helper de sesiones de vinxi, la capa de servidor sobre h3 que da potencia a Start—. Sella la cookie, la verifica al volver y te entrega su contenido como un objeto que lees, mutas y destruyes. Esta lección disecciona ese ciclo.
- Crear una sesión con
useSessiondevinxi/httpy entender que vive solo en el servidor. - Leer el estado con
session.data, mutarlo consession.updatey destruirlo consession.clear. - Comprender qué hace el
password: cifra y firma la cookie para que el cliente no pueda leerla ni falsificarla. - Configurar el nombre y los atributos de la cookie, y guardar el secreto en una variable de entorno privada.
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.
// src/lib/sesion.ts
import { 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.
// 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.
# Genera un secreto fuerte y guardalo en .env como SESSION_SECRET
openssl 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.
flowchart LR A[navegador manda cookie sellada] --> B[useSession desella y verifica la firma] B --> C[session data lista para leer] C -->|session update| D[reescribe y vuelve a sellar] D --> E[Set-Cookie en la respuesta] style B fill:#89b4fa,color:#11111b style C fill:#a6e3a1,color:#11111b style E fill:#f9e2af,color:#11111b
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.
// El cliente nunca desella la cookie: pide el dato a una funcion de servidor
import { 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.
Que useSession sea server-only es una garantía, no un estorbo. Si el password o el contenido desellado llegaran al cliente, cualquiera podría forjar sesiones a voluntad. Manteniéndolos en el servidor, la cookie que viaja es opaca —solo el servidor que la selló sabe leerla— y el navegador se limita a transportarla de vuelta en cada petición. Cuando necesites datos de sesión en la UI, devuélvelos desde una función de servidor; nunca intentes leer ni descifrar la cookie en el cliente.
Sellada, no firmada
El password cifra y autentica el contenido: el cliente no lo lee ni lo altera sin que la verificacion falle.
Solo servidor
useSession toca cabeceras HTTP y el secreto. Vive tras use server, en funciones de servidor y rutas de API.
data, update, clear
Leer el estado, fusionar cambios reescribiendo la cookie, y vaciarla. Todo el ciclo en tres operaciones.
Una cookie tiene un límite práctico de unos 4 KB y viaja en cada petición al dominio, así que llenarla de datos encarece todo el tráfico. Guarda lo mínimo —un userId, quizá un rol— y busca el resto en tu base de datos cuando lo necesites. Y recuerda el reverso de una sesión autocontenida: como el servidor no guarda copia, no puedes revocar una cookie concreta desde el servidor sin añadir un campo de versión que compares, o sin migrar a sesiones con almacén. Con solo el password, tu única revocación global es rotarlo, lo que echa a todos a la vez.
El giro conceptual que hay que interiorizar es que una sesión sellada traslada el almacén de sesiones al propio cliente. En el modelo clásico de sesión en base de datos, la cookie guarda un identificador opaco y el servidor mantiene, en su propio almacén, la tabla que asocia ese identificador con los datos del usuario; la confianza nace de una búsqueda —el servidor mira su tabla y decide si te conoce—. En el modelo de useSession no hay tabla: el dato del usuario viaja dentro de la cookie, y la confianza nace de la criptografía —el servidor confía en la cookie porque él mismo la selló con un secreto que nadie más posee, y lo comprueba desellándola—. Esa diferencia tiene consecuencias de arquitectura enormes. Sin almacén de sesiones, tu backend es apátrida: cualquier instancia detrás de un balanceador puede atender cualquier petición sin compartir estado, porque toda la autoridad viaja con el cliente y se valida con la clave que todas comparten. Escalas horizontalmente sin pegar sesiones a un servidor concreto ni montar un Redis compartido. El precio de esa elegancia es simétrico: pierdes la revocación instantánea que te daba la tabla —no puedes «borrar la fila» de una sesión que ya no vive en ningún sitio— y aceptas el techo de tamaño de la cookie. Entender este eje —autoridad por búsqueda frente a autoridad por firma— es lo que te deja elegir con criterio: sesión sellada cuando priorizas escalado y simplicidad; identificador más almacén cuando necesitas revocar al instante y guardar más de lo que cabe en 4 KB. useSession es la primera; el resto de este nivel construye la autenticación sobre ella.
- Crea
src/lib/sesion.tscon un helperusarSesiontipado que leaSESSION_SECRETy nombre la cookiesesion; confirma en las DevTools que la cookie sale comohttpOnlyySecure. - Escribe
anotarUsuarioy comprueba que tras llamarla la respuesta trae una cabeceraSet-Cookiecon el nuevo valor sellado, ilegible a simple vista. - Lee
session.data.userIddesde otra función de servidor y verifica que persiste entre peticiones sin que tú guardes nada en el servidor. - Llama a
session.clear, observa que la cookie caduca, y razona por qué eluserIddesaparece sin borrar ninguna fila en ninguna parte. - Cambia
SESSION_SECRETpor otro valor y explica por qué todas las sesiones abiertas dejan de validar de golpe: acabas de descubrir tu único botón global de logout.