# Expulsa tu panel

Publicar en Vibedasher es el final normal del ciclo: eso es lo que mantiene un panel alojado, gobernado y conectado a datos que alguien mantiene. **Expulsar es la otra puerta, y está abierta a propósito.** El panel es React y SQL sencillos, y en cualquier momento puedes llevarte el árbol completo y ejecutarlo en tu propia aplicación, en tu propio hosting, bajo tu propio build.

Esto no es una exportación de una imagen o un blob JSON que tendrías que reimplementar. Es el código que corre: componentes, páginas, gráficos, stores, hooks, un `package.json` reconstruido a partir de los imports reales del árbol, una configuración de Tailwind, una entrada CSS, un `tsconfig.json`, una configuración de Vite, un `index.html`, y un `main.tsx` que lo monta. `npm install && npm run dev` lo sirve como su propia aplicación React.

nota

Expulsar es lo opuesto a [incrustar](/es/api/embedding-the-share-iframe.md). Incrustar pone *nuestro* panel renderizado dentro de un iframe en tu página. Expulsar te entrega el código y nos saca por completo de la ruta de renderizado — solo conservamos el motor de datos por debajo. Si la pregunta es «¿este código es mío?», expulsar es la respuesta. Si todavía estás eligiendo entre las dos, **[Incrustar o expulsar](/es/guides/embed-or-eject.md)** las pone una al lado de la otra.

## Las tres llamadas[​](#las-tres-llamadas "Enlace directo al Las tres llamadas")

* **`eject_viz(viz_id, out_dir=...)`** — el árbol completo más un **manifiesto de conexión**: el contrato de consulta, los tokens de tema, el punto de entrada y el orden de sus props, la lista de archivos, la región, y el carril de datos.
* **`eject_viz_files(viz_id, paths=[...])`** — el contenido de los archivos a demanda. Es el seguimiento cuando una respuesta vino paginada, y también es la forma de leer un archivo de forma barata.
* **`eject_instructions()`** — la referencia de conexión, devuelta como datos estructurados en lugar de prosa que tengas que analizar. Todo lo de esta página es una lectura de ella; llámala cuando quieras el original legible por máquina.

### Pasa `out_dir`, y ten claro en qué transporte estás[​](#pasa-out_dir-y-ten-claro-en-qué-transporte-estás "Enlace directo al pasa-out_dir-y-ten-claro-en-qué-transporte-estás")

Un panel real tiene del orden de cuarenta y tantos archivos y medio millón de caracteres. Con `out_dir` apuntando a una ruta **absoluta**, el árbol se escribe directamente a disco y la respuesta se mantiene pequeña por grande que sea el árbol.

Sobre el transporte **stdio** — el arranque estándar, donde tu cliente MCP lanza el servidor como un subproceso local bajo tu propio usuario — esa ruta es tu propio disco, que es lo que hace legítimo escribir ahí. Sobre el transporte **HTTP** sería el disco del *servidor*, así que `out_dir` se **rechaza** ahí en lugar de escribir archivos en un sitio que no puedes leer mientras informa de éxito. La paginación es la única vía en HTTP.

Sin `out_dir`, los archivos se empaquetan en la respuesta hasta un presupuesto de caracteres y el resto vuelve como **`delivery.pendingPaths`**. Esa lista es el contrato: **una respuesta con un `pendingPaths` no vacío es una página, no un árbol.** Sigue llamando a `eject_viz_files` con ella hasta que vuelva vacía.

## Lee `manifest.dataLane` antes de escribir cualquier conexión de datos[​](#lee-manifestdatalane-antes-de-escribir-cualquier-conexión-de-datos "Enlace directo al lee-manifestdatalane-antes-de-escribir-cualquier-conexión-de-datos")

Esta es la primera decisión y la que no puedes cambiar después, porque decide quién puede leer el panel.

