Leer variables: astro:env/client, /server y getSecret
Cómo se consume el esquema de astro:env desde el código: los módulos virtuales astro:env/client y astro:env/server, la importación por nombre ya tipada, la función getSecret para leer secretos en runtime cuando su valor solo existe en la plataforma, y cuándo valida Astro cada variable, en el build para las públicas y en ejecución para los secretos salvo que actives validateSecrets.
Declarar el esquema es la mitad del contrato; la otra mitad es leerlo. astro:env no te devuelve las variables por el viejo import.meta.env, sino por dos módulos virtuales que Astro fabrica a partir de tu esquema: astro:env/client y astro:env/server. La separación no es cosmética. Cada módulo expone solo lo que su contexto puede ver, de modo que la frontera cliente/servidor que declaraste deja de ser una promesa y se vuelve una barrera que el propio sistema de importaciones vigila. Y para los secretos que ni siquiera existen cuando compilas —porque solo aparecen en la plataforma de despliegue— hay una puerta especial: getSecret.
- Importar variables públicas ya tipadas desde
astro:env/clientyastro:env/server. - Entender por qué cada módulo solo expone las variables de su contexto.
- Usar
getSecretpara leer secretos cuyo valor solo se conoce en tiempo de ejecución. - Saber cuándo valida Astro cada variable: en el build o en ejecución, según su naturaleza.
Dos módulos, una frontera respetada
Una vez definido el esquema, cada variable se importa por su nombre desde el módulo que le corresponde. Las variables de context: 'client' viven en astro:env/client; las de context: 'server' —públicas y secretas— viven en astro:env/server. La importación no te da texto crudo: te da el valor ya convertido al tipo que declaraste.
// Cliente: solo ve las variables context client
import { PUBLIC_API_URL } from 'astro:env/client';
// Servidor: ve las variables server, publicas y secretas
import { PORT, API_SECRET } from 'astro:env/server';
Repara en lo que desaparece respecto a import.meta.env. No hay conversión manual: PORT llega como número porque el esquema lo declaró number. No hay nombres mágicos escritos como cadenas: importas identificadores reales que el editor autocompleta y renombra. Y no hay duda sobre la presencia: si una variable requerida falta, el fallo salta antes, no en la línea donde intentas usarla.
La barrera es estricta y esa es su virtud. Si intentas importar API_SECRET desde astro:env/client, no obtienes undefined ni un valor vacío que se cuele silenciosamente hasta producción: obtienes un error que impide compilar. El módulo de cliente sencillamente no exporta ese nombre, porque el esquema declaró la variable como de servidor. Con import.meta.env un secreto mal ubicado se degradaba a undefined y el problema aparecía tarde; aquí la frontera es una lista de exportaciones que no se puede burlar por descuido.
getSecret: leer lo que solo existe en runtime
La importación directa de un secreto asume que su valor puede conocerse cuando Astro construye el módulo. Pero muchos secretos no cumplen eso: la clave de una pasarela de pago no está en tu máquina ni en el build, solo aparece inyectada en el entorno del servidor de despliegue, en el instante en que atiende una petición. Para ese caso existe getSecret, una función que también se importa de astro:env/server y lee el valor por su nombre en tiempo de ejecución.
import { getSecret } from 'astro:env/server';
export const GET = () => {
const clave = getSecret('PAYMENT_KEY'); // string | undefined, sin validar
if (!clave) return new Response('config incompleta', { status: 500 });
// usar la clave para firmar o autenticar...
};
La diferencia con la importación por nombre es profunda. El identificador importado es un valor validado y tipado que el esquema garantiza; getSecret es una lectura dinámica y en crudo que devuelve una cadena o undefined, sin pasar por la validación del esquema. Por eso getSecret sirve para dos cosas: leer un secreto que solo aparece en runtime, y leer una variable que ni siquiera está en el esquema. Es la vía de escape deliberada para lo que no se puede conocer de antemano, con el coste justo de que tú te encargas de comprobar que llegó.
getSecret no lee siempre del mismo sitio: delega en el adapter para saber de dónde salen las variables en cada runtime. En un servidor Node lee del entorno del proceso; en un runtime del edge puede leer del objeto de entorno que la plataforma inyecta en cada petición. Esa indirección es justo lo que te permite escribir el mismo código de lectura sin importar dónde despliegues, porque el adapter traduce el concepto de secreto de runtime al mecanismo concreto de la plataforma que tengas debajo.
Un uso frecuente de getSecret que conviene destacar es leer variables que ni siquiera están en el esquema. A veces una biblioteca de terceros espera encontrar su clave en una variable con un nombre fijo que tú no declaraste; getSecret te deja alcanzarla sin forzarla dentro de tu contrato, reconociendo que no toda variable del entorno es tuya ni tiene por qué pasar por tu validación.
Una división útil: declara en el esquema las variables que tu código posee y controla, y reserva getSecret para las que pertenecen a otra herramienta o solo existen en la plataforma. Así tu esquema sigue siendo un retrato fiel de la configuración que tú gobiernas, sin inflarse con nombres que no son tu responsabilidad, y getSecret queda como la puerta honesta hacia todo lo demás.
Cuándo se valida cada variable
El esquema promete validar, pero no todo se valida en el mismo momento, y la razón es puramente física: no puedes comprobar en el build un valor que aún no existe. Astro reparte la validación según la naturaleza de cada variable.
astro:env/client
Expone las variables client mas public. Se validan e inlinean en el build, como las clasicas PUBLIC.
astro:env/server
Expone las variables server, publicas y secretas, ya tipadas. Tambien ofrece la funcion getSecret.
getSecret
Lectura dinamica por nombre en runtime. Devuelve cadena o indefinido, sin validar contra el esquema.
validateSecrets
Opcion que fuerza a validar tambien los secretos en el build, si su valor esta disponible entonces.
Las variables públicas —de cliente o de servidor— se validan en el build, porque su valor está presente cuando compilas y no hay motivo para posponer el chequeo. Los secretos, en cambio, se validan por defecto en tiempo de ejecución, cuando el módulo se carga en el servidor, precisamente porque su valor puede no existir todavía durante el build. Si sabes que tus secretos sí están disponibles al compilar y prefieres que su ausencia rompa el build en lugar de la primera petición, actívalo explícitamente.
// astro.config.mjs
export default defineConfig({
env: {
schema: { /* ... */ },
validateSecrets: true, // valida tambien los secretos en el build
},
});
flowchart LR SCH[esquema en la config] --> GEN[Astro genera modulos virtuales] GEN --> CLI[astro env client] GEN --> SRV[astro env server] CLI --> BUILD[validado en el build] SRV --> BUILD SRV --> GS[getSecret lee en runtime] GS --> RT[validado o crudo en ejecucion] style GEN fill:#89b4fa,color:#11111b style BUILD fill:#a6e3a1,color:#11111b style RT fill:#f9e2af,color:#11111b
La regla práctica para decidir entre la importación por nombre y getSecret no es de gusto, es de tiempo. Si el valor existe cuando compilas y quieres que su ausencia sea un error de build, impórtalo por nombre y deja que el esquema lo valide. Si el valor solo aparece en la plataforma en cada petición, usa getSecret y valida su presencia tú mismo. Mezclar los dos criterios —querer validar en build algo que solo llega en runtime, o leer con getSecret algo perfectamente conocido de antemano— es la fuente de los enredos más comunes al migrar a astro:env.
Errores tempranos y tipos generados
El mayor beneficio diario del esquema no es la lectura tipada, sino lo que ocurre cuando algo va mal. Si una variable requerida falta, Astro no te deja avanzar con un undefined que reventará tres capas más adentro: detiene el arranque con un mensaje que nombra la variable ausente y el módulo que la esperaba. El fallo aparece donde se puede arreglar —la configuración del entorno— y no en la línea remota donde por fin se usaba.
# Fallo temprano al arrancar sin una variable requerida por el esquema
[astro:env] API_SECRET es requerida por el esquema pero no esta definida
Ese diagnóstico depende de que el esquema sea la única fuente de verdad. Como Astro conoce el nombre, el tipo y la obligatoriedad de cada variable, los comprueba todos de una vez al inicio y te ofrece, además, autocompletado real: al escribir el import desde astro:env/server, el editor propone exactamente los nombres declarados y ninguno inventado. Los tipos no los escribes tú; los genera Astro a partir del esquema y los mantiene sincronizados con él, de modo que renombrar una variable en la configuración se propaga a cada uso.
Un efecto secundario valioso es que el esquema se vuelve documentación ejecutable. Quien llega al proyecto no rastrea los .env ni pregunta qué variables hacen falta: las lee en un solo lugar, con su tipo y su naturaleza declarados. Y a diferencia de un comentario, esta documentación no puede quedar desfasada sin que se note, porque es la misma que la máquina usa para validar. Si miente, el proyecto deja de arrancar; esa es la clase de documentación en la que se puede confiar.
Recuerda la contrapartida de la puerta dinámica. Un secreto importado por nombre pasa por el esquema y llega garantizado; el mismo secreto leído con getSecret llega como cadena o undefined, sin que nadie compruebe su forma. Si eliges getSecret, hazte cargo tú de lo que el esquema haría: verifica que el valor existe y tiene sentido antes de usarlo, porque aquí no hay una red debajo que atrape su ausencia.
Merece la pena insistir en un punto práctico antes de la reflexión final: la elección del módulo la dicta el esquema, no tú. No decides en el import si una variable es de cliente o de servidor; esa decisión ya la tomaste al declararla, y el módulo del que puedes importarla es su consecuencia directa. Si alguna vez te sorprende no encontrar un nombre donde lo buscas, la respuesta no está en el import sino en el esquema: vuelve a él y revisa qué context le diste, porque ahí se decidió a qué lado de la frontera vive esa variable para siempre.
Una forma rápida de aprender la frontera es dejar que la herramienta te la enseñe. Escribe el import desde astro:env/client y mira qué nombres te ofrece el autocompletado: solo verás las variables de cliente. Repite con astro:env/server y aparecerán las de servidor junto a getSecret. Esa lista, generada del esquema, es la frontera hecha visible, y consultarla es más fiable que recordar de memoria dónde pusiste cada variable.
La decisión más elegante de astro:env es que la seguridad no vive en una función que compruebas, sino en la forma de un módulo que importas. Piensa en lo que significa que astro:env/client sencillamente no exporte tus secretos. No hay una comprobación que decida, en cada acceso, si tienes permiso; hay una ausencia estructural. El secreto no está prohibido en el cliente: no está. Y como el sistema de módulos es el mismo que resuelve todas tus importaciones, esa frontera la vigila la herramienta más incansable que tienes, el resolvedor de módulos, que jamás se cansa ni se distrae ni hace una excepción por las prisas del viernes. Esto ilustra un patrón que reaparece en el buen diseño una y otra vez: convertir una regla de política en una propiedad de la estructura. Una política —no leas secretos en el cliente— depende de que alguien la recuerde y la respete; una estructura —el cliente no tiene de dónde leerlos— no depende de nadie. Y observa el segundo acierto: getSecret existe como puerta explícita y estrecha para lo que la estructura no puede conocer de antemano, los valores que solo el runtime posee. En vez de debilitar toda la frontera para acomodar ese caso, Astro abre una única salida nombrada, con su propio contrato —devuelve cadena o indefinido, tú validas— de modo que lo dinámico queda confinado y señalado en lugar de contaminarlo todo. Esa es la firma del diseño maduro: haz que lo seguro sea lo estructural y lo por defecto, y que lo dinámico sea una excepción visible y acotada, nunca la regla difusa que todo lo permite.
- Importa una variable pública desde
astro:env/clienten un<script>y comprueba que llega tipada, sin conversión manual. - Importa una secreta desde
astro:env/serveren un endpoint y confirma que el mismo import desde el módulo de cliente no compila. - Sustituye esa lectura por
getSecrety razona qué cambia en el tipo devuelto y en quién valida la presencia. - Activa
validateSecretsen la configuración, quita un secreto del entorno y observa cómo el fallo se adelanta del runtime al build.