# Ejecutar, previsualizar y probar la app (/dev/es/run-and-test)



<CalloutContainer type="info">
  <CalloutDescription>
    Las comprobaciones de navegador usan el Chromium propio de Playwright (la tarea `browsers` lo instala). Todo se ejecuta localmente en el runtime de Workers, sobre la
    D1 local de Wrangler; no se necesita ninguna cuenta de Cloudflare, base de datos remota ni credenciales de producción. Las pruebas
    de navegador son dueñas de `PREVIEW_PORT` (por defecto 4173; cada agente define el suyo) y se niegan a reutilizar
    un proceso no relacionado. Detén una preview manual antes de una comprobación de navegador; el servidor de desarrollo en el 5173 puede seguir en ejecución.
    El Chromium de las comprobaciones es el de Playwright (la tarea `browsers`\: `playwright install chromium`); en Linux, los idiomas han
    también necesitan una fuente CJK\: `sudo apt-get install -y --no-install-recommends fonts-noto-cjk`, como la ejecutan los jobs de navegador de GitHub.
  </CalloutDescription>
</CalloutContainer>

## Un único scaffold para local y Cloudflare [#one-scaffold-for-local-and-cloudflare]

`wrangler.jsonc` es la fuente de verdad para la entrada del Worker, los flags de compatibilidad,
los bindings y la observabilidad. Vite genera `dist/server/wrangler.json` y
`.wrangler/deploy/config.json`; no edites a mano ni hagas commit de ninguno de los dos archivos generados.
Tanto la preview local como el deployment de Wrangler consumen la build de producción generada.
El desarrollo usa el mismo código fuente del Worker y el mismo runtime de Cloudflare con hot reload.