### Carril A — `dataLane.id == "wasm"`: ya renderiza datos reales[​](#carril-a--datalaneid--wasm-ya-renderiza-datos-reales "Enlace directo al carril-a--datalaneid--wasm-ya-renderiza-datos-reales")

Para una visualización **pública y publicada**, la expulsión emite un pequeño runner (`vibedasherWasm.ts`) y el árbol renderiza **filas reales con `npm run dev` sin ninguna credencial**: sin clave de API, sin token de incrustación, sin backend propio, y funciona desde cualquier origen incluidos `localhost` y un simple servidor de archivos estáticos.

El mecanismo: un `GET` anónimo del plan wasm de la visualización devuelve un extracto Parquet prefirmado por conjunto de datos; el runner registra cada uno bajo su alias SQL en DuckDB-WASM y ejecuta el SQL de cada gráfico en el navegador del visitante. Su única dependencia, `@duckdb/duckdb-wasm`, está en npm. `main.tsx` espera el calentamiento del runner antes de montar, así que una configuración rota es **un panel pintado** en lugar de una rejilla de mosaicos en blanco.

Aquí no hay nada que conectar. **No añadas una credencial encima** — te compra un arranque roto a cambio de nada.

Lo que este carril no es: sirve el extracto **publicado**, no una consulta viva, así que refrescar lo que muestra significa volver a publicar. Y existe solo mientras la visualización siga siendo pública y publicada.

**Qué obtiene cada visitante, dicho con claridad.** El extracto está indexado por conjunto de datos e instantánea, nunca por una consulta — el mismo `SELECT` con y sin `WHERE` devuelve el archivo idéntico. Así que contiene todas las filas de cada conjunto de datos que lee el panel, y es un único archivo, el mismo, para todo el mundo. Un filtro que añadas en la página cambia lo que se dibuja, nunca lo que se entregó, y los bytes están en la pestaña de red en cualquier caso. Eso es inherente a poner los datos en el navegador. Si los visores deben ver filas distintas, toma el carril B.

### Carril B — `dataLane.id == "token"`: cualquier otra visualización[​](#carril-b--datalaneid--token-cualquier-otra-visualización "Enlace directo al carril-b--datalaneid--token-cualquier-otra-visualización")

No hay carril sin credenciales y la expulsión no emitió ninguno. El endpoint del plan wasm está protegido por la misma comprobación de público-y-publicado que el resto de la superficie anónima, así que apuntar a él a mano no ayuda. `npm run dev` sigue montando, y pinta un panel que nombra el endpoint que necesita — no falla en silencio y no sustituye datos falsos.

Dos montajes funcionan, y **los dos ponen la credencial en un servidor que tú controlas**:

1. **Tu aplicación sirve una ruta de token de incrustación.** Del mismo origen, respondiendo `application/json` `{"token": "..."}`, emitido con la clave de API que guarda en el servidor. Apunta la URL de token de incrustación del árbol a ella y construye el cliente con el token.
2. **Consultas en código de servidor confiable** con `new Vibedasher({ apiKey })` y pasas el resultado hacia abajo, o haces de proxy de `{ sql, params }` a través de tu propia ruta.

**Nunca pongas una clave de API en código de navegador.** Lleva la autoridad completa de la cuenta y no tiene scopes. Un token de incrustación con alcance limitado es la credencial del lado cliente; una clave de API no lo es.

Esos dos son también las **únicas** formas que pueden mostrar a un visor menos filas que a otro. Un token de incrustación puede llevar `filters`, que la plataforma añade a cada consulta que autoriza — en el servidor, antes de cualquier join, agregación o `LIMIT`, donde la página no puede ensancharlos. En código de servidor confiable, acotas el SQL tú mismo.

Si la expulsión informa del carril `token` con la razón `no-wasm-extract` en una visualización que *sí* es pública y publicada, el conjunto de datos simplemente todavía no tiene extracto. Pedir uno es lo único que lo construye — mira [El carril de consultas wasm](/es/guides/the-wasm-query-lane.md#you-have-to-ask-for-the-extract-before-it-exists). Volver a publicar no cuece un extracto, así que «publicar y volver a expulsar» da vueltas para siempre.

## Instala el cliente[​](#instala-el-cliente "Enlace directo al Instala el cliente")

```
npm i @vibedasher/client
```

```
pip install vibedasher
```

El paquete de npm es **`@vibedasher/client`**. No existe `@vibedasher/sdk` y nunca existirá. Instala los paquetes reales en lugar de copiarlos dentro de tu repo.

## Estilos, typecheck, y el App Router[​](#estilos-typecheck-y-el-app-router "Enlace directo al Estilos, typecheck, y el App Router")

**Los estilos son tres piezas y necesitas las tres.** `tailwind.config.js` (modo oscuro con una estrategia de selector ligada a un atributo que el árbol pone en su propia raíz, así que el modo oscuro del panel queda acotado independientemente del de tu aplicación, más una safelist para las clases ensambladas en ejecución y por tanto invisibles al escáner de Tailwind); `styles/dashboard.css` (la entrada de Tailwind y un bloque `:root` de tokens de diseño como valores por defecto en tiempo de build); y `themeStyles.ts` (los mismos tokens como una cadena que la aplicación inyecta en ejecución, por tema). El CSS te da un primer pintado tematizado; `themeStyles.ts` te da el cambio claro/oscuro en vivo. Si tu aplicación ya usa Tailwind con un modo oscuro basado en clase, no fusiones las configuraciones — una configuración de Tailwind soporta un único ajuste de modo oscuro. En Tailwind v4, referencia la configuración entregada con `@config`.

**Typecheck.** `npm run typecheck` (`tsc --noEmit`) debe informar de cero errores en un árbol recién expulsado, y dos gates lo sostienen en cada ejecución de nuestra suite de tests — uno hace typecheck de una expulsión generada completa, el otro instala, hace typecheck *y* construye una escrita a mano. Dos condiciones acotan la afirmación, y ninguna es una salida. Se cumple **bajo el `tsconfig.json` entregado**, que pone `strict: false` a propósito, porque los efectos de gráfico generados están escritos contra filas de consulta débilmente tipadas; activar strict producirá errores que luego serán tuyos de mantener. Y se cumple para un árbol cuyos archivos de rama se escribieron recientemente — un panel publicado hace suficiente tiempo lleva archivos congelados de una forma más antigua, dos puntos de los cuales se sabe que fallan `tsc` aunque construyan y corran correctamente, ya que Vite transpila sin hacer typecheck. El arreglo es **volver a publicar**, lo que reescribe los archivos de rama a partir de la forma actual.

**Next.js App Router.** Cada módulo es un server component por defecto. `App.tsx` y los módulos `stores/` ya llevan `'use client'` en la expulsión — los stores tocan `localStorage` en el ámbito del módulo y se caerían en el servidor. Marcar `App.tsx` mete todo su grafo de imports en el bundle del cliente, así que nada más necesita la directiva a menos que importes un gráfico, componente o store directamente desde un server component.

## Qué no te entrega la expulsión[​](#qué-no-te-entrega-la-expulsión "Enlace directo al Qué no te entrega la expulsión")

* **Nada de lo que hay en `manifest.internalDescriptors`** — metadatos de plataforma, ya excluidos de `files`. No los copies.
* **Nada de lo que hay en `manifest.hostOnlyImports`** — servido desde el propio bundle de nuestro renderizador, en ningún registro, deliberadamente ausente. Reemplázalos antes de construir.
* **La plataforma de datos.** Expulsar nos saca de la ruta de renderizado, no de la ruta de datos. Las conexiones, conjuntos de datos, actualizaciones, proyectos dbt, la gobernanza y el motor de consultas de debajo se quedan donde se mantienen — esa es la mitad de la división que no intentabas poseer. El carril sin credenciales es la única excepción, y solo porque está leyendo un extracto publicado en lugar de consultar en vivo.