Todos los comandos de la aplicación, sus comprobaciones y sus deployments, con lo que hace cada uno\: [Comandos](./commands.mdx).
En resumen, la aplicación se ejecuta en local en el puerto 5173 con hot reload (`vp run dev`) y como build de producción en
el 4173 (`vp run preview`), y las comprobaciones se ejecutan en ese mismo host local. Cada rama distinta de `main` obtiene una
preview en Cloudflare\: [en Cloudflare](#on-cloudflare), más abajo. El navegador propio del agente en un portátil
(`pnpm dlx chrome-devtools-mcp`, Chrome DevTools) no es CI\: las comprobaciones nunca lo usan.

`vp run ci` ejecuta toda la suite sobre la build de producción, aquí y en GitHub. Para ejecutarla contra una URL
desplegada (la preview de una rama o producción)\:

```sh
pnpm remy test remote https://your-worker.your-subdomain.workers.dev
```

La URL debe ser un origin, sin path ni query. Las comprobaciones que
necesitan una persona nueva leen su código de la captura de correo local, así que solo se ejecutan contra
el destino local. El resto le preguntan al Worker qué entorno es (`/healthz`) y esperan lo que
[la tabla](./auth.md#environments-one-table) les dé\: contra una preview las personas precargadas están presentes
y una pulsación inicia sesión como una de ellas; contra producción nada de eso existe. La tarea de pruebas
remotas también puede apuntar a una preview local ya en ejecución para comprobar la ruta del servidor externo.

Los comandos son los de remy-cli (`remy`), y cada uno lee lo que necesita donde se guarda (los origins\: el `.env` de la app); los propios de este repositorio son sus tareas (el `vite.config.ts` de la raíz\: `ready`, `new-app`, `skills:build`,
`release:publish`) y sus scripts. La configuración de Playwright
es el `playwrightConfig()` del paquete.

Existe una única configuración de Worker, `wrangler.jsonc`\: producción, y su sección `previews` para la preview de cada rama
(con sus propias bases de datos y secreto, `ENVIRONMENT: "staging"`, de modo que el inicio de sesión de prueba está activo). No hay un
Worker de staging aparte\: una preview es eso (propietario, 2026-10-18). Mantén la implementación compartida; varía solo los
identificadores de recursos, los secretos y demás valores de entorno. La sección `previews` no hereda los bindings ni las vars\:
cada uno se declara de nuevo ahí. No dupliques la aplicación ni uses bindings remotos para el desarrollo local
habitual. Los secretos locales permanecen en `.dev.vars`, que está ignorado; los secretos desplegados se gestionan
a través de Cloudflare. Los datos locales no se suben con el deployment.

## Dos tipos de página [#two-kinds-of-page]

[`packages/remy-ui/src/paths.js`](https\://github.com/joeblew999/remy-auth/blob/main/packages/remy-ui/src/paths.js) es la única lista de páginas y dice qué
promete cada tipo; los dos nunca se mezclan.

* **Las páginas de sitio** (`sitePaths`\: el home y los formatos) son para Google\: completas en el HTML del servidor
  sin JavaScript, indexadas, en el sitemap con alternativas `hreflang`, y evaluadas por las auditorías de Google.
  Su marco es `SiteShell` en [`shell.tsx`](https\://github.com/joeblew999/remy-auth/blob/main/packages/remy-ui/src/shell.tsx), construido con partes
  estáticas de shadcn.
* **Las páginas de app** (`appPaths`, bajo `/app`) necesitan JavaScript, llevan `noindex` (añadido por `pageHead`)
  y quedan fuera del sitemap. Su marco es `AppShell` en
  [`app-shell.tsx`](https\://github.com/joeblew999/remy-auth/blob/main/packages/remy-ui/src/app-shell.tsx), el bloque sidebar-16 de shadcn, gestionado en
  [`blocks/sidebar-16`](https\://github.com/joeblew999/remy-auth/blob/main/packages/remy-ui/src/blocks/sidebar-16/README.md).

Los docs no son páginas de esta app\: son el Worker de docs (`apps/remy-auth-docs/` aquí, el `docs/` de una app;
Fumadocs), con la guía en `/docs`,
estos docs para desarrolladores en `/dev` y la referencia de la API en `/reference`, cada uno con búsqueda y Ask AI
([escribir docs](./writing-docs.mdx)). El encabezado de la app enlaza allí, y sus antiguas direcciones `/<locale>/docs/...`
redirigen allí de forma permanente.

El selector de idioma sigue el tipo\: las páginas de sitio usan `LanguageSwitcher`, enlaces simples que no necesitan
JavaScript; las páginas de app usan `LanguageMenu`, el DropdownMenu de shadcn que llama a `setLocale` de Paraglide
(ambos en [`language.tsx`](https\://github.com/joeblew999/remy-auth/blob/main/packages/remy-ui/src/language.tsx)).

## Qué está implementado [#what-is-implemented]

Cada página existe en cada locale (13 prefijos como `/en`, `/ar`, `/ja`; las páginas de docs solo en inglés); la tabla las nombra sin él.

| Ruta                                                           | Tipo              | Comportamiento                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| -------------------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/`, `/formats`, `/app`, ... (todas las rutas sin locale)      | Entrada           | Redirige al idioma del visitante\: la `cookie` de Paraglide (una elección recordada), luego `preferredLanguage` (Accept-Language), si no, inglés, con `Vary`. Estas URLs de entrada son los destinos `x-default`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `/en`                                                          | Sitio             | Home localizado con dirección, metadatos y enlaces alternativos                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `/en/formats`                                                  | Sitio             | Las convenciones del locale en cinco secciones (este idioma, fechas y horas, números, dinero, palabras), cada control por search-param en la sección que cambia; las fechas incluyen el lugar del visitante a partir de la geolocalización de la petición de Cloudflare, transmitida en streaming tras `Await` y nunca almacenada, y la propia zona horaria del dispositivo. `?currency`, `?count` y `?calendar` son search params tipados y validados (los valores por defecto se omiten de las URLs, los valores inválidos redirigen a la URL canónica); los datos del loader se mantienen frescos durante cinco minutos. El contenido es `FormatsContent`, compartido con `/app/formats`                                                                                                                                                                                                    |
| `/en/time-zones/Asia/Tokyo` (cualquier nombre IANA)            | Sitio             | Subrecurso de la página de formatos\: el nombre localizado de la zona, el offset y el instante de ejemplo ahí. Un nombre desconocido es un 404 localizado que lo nombra (`notFound()`); otra grafía de un nombre conocido responde 301. No está en el sitemap                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `/en/app`                                                      | App               | Tarjeta de estado en vivo\: TanStack Query sobre el `GET /api/status` del contrato (el propio router durante el SSR, HTTP en el navegador), renderizado en servidor, sondeado cada 10 s, con un refresco que invalida todos los loaders y queries a la vez (`invalidateEverything` en `@joeblew999/remy-ui/invalidate`); la tarjeta en sí es la de la parte status-card (`@joeblew999/remy-showcase/parts/status-card/card`), que remy-auth-app también muestra, consultada entre orígenes (CORS para su origen registrado, `src/api/origins.ts`)                                                                                                                                                                                                                                                                                                                                              |
| `/en/app/formats`                                              | App               | El contenido de la página de formatos dentro del marco de app                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `/en/app/demo`                                                 | App               | `ssr: false`. Contador y formulario de reserva validados en el navegador y de nuevo por el `POST /api/reservations` del contrato (una mutación de TanStack Query), que responde en el idioma de la página; las reglas incumplidas vuelven como su 400 tipado y se muestran como errores propios del formulario; salir con entradas sin guardar pregunta primero (`useBlocker`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `/en/app/location`                                             | App               | La ubicación de la petición según Cloudflare junto a la propia del dispositivo, que la API de Geolocation solo entrega después de que el visitante pulse su botón                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `/en/app/account`                                              | App               | Quién ha iniciado sesión (nombre y correo, con cierre de sesión), o el formulario de inicio de sesión\: una dirección de correo y después el código de seis cifras que Better Auth le envía (TanStack Form; el cliente de Better Auth llamando a `/api/auth`). El loader pregunta a una función de servidor que llama al `me` del contrato dentro del servidor, de modo que el servidor renderiza el estado correcto a partir de las cookies de la página y nunca lo cachea. El correo del código se escribe en el idioma de la página y se envía a través de Cloudflare Email Service. Donde el entorno ofrece personas precargadas (solo en local), el formulario las enumera con su rol y lo que poseen, con una sola pulsación para iniciar sesión como cada una. Donde no se pudo entregar ningún código de inicio de sesión, mantiene el estado vacío compartido en lugar del formulario |
| `/en/app/settings`                                             | App               | El idioma y la apariencia (guardados en el dispositivo), lo que el dispositivo le dice a la app, [el nombre del producto](#the-products-name) dónde se usa y cómo se leería con otro nombre, y [qué está desplegado](#which-version-is-deployed)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| `/en/app/notes`                                                | App               | Página propia de remy-auth, la demo de notas\: una persona que ha iniciado sesión escribe notas y comparte cada una con una dirección, para leer o para editar. Quién puede hacer qué lo deciden las relaciones (su autor, sus editores, sus lectores; `packages/remy-auth-contract/src/notes.ts` contiene el vocabulario), el servidor lo aplica mediante el guard, y la página muestra un control solo dentro de `<Allowed>`, a partir de los permisos que el servidor envió con cada nota. Sin sesión iniciada, apunta a la página de cuenta                                                                                                                                                                                                                                                                                                                                                |
| `/api/status`, `/api/me`, `/api/notes`..., `/api/reservations` | API               | Endpoints del contrato ([@joeblew999/remy-auth-contract](https\://github.com/joeblew999/remy-auth/blob/main/packages/remy-auth-contract/README.md)) servidos por oRPC tras una única ruta de servidor de Start (`src/routes/api.$.ts`, `src/api/`)\: entrada y salida validadas, errores tipados, el idioma a partir de Accept-Language (`routeStrategies` de Paraglide), sin locale en la URL. Cada uno se ejecuta tras el guard de la plataforma, que aplica su política de contrato\: `status` y `reservations` son públicos, `me` y el listado de notas responden solo a una persona que haya iniciado sesión y rechazan a cualquier otra con 401, y cada cambio a una nota es una acción que decide el motor de relaciones (404 para una nota que no existe, 403 sin una relación que lo permita)                                                                                         |
| `/api/auth/*`                                                  | API               | Los endpoints propios de Better Auth (enviar un código, iniciar sesión, la sesión, cerrar sesión) tras una única ruta de servidor catch-all de Start (`src/routes/api.auth.$.ts`), sobre la base de datos D1 del Worker (`DB`); no forma parte del contrato ni de su documento                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `/dev/mail`, `/dev/people`                                     | Solo local        | La bandeja de salida del entorno local (el correo capturado para `?recipient=` en lugar de enviado) y su inicio de sesión precargado (las personas que ofrece, lo que posee cada una, y el código publicado con el que inician sesión); 404 en cualquier otro entorno (la tabla en `src/auth/environment.ts`, [explicado](./auth.md#environments-one-table))                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `/healthz`                                                     | Cada Worker       | Liveness, y [qué deployment respondió](#which-version-is-deployed)\: su entorno, la versión de Cloudflare y el sello de build                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `/api/openapi.json`, `/api/doc`                                | API               | El documento OpenAPI 3.1 generado a partir del router en el propio proceso, y su página de referencia (la página Scalar de oRPC, con el script fijado a una versión)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `/robots.txt`, `/sitemap.xml`                                  | Rutas de servidor | `Cache-Control: public, max-age=3600, s-maxage=3600`; los métodos distintos de GET y HEAD responden 405 con `Allow`; el sitemap enumera las páginas de sitio en cada locale con alternativas `hreflang`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| Ruta o locale desconocidos                                     |                   | HTTP 404 con la página de no encontrado localizada (una ruta desconocida sin localizar primero redirige al idioma del visitante, ya que el rewrite de TanStack la canonicaliza)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| El loader de una página falla                                  |                   | La página de error localizada de esa ruta (500 cuando se renderiza en servidor) con un reintento que vuelve a ejecutar los loaders; toda ruta de página y la raíz configuran ambas páginas de problema (`src/problem.tsx`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |

Las páginas localizadas nunca redirigen. Cuando el idioma preferido del visitante difiere del de la página, una sugerencia descartable ofrece esa versión; elegirla o descartarla se recuerda en la cookie `PARAGLIDE_LOCALE` de Paraglide, sobre la que solo actúan las URLs de entrada. La detección de locale, la cookie, la localización de URLs y la redirección son las propias estrategias y el middleware de Paraglide (`packages/remy-ui/paraglide.mjs` contiene la única configuración del compilador), integrados como la integración oficial de Paraglide con TanStack Start\: el middleware envuelve la entrada del Worker, y el `rewrite` del router elimina el locale antes de hacer el matching y lo añade a cada enlace, de modo que las rutas no llevan segmento de locale.

TanStack Start se ejecuta a través del plugin de Vite de Cloudflare ([`apps/remy-auth/vite.config.ts`](https\://github.com/joeblew999/remy-auth/blob/main/apps/remy-auth/vite.config.ts), el `remyApp()` del paquete). Todas las
rutas se renderizan en servidor excepto la demo. Los enlaces dentro de la app precargan el código y los datos de su ruta
según la intención; los cambios de idioma en las páginas de sitio son navegaciones completas.

## shadcn, de fábrica [#shadcn-stock]

Los componentes y el tema son lo que escribe la CLI de shadcn, en el diseño de monorepo de shadcn\: el
[`apps/remy-auth/components.json`](https\://github.com/joeblew999/remy-auth/blob/main/apps/remy-auth/components.json) de esta app dirige `shadcn add` hacia el paquete
([`packages/remy-ui/components.json`](https\://github.com/joeblew999/remy-auth/blob/main/packages/remy-ui/components.json)), y
`packages/remy-ui/src/styles/globals.css` también lo escribe la CLI. Nadie edita ninguno de los dos a mano; los
scripts de [`package.json`](https\://github.com/joeblew999/remy-auth/blob/main/package.json) son la única forma en que cambian\:

```sh
vp run ui:components       # Re-add every shadcn component (extend the list there to add one)
vp run ui:theme            # Rewrite globals.css with shadcn's default theme
vp run ui:blocks           # Diff each owned block against upstream, in place
vp run check:shadcn        # Re-run both and fail on any difference (the pre-commit hook, when a commit stages one)
```

Los bloques son copias propias, tal como shadcn pretende; el README de sidebar-16 explica qué se cambió. El
alias `components` del paquete (`@joeblew999/remy-ui/blocks`) es donde shadcn escribe los archivos propios de un
bloque cuando se ejecuta desde el paquete (`shadcn add <block> -c packages/remy-ui`)\: `blocks/<name>/components`.
Así, `ui:blocks` los compara en su sitio y los cambios de upstream se fusionan a mano.

Las fuentes viven en [`packages/remy-ui/src/fonts.css`](https\://github.com/joeblew999/remy-auth/blob/main/packages/remy-ui/src/fonts.css), importado después de
`globals.css` (ver [`src/styles.css`](https\://github.com/joeblew999/remy-auth/blob/main/apps/remy-auth/src/styles.css)); el archivo explica sus reglas. fontaine, en
la configuración de Vite de la app (`remyApp()`), genera las fuentes de respaldo ajustadas en tamaño que nombra.
`publicPageChecks` falla ante cualquier familia nombrada que no esté cargada.

## En Cloudflare [#on-cloudflare]

Hay una sola forma de desplegar\: hacer push. Cloudflare compila y despliega este repositorio por sí mismo, desde GitHub; nada nuestro
sube código, y nadie ejecuta `wrangler deploy` a mano (propietario, 2026-10-18\: "you've got multiple ways to deploy.
It's bloody confusing"). Las palabras, con un solo significado cada una\:

| Palabra                  | Qué es                                                                                                                                                 | Qué lo provoca                                                                                  | Dónde responde                                                                           |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **Deploy**               | una versión nueva de un Worker pasa a estar en vivo en su dirección                                                                                    | una build que termina en `pnpm wrangler deploy`                                                 | la dirección del Worker                                                                  |
| **Deploy de producción** | `main` pasando a estar en vivo                                                                                                                         | cada push a `main` (la build de Git de Cloudflare, Workers Builds)                              | `https://remy-auth.gedw99.workers.dev`, `https://remy-auth-docs.gedw99.workers.dev`      |
| **Preview**              | la copia propia de una rama, con sus propias bases de datos y el inicio de sesión de prueba (`previews` de `wrangler.jsonc`, `ENVIRONMENT: "staging"`) | cada push a cualquier otra rama (la misma build de Git, que termina en `pnpm wrangler preview`) | `https://<branch>-remy-auth.gedw99.workers.dev` (una `/` en la rama se convierte en `-`) |
| **Rebuild**              | una de esas builds ejecutada de nuevo, para un commit ya subido                                                                                        | la API de Workers Builds (el conector), o el Retry del dashboard                                | como arriba                                                                              |

* **Dos Workers, uno por app, cada uno con su propia build de Git**, ambos sobre el `main` del repositorio\: `remy-auth`
  (raíz `/apps/remy-auth`) y `remy-auth-docs` (raíz `/apps/remy-auth-docs`), cada uno con su propio `wrangler.jsonc`.
  Cada build instala con pnpm (a partir de `pnpm-lock.yaml`, la variable de build `PNPM_VERSION` como en `devEngines`,
  `NODE_VERSION` como en `.node-version`), luego ejecuta `pnpm vp run build`, y después `pnpm wrangler deploy` en `main` o
  `pnpm wrangler preview` en cualquier otra rama. Estos ajustes viven en Cloudflare, no en el repositorio\: se escriben
  aquí y solo se cambian a través del conector, para que coincidan.
* **Desde el Mac del propietario** (con sesión iniciada en Cloudflare)\: el propio Wrangler (`pnpm wrangler deployments list`,
  `pnpm wrangler tail`, solo lectura), `pnpm remy docs publish` después de que cambien los docs (el índice de Ask AI) y
  las migraciones de bases de datos remotas. **Desde la nube de Claude** (sin sesión de Cloudflare para Wrangler)\: push, y Cloudflare
  compila; el conector de Cloudflare muestra una build y su log, cambia los ajustes de build e inicia un rebuild.
  Después comprueba una preview\: `pnpm remy test remote https://<branch>-remy-auth.gedw99.workers.dev` (solo nuestras comprobaciones\: una
  preview se oculta a propósito de los buscadores, así que las auditorías de Google evalúan producción).
* **Velocidad lejos de las bases de datos**\: el Worker se ejecuta con Smart Placement (`wrangler.jsonc`, comprobado por
  `check:wrangler` en `vp run ready`), y cada petición lee a través de su propia sesión de D1, de modo que las lecturas vienen de la
  copia más cercana (`@joeblew999/remy-ui/replicas`; una comprobación unitaria rechaza el código de la app que use un binding de base de datos en bruto).
* **Toda base de datos D1 mantiene copias de lectura y vive en la región de producción**\: la regla de la plataforma, que
  `pnpm remy cloudflare provisioned` consulta a Cloudflare, incluidas las bases de datos de las previews, nombrando la corrección.
  Ninguna de las dos es un ajuste que la configuración de Wrangler pueda contener.

## Qué versión está desplegada [#which-version-is-deployed]

Nada registra qué está desplegado\: cada deployment dice qué es cuando se le pregunta, de modo que la
respuesta no puede quedar desactualizada (el diseño es de remy-sport, cuyo registro versionado de deployments
estuvo equivocado durante semanas).

* **El sello de build.** `remyApp()` y `remyDocs()` lo calculan una vez por build a partir de las
  fuentes\: el paquete de la app, el commit, un hash corto de cualquier cambio sin confirmar, y la versión
  instalada de cada paquete de la plataforma (`build: { packages: [...] }` en `remyApp()` enumera más). El
  mismo checkout produce el mismo sello; no incluye la hora de build, así que un bundle solo cambia cuando
  cambia su código.
* **`/healthz`**, en cada Worker construido sobre el paquete, responde con él\: `service`, `environment`
  (lo que el Worker declara; producción cuando no declara ninguno), `release` y `deployedAt` (la versión
  de Cloudflare y cuándo se le asignó) y `build`. No nombra a nadie, y cualquier origin puede leerlo.
* **`BuildStamp`** (`@joeblew999/remy-ui/versions`) está en el pie del marco de la app, así que toda página
  de app de toda app lo tiene\: el entorno salvo que sea producción, el nombre del producto, el commit. La
  página lleva el sello con el que se construyó; cuando su deployment responde con otro, la página es una
  pestaña que quedó abierta a través de un deploy, y el sello ofrece el reload. Nunca se recarga por sí sola.
* **`Versions`**, para una página de ajustes o acerca de\: esta app, su Worker de docs (`docs` en
  `defineRemyApp`) y los `deployments` que la app enumera ahí (un servicio al que llama), cada uno
  respondiendo por sí mismo, y luego los paquetes con los que se hizo este build. La página de Settings de
  remy-auth lo muestra.
* **`curl <origin>/healthz`** consulta un deployment (los origins están en el `.env` de la app), y
  `pnpm wrangler deployments list` da la lista propia de Cloudflare de los deployments de un Worker. Pregunta antes de decir qué está
  en vivo.

Los conjuntos de comprobaciones compartidos lo demuestran en cada app\: `/healthz` nombra el commit de este
checkout en una ejecución local (un sello que quedó de un build anterior falla), y el marco ofrece el reload
solo cuando el deployment ha avanzado.

## El nombre del producto [#the-products-name]

Remy es el nombre de este prototipo. Un proyecto construido sobre el mismo código tiene el suyo propio, y lo
escribe una sola vez\:

```ts
// docs/docs.config.ts: los docs lo dicen, y la app lo toma de aquí
const product = 'Harbor';
export const docsConfig = defineDocsConfig({ product, ... });

// src/remy-app.tsx
export const remyApp = defineRemyApp({ brand: docsConfig.product, ... });
```

Una app sin docs escribe `brand` directamente. Todo lo que una persona lee toma el nombre de ahí\: el marco,
el título de cada página, los datos estructurados de la página de inicio, y todo mensaje que diga el nombre,
que lo recibe como parámetro\:

```tsx
m.home_title({ product: useRemyApp().brand }, { locale })                       // en una página
pageHead({ ..., description: (locale, product) => m.home_description({ product }, { locale }) })   // en un head
codeMail({ otp, product }, locale)                                              // el correo de inicio de sesión
```

Un mensaje que nombra al producto se escribe con `{product}`, nunca con un nombre fijo, de modo que los
tipos de Paraglide rechazan a quien lo omita. La página de Settings de remy-auth muestra dónde se usa el
nombre, y cómo se leería cada lugar con otro nombre.

Lo que no es el nombre del producto, y permanece\: los paquetes (`@joeblew999/remy-ui`), la skill `remy`, los
nombres de tareas, el nombre de servicio de un Worker y el nombre de un servidor MCP. Son identificadores
que ve un desarrollador.

Comprobaciones\: el título de cada página termina con el nombre de la app, y una app con otro nombre nunca
muestra «Remy» (`productNameChecks`, en el conjunto compartido de cada app; el fixture consumidor, llamado
«My app», es donde esto se nota); el catálogo de la plataforma no nombra a ningún producto
(`tests/product.spec.ts`).

## Estructura y reutilización [#structure-and-reuse]

La app es `apps/remy-auth/` (sus rutas de abajo parten de ahí); los paquetes junto a ella son `packages/*`,
los docs `apps/remy-auth-docs/`, todo un único workspace de pnpm (`pnpm-workspace.yaml`) cuya raíz contiene las comprobaciones propias del repositorio.

* `src/routes/`\: rutas de archivo de TanStack (loaders, `head`, renderizado por ruta) que renderizan las páginas del paquete, más las filas de formatos adicionales de esta app (`src/formats-extras.tsx`), mientras que `robots.txt` y `sitemap.xml` (la parte seo-routes) y `/csp-report` (`app-routes`) son las rutas de servidor del paquete, montadas junto a ellas; `src/routeTree.gen.ts` lo genera el plugin del router durante el dev y el build, y se versiona.
* `src/server.ts`\: entrada del Worker a través del `localizedWorker` del paquete (IDs de petición, logs de estado estructurados, `/healthz`, el middleware de Paraglide, redirecciones de entrada). Start recibe la petición original, así que las funciones de servidor leen la geolocalización de Cloudflare a partir de las propiedades `cf` de la petición en un módulo exclusivo de servidor bajo la protección de imports de Start (`packages/remy-showcase/src/parts/deferred-place/place.server.ts`, la parte deferred-place del showcase). El wrapper pasa su ID de petición hacia dentro como `X-Request-ID`; el middleware de petición de Start lo expone como `context.requestId`, y un middleware de función registra una línea `server_fn` por cada llamada (el `start` del paquete, registrado en `src/start.ts`). Ahí mismo, un segundo middleware de petición genera el nonce de CSP por petición que el router compartido (`router`) entrega a TanStack Router, y la ruta de servidor `/csp-report` del paquete (`app-routes`) registra los reportes de la política ([cabeceras de seguridad](https\://github.com/joeblew999/remy-auth/blob/main/.plans/parked/gui-portal.md)). El nombre del servicio tiene un único hogar, `src/service.ts`.
* `src/api/`\: la implementación del contrato (`router.ts`, tras el guard; sin imports de Workers para que las comprobaciones puedan cargarlo en Node), el contexto de cada llamada, con la sesión de quien llama (`context.server.ts`), y el cliente isomórfico con sus utilidades de TanStack Query (`client.ts`). El contrato es `packages/remy-auth-contract/`; el mecanismo compartido son las exportaciones `api/*` del paquete; [el plan de contratos](https\://github.com/joeblew999/remy-auth/blob/main/.plans/done/openapi-contracts.md) es dueño del diseño.
* `src/auth/`\: Better Auth en este Worker ([el plan de autenticación](https\://github.com/joeblew999/remy-auth/blob/main/.plans/auth-service.md))\: sus opciones, compartidas por el Worker y la CLI (`options.ts`), la instancia construida en el primer uso a partir del binding D1 (`auth.server.ts`), la configuración de la CLI (`cli.ts`), la tabla de entorno (`environment.ts`), las personas precargadas (`seed.ts`, `people.server.ts`), el envío de correo y el correo del código (`mail.server.ts`, `code-mail.ts`), la regla sobre los campos que una persona puede escribir (`fields.ts`), la función de servidor de la cuenta (`account.ts`) y el formulario de inicio de sesión, su selector y el cliente de navegador de Better Auth (`sign-in.tsx`, `client.ts`). `migrations/` contiene las migraciones numeradas de D1 ([los scripts de Better Auth](./tooling.md#the-tooling)).
* `src/notes/`\: la demo de notas, que usa el motor de relaciones como lo haría cualquier app\: sus datos en su propia D1 (`DEMO_DB`, `migrations-demo/`; ahí una persona es solo un ID de cuenta) a través de `store.ts`, la página (`page.tsx`) y la función de servidor que su loader consulta (`state.ts`). El vocabulario y las formas son los del contrato (`packages/remy-auth-contract/src/notes.ts`).
* `src/router.tsx`\: el `remyRouter` del paquete, un router y un cliente de TanStack Query nuevos por petición, con la integración SSR de Query. `src/routes/__root.tsx` es el `remyRoot` del paquete con la configuración de esta app; TanStack Devtools (paneles de Router y Query) se pasa ahí, en el propio archivo de la app, porque su plugin de Vite `devtools()` lo elimina de las builds de producción solo fuera de `node_modules`; la comprobación de frontera de build demuestra que ni devtools ni código exclusivo de servidor llegan al navegador.
* `src/parts.json`\: las [partes](https\://github.com/joeblew999/remy-auth/blob/main/.plans/done/parts.md) del paquete que usa esta app, una por línea (`@joeblew999/remy-showcase/time-zones`, `deferred-place` y `status-card` del showcase, y `seo-routes` de la plataforma); `remyApp()` (en `vite.config.ts`) monta sus rutas y `partChecks()` en `tests/gui.spec.ts` ejecuta sus comprobaciones.
* `packages/remy-ui/` (en la raíz)\: todo lo que comparten ambas apps; su [README](./remy-ui.md) enumera las exportaciones.
* `tests/`\: las comprobaciones de navegador de la app\: las comprobaciones compartidas del paquete (`@joeblew999/remy-ui/checks`, el `@joeblew999/remy-showcase/*.checks` del showcase) más las comprobaciones que solo posee este repositorio (catálogos, renderizados concurrentes en servidor, hidratación, sus filas de formatos adicionales); `lighthouse.spec.ts` es la parte `audits` de `vp run ci` y `performance.spec.ts` los Core Web Vitals de un deployment (`pnpm remy test cwv`).

El locale se pasa explícitamente a las funciones de mensaje compiladas; las peticiones concurrentes no comparten
estado mutable de locale. Los catálogos son los `locales` en
[settings.json](https\://github.com/joeblew999/remy-auth/blob/main/packages/remy-ui/project.inlang/settings.json); todos salvo inglés y español
fueron escritos por un agente y no están revisados. La dirección, los endónimos, las fechas, los números, la moneda y los plurales
siguen las decisiones registradas en
[el plan de GUI](https\://github.com/joeblew999/remy-auth/blob/main/.plans/done/gui.md#dates-numbers-currency-and-direction).

El consumidor real del paquete es [remy-auth-app](https\://github.com/joeblew999/remy-auth-app),
que instala el paquete publicado y ejecuta sus comprobaciones sobre páginas prerenderizadas, mientras que esta app
las ejecuta sobre páginas renderizadas en servidor.

## Evidencia y límites [#evidence-and-limits]

`vp run ci` ejecuta todas las comprobaciones (`vp run ready`), luego las comprobaciones de navegador (`test:browser`), después las auditorías de Lighthouse de Google
(`test:audits`), una tras otra (una release añade además una app nueva sobre los paquetes de este commit, `new-app`)\: las
auditorías miden la velocidad, así que nunca se ejecutan junto a otra ejecución de navegador. GitHub ejecuta las mismas tareas como jobs en paralelo (las
comprobaciones de navegador en tres fragmentos, `vp run test:browser --shard 1/3`). Los Core Web Vitals se miden sobre un deployment, a
mano (`pnpm remy test cwv`\: producción, o el origin de una preview). No hay niveles que elegir y ninguna parte decide omitirse, y lo que nada ha tocado sale de la caché de Vite Plus en lugar de ejecutarse de nuevo. Qué se prueba se decide una sola vez, según lo que una comprobación
puede demostrar (propietario, 2026-10-17\: "you really need to decide what is tested and what isn't")\:

* Una comprobación de cómo se dibuja el texto (fuentes y escrituras, separación silábica, dirección, ajuste a un teléfono, formatos) se ejecuta en
  un idioma por cada forma en que la app dibuja texto (`checkedLocales`)\: cada idioma al que fonts.css da su propia pila de fuentes, cada idioma han, y el inglés para la pila predeterminada compartida; hoy 9 de 13, calculados a partir de fonts.css. El español, el polaco, el turco y el alemán dibujan exactamente igual que el inglés.
* Una comprobación de un comportamiento que no depende del idioma (la política de seguridad, noindex, los parámetros de búsqueda, el formulario de demo, las páginas de error, las respuestas de la API) se ejecuta en inglés y en el primer idioma de derecha a izquierda (`behaviourLocales`).
* Lo que sí difiere en cada idioma se comprueba en todos\: los catálogos y sus plurales (las comprobaciones unitarias),
  el sitemap, hreflang y el selector de idioma, y las muestras de mayúsculas/minúsculas y de separación silábica.
* Una release ejecuta los mismos idiomas\: uno por cada forma de dibujar texto. El resto dibuja con el mismo código, así que ejecutarlos no encontró nada más y costaba cuatro minutos; que exista el texto de cada idioma lo comprueba `remy i18n check`, sin conexión.
  A mano, `CHECK_LOCALES` acepta `quick` (inglés y árabe) o una lista separada por comas.

Lo que cubren las comprobaciones son las propias comprobaciones\: `tests/` y el
`@joeblew999/remy-ui/checks` del paquete (el título de cada comprobación dice qué demuestra); los pasos del flujo que las ejecutan están en
[cómo trabajamos](./how-we-work.md#the-flow-the-standard-way-in-plain-sight). En resumen\: cada página en cada
idioma sin JavaScript (idioma, dirección, metadatos, enlaces), catálogos y plurales, los valores de formatos
frente a Intl, las redirecciones de entrada y la sugerencia de idioma, la hidratación, el formulario de demo y la API,
los estados HTTP, el sitemap y `hreflang`, las cabeceras de seguridad y el nonce de CSP, las pantallas estrechas, y
las páginas de docs, búsqueda y ask.

Lo de Google cubre solo las páginas de sitio (las páginas de app son noindex por diseño)\: Lighthouse audita `/en`
(móvil y escritorio), `/es`, `/ar` y `/en/formats`
([`tests/lighthouse.spec.ts`](https\://github.com/joeblew999/remy-auth/blob/main/apps/remy-auth/tests/lighthouse.spec.ts)); Core Web Vitals, a partir del paquete
`lighthouse` fijado de Google, evalúan `/en` (móvil y escritorio) y `/en/formats`
([`tests/performance.spec.ts`](https\://github.com/joeblew999/remy-auth/blob/main/apps/remy-auth/tests/performance.spec.ts)) frente a los umbrales buenos de Google (LCP
2.5 s, CLS 0.1, TBT 200 ms, Performance de al menos 0.9), en un deployment
(`pnpm remy test cwv`\: producción salvo que se dé el origin de una preview), no en localhost.

Límites\: todavía no hay organizaciones, tokens OAuth ni inicio de sesión entre apps, así que solo las páginas propias de remy-auth saben quién ha iniciado sesión
([el plan de autenticación](https\://github.com/joeblew999/remy-auth/blob/main/.plans/auth-service.md)). El sitio se sirve desde su dirección de workers.dev (`VITE_DEPLOY_ORIGIN` en el `.env` de la app); el origin público de producción y
Search Console son decisiones del propietario ([now](https\://github.com/joeblew999/remy-auth/blob/main/.plans/now.md)).

Avisos de herramientas conocidos\: Node puede imprimir el aviso experimental de localStorage de Chrome DevTools;
pnpm no ejecuta ningún script de instalación fuera de `allowBuilds` (`pnpm-workspace.yaml`) y puede nombrar los que
ha omitido; la comprobación de paquete aislado de Vite
puede informar de que las directivas `use client` de Base UI se ignoran en un bundle
de cliente plano. Estos avisos no cambian los comportamientos probados.
