Crea una App de Tareas con TanStack Start, Bunny Database y Drizzle ORM
Crea una app de tareas con tipado completo usando TanStack Start, Bunny Database (libSQL gestionado) y Drizzle ORM.
28 min de lectura

Buscaba una forma de lanzar una pequeña app TypeScript full-stack sin montar un servidor Postgres ni pagar por una base de datos que me factura mientras está inactiva. TanStack Start me da renderizado del lado del servidor y funciones de servidor con tipado seguro, Drizzle me da un esquema en el que puedo confiar, y Bunny.net añadió hace poco una base de datos libSQL gestionada que entra en reposo cuando nadie la consulta. Juntando las tres consigues seguridad de tipos desde la fila de la base de datos hasta el botón del navegador.
Esta es exactamente la configuración que monté yo mismo: crear la base de datos en el panel, generar un proyecto TanStack Start con shadcn, definir un esquema Drizzle, generar y ejecutar las migraciones, y luego construir una interfaz que lista, añade, marca y borra tareas. Nada sofisticado: solo una guía clara que puedes copiar línea por línea.
Prueba Bunny.net gratis durante 14 días
Necesitarás una cuenta de Bunny para seguir la guía. Regístrate en Bunny.net sin tarjeta de crédito y obtén una prueba de 14 días. Mi reseña completa de Bunny.net cubre el resto de la plataforma si quieres primero una visión general.
Qué vamos a construir
Una app de tareas de una sola página con:
- Una tabla
todosreal almacenada en Bunny Database (libSQL gestionado) - Una capa ORM de Drizzle con migraciones SQL generadas y versionadas
- Funciones de servidor mediante
createServerFnde TanStack Start, para que el acceso a la base de datos nunca salga del servidor - Una interfaz React estilizada con shadcn y Tailwind, hidratada desde un renderizado SSR
El resultado final es una interfaz pequeña donde escribes una tarea, pulsas enter, la marcas como hecha o la borras. Cada acción hace un viaje de ida y vuelta a Bunny Database.
Requisitos previos
Antes de empezar, necesitas:
- Una cuenta de Bunny.net con acceso a Bunny Database (actualmente en vista previa pública)
- Node.js o Bun instalado. Aquí usé Bun porque las instalaciones y los scripts van rápido
- Conocimientos prácticos de TypeScript, React y SQL
Paso 1: Genera el proyecto TanStack Start con shadcn
El camino más rápido a un proyecto TanStack Start que ya tenga shadcn configurado es la CLI de shadcn con la plantilla de TanStack Start. Obtienes Tailwind, el sistema de componentes de shadcn y enrutado basado en archivos listos para usar.
bunx --bun shadcn@latest init \
--preset b1aIcEaeG \
--base base \
--template start \
--pointerQué hace cada flag:
--preset b1aIcEaeGdescarga un starter de TanStack Start seleccionado (router, SSR, configuración de Vite)--base baseelige el tema de color base neutro--template startgenera TanStack Start en lugar de Next.js o Vite puro--pointeractiva la utilidad pointer para las interacciones del cursor
Cuando termina, el proyecto queda más o menos así:
start-app/
├── src/
│ ├── components/
│ │ └── ui/ # shadcn components (button, etc.)
│ ├── lib/
│ │ └── utils.ts # cn() helper for class merging
│ ├── routes/
│ │ ├── __root.tsx # root layout + providers
│ │ └── index.tsx # home route
│ ├── router.tsx
│ ├── routeTree.gen.ts # generated, do not edit
│ └── styles.css # Tailwind entry
├── package.json
├── vite.config.ts
└── tsconfig.jsonInstala las dependencias y arranca el servidor de desarrollo para confirmar que inicia:
bun install
bun run devEso te da una app estilizada y lista para SSR. Lo único que falta es una base de datos.
Desplegar TanStack Start en otro lugar
Esta guía aloja la base de datos en Bunny. Si prefieres ejecutar toda la app en tu propio servidor, tengo otra guía sobre cómo desplegar TanStack Start en un VPS con Dokploy que usa Postgres.
Paso 2: Crea tu Bunny Database
Bunny Database es una base de datos relacional gestionada construida sobre libSQL, un fork de SQLite. Obtienes SQL estándar, replicación global opcional, facturación por uso (entra en reposo cuando nada la consulta) y varias formas de conectarte: API HTTP, SDKs nativos y ORMs como Drizzle.
Crea la base de datos en el panel
- Inicia sesión en el panel de Bunny.net
- En la barra lateral izquierda, haz clic en + Add y luego en Database
- Ponle un nombre (por ejemplo
todotest). Este nombre aparece en tu URL de conexión - Elige un modo de despliegue:
- Automatic - Bunny elige las regiones según tu ubicación. Un buen valor por defecto para desarrollo
- Single region - sin replicación, la opción más barata, ideal para un solo VPS o para pruebas
- Manual - tú eliges la región primaria y las réplicas, útil por latencia o cumplimiento normativo
- Haz clic en Add Database
Obtén tus credenciales
Abre la base de datos y ve a la pestaña Access:
- Database URL: tiene el formato
libsql://<your-database-id>.lite.bunnydb.net. Todas las librerías cliente usan este endpoint - Access Token: haz clic en Generate Tokens. Obtienes un token Full Access (lectura y escritura, usa este para la app) y un token Read Only (limitado a
SELECT, práctico para analíticas)
Copia la URL y el token Full Access de inmediato. Los tokens solo se muestran una vez. Si pierdes uno, generas un par nuevo desde el panel.
Nunca subas las credenciales al repositorio
Mantén los tokens fuera del control de versiones. Los pondremos en un archivo .env ignorado por git.
Guarda las credenciales en .env
Crea un .env en la raíz del proyecto:
BUNNY_DATABASE_URL="libsql://your-database-id.lite.bunnydb.net/"
BUNNY_DATABASE_AUTH_TOKEN="your-full-access-token"
BUNNY_DATABASE_READ_ONLY_AUTH_TOKEN="your-read-only-token"Añade un .env.example (seguro de subir al repo) para que la siguiente persona sepa qué espera la app:
BUNNY_DATABASE_URL="libsql://your-database-id.lite.bunnydb.net/"
BUNNY_DATABASE_AUTH_TOKEN="your-full-access-token"
BUNNY_DATABASE_READ_ONLY_AUTH_TOKEN="your-read-only-token"El starter ya ignora .env* en git, pero revísalo de todos modos:
# .gitignore
.env*Paso 3: Instala Drizzle y el cliente libSQL
Bunny Database habla libSQL, así que usamos el cliente oficial @libsql/client como driver y el adaptador libSQL de Drizzle para consultas con tipado seguro.
bun add drizzle-orm @libsql/client
bun add -d drizzle-kitdrizzle-ormes el ORM@libsql/clientes el driver que habla el protocolo libSQL sobre HTTPdrizzle-kites una dependencia de desarrollo para generar migraciones, hacer push del esquema y abrir Drizzle Studio
Añade algunos scripts a package.json para trabajar más cómodo con la base de datos:
{
"scripts": {
"dev": "vite dev --port 3000 --host",
"db:generate": "drizzle-kit generate",
"db:migrate": "bun run src/db/migrate.ts",
"db:studio": "drizzle-kit studio"
}
}Fíjate en el flag --host de dev. Vincula el servidor a tus interfaces de red, lo cual importa cuando desarrollas en un VPS y quieres acceder desde el navegador de tu portátil.
Paso 4: Configura Drizzle
Crea drizzle.config.ts en la raíz del proyecto. Le dice a drizzle-kit dónde vive el esquema, dónde van las migraciones y cómo conectar con la base de datos:
// drizzle.config.ts
import { defineConfig } from "drizzle-kit"
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "turso",
dbCredentials: {
url: process.env.BUNNY_DATABASE_URL!,
authToken: process.env.BUNNY_DATABASE_AUTH_TOKEN!,
},
})Un par de cosas a destacar:
dialect: "turso"le dice a drizzle-kit que emita SQL compatible con SQLite, que es exactamente lo que libSQL (y Bunny Database) esperaout: "./drizzle"es donde caen los archivos de migracióndbCredentialslee las mismas variables de entorno del.env
Paso 5: Define el esquema
Crea src/db/schema.ts. Es la única fuente de verdad sobre la forma de la base de datos:
// src/db/schema.ts
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core"
export const todos = sqliteTable("todos", {
id: integer("id").primaryKey({ autoIncrement: true }),
title: text("title").notNull(),
completed: integer("completed", { mode: "boolean" }).notNull().default(false),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.$defaultFn(() => new Date()),
})
export type Todo = typeof todos.$inferSelect
export type NewTodo = typeof todos.$inferInsertLa tabla todos tiene cuatro columnas:
id- clave primaria entera autoincrementaltitle- el texto de la tarea, no nulocompleted- un booleano almacenado como 0/1 en SQLite pero expuesto comobooleanreal en TypeScript gracias amode: "boolean"created_at- un timestamp Unix entero expuesto comoDatede JavaScript mediantemode: "timestamp"
Los dos tipos exportados, Todo y NewTodo, se infieren directamente del esquema. Nunca los escribes a mano y se mantienen sincronizados a medida que el esquema cambia.
Paso 6: Crea el cliente de base de datos
Crea src/db/index.ts. Inicializa el cliente libSQL y lo envuelve con Drizzle:
// src/db/index.ts
import { drizzle } from "drizzle-orm/libsql"
import { createClient } from "@libsql/client/web"
import * as schema from "./schema"
const client = createClient({
url: process.env.BUNNY_DATABASE_URL!,
authToken: process.env.BUNNY_DATABASE_AUTH_TOKEN!,
})
export const db = drizzle(client, { schema })
export { schema }Usamos @libsql/client/web (el transporte HTTP) en lugar del socket nativo. Es la elección correcta para Bunny Database, al que accedes por HTTPS, y funciona igual en Node, Bun y runtimes edge.
Paso 7: Genera la primera migración
Con el esquema listo, genera la migración SQL:
bun run db:generatedrizzle-kit lee src/db/schema.ts, lo compara con el historial de migraciones y escribe un archivo nuevo en ./drizzle/. Para la tabla todos produce algo como:
-- drizzle/0000_certain_bug.sql
CREATE TABLE `todos` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`title` text NOT NULL,
`completed` integer DEFAULT false NOT NULL,
`created_at` integer NOT NULL
);Es sintaxis SQLite pura, que Bunny Database acepta sin cambios. El sufijo aleatorio (certain_bug aquí) mantiene las migraciones ordenadas sin conflictos de nombre.
Paso 8: Aplica la migración
Para ejecutar las migraciones de forma programática, crea src/db/migrate.ts:
// src/db/migrate.ts
import { migrate } from "drizzle-orm/libsql/migrator"
import { db } from "./index"
async function main() {
console.log("Running migrations...")
await migrate(db, { migrationsFolder: "./drizzle" })
console.log("Migrations complete.")
process.exit(0)
}
main().catch((err) => {
console.error("Migration failed:", err)
process.exit(1)
})Ejecuta:
bun run db:migrateDeberías ver:
Running migrations...
Migrations complete.La tabla todos ya existe de verdad en Bunny Database. Puedes confirmarlo mediante la API HTTP:
HTTP_URL=$(echo $BUNNY_DATABASE_URL | sed 's/^libsql:/https:/')
curl -X POST "${HTTP_URL%/}/v2/pipeline" \
-H "Authorization: Bearer $BUNNY_DATABASE_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"requests":[{"type":"execute","stmt":{"sql":"SELECT name FROM sqlite_master WHERE type='"'"'table'"'"'"}}]}'La respuesta lista todos, __drizzle_migrations y sqlite_sequence. Esa tabla __drizzle_migrations es como Drizzle registra qué migraciones se han ejecutado.
La shell bsql
Bunny también incluye una shell SQL interactiva llamada bsql. Si tienes la CLI de Bunny, conéctate con bsql libsql://your-database-id.lite.bunnydb.net --token your-token y ejecuta .tables, SELECT * FROM todos; y similares. Es una forma rápida de curiosear los datos sin escribir código.
Paso 9: Construye las funciones de servidor
Con la capa de base de datos lista, exponemos las operaciones al frontend mediante las funciones de servidor de TanStack Start. Se ejecutan solo en el servidor, aunque las importas y llamas como funciones async normales desde el código cliente. Así el cliente de base de datos y tus credenciales se quedan completamente fuera del bundle del navegador.
Crea src/db/queries.ts:
// src/db/queries.ts
import { eq } from "drizzle-orm"
import { createServerFn } from "@tanstack/react-start"
import { db, schema } from "./index"
import type { NewTodo } from "./schema"
export const getTodos = createServerFn({ method: "GET" }).handler(async () => {
const rows = await db.select().from(schema.todos).orderBy(schema.todos.id)
return rows
})
export const addTodo = createServerFn({ method: "POST" })
.validator((title: string) => ({ title }))
.handler(async ({ data }) => {
const newTodo: NewTodo = { title: data.title }
const [created] = await db.insert(schema.todos).values(newTodo).returning()
return created
})
export const toggleTodo = createServerFn({ method: "POST" })
.validator((input: { id: number; completed: boolean }) => ({
id: input.id,
completed: input.completed,
}))
.handler(async ({ data }) => {
const [updated] = await db
.update(schema.todos)
.set({ completed: data.completed })
.where(eq(schema.todos.id, data.id))
.returning()
return updated
})
export const deleteTodo = createServerFn({ method: "POST" })
.validator((id: number) => ({ id }))
.handler(async ({ data }) => {
await db.delete(schema.todos).where(eq(schema.todos.id, data.id))
return { id: data.id }
})Todas las funciones siguen la misma forma:
.validator()parsea y valida el argumento entrante, dándote un objetodatatipado.handler()ejecuta la consulta Drizzle contra Bunny Database y devuelve el resultado- La función es async y devuelve el resultado tipado, así que quien llama desde React obtiene seguridad de tipos completa
Paso 10: Conecta el QueryClientProvider
TanStack Start hace SSR por defecto. Como la interfaz usa TanStack Query para obtener datos, necesitas un QueryClientProvider en la raíz para que los renderizados del servidor y del cliente compartan el mismo query client.
Actualiza src/routes/__root.tsx:
// src/routes/__root.tsx
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router"
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"
import { TanStackDevtools } from "@tanstack/react-devtools"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import appCss from "../styles.css?url"
const queryClient = new QueryClient()
export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{ title: "Todos" },
],
links: [{ rel: "stylesheet", href: appCss }],
}),
notFoundComponent: () => (
<main className="container mx-auto p-4 pt-16">
<h1>404</h1>
<p>The requested page could not be found.</p>
</main>
),
shellComponent: RootDocument,
defaultPreload: "intent",
})
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
<TanStackDevtools
config={{ position: "bottom-right" }}
plugins={[
{ name: "Tanstack Router", render: <TanStackRouterDevtoolsPanel /> },
]}
/>
<Scripts />
</body>
</html>
)
}Si omites este provider, el SSR lanza No QueryClient set, use QueryClientProvider to set one y luego cae a renderizado solo en cliente. Eso anula el propósito del SSR y ralentiza tu primer pintado.
Paso 11: Construye la interfaz de tareas
Último paso: sustituye src/routes/index.tsx por la interfaz de tareas. Usa useQuery para cargar la lista y useMutation para cada acción, invalidando la query todos después de cada mutación para que la lista se refresque.
El layout es una columna centrada: un encabezado, una fila de input y botón para añadir tareas, y la lista debajo. Cada elemento tiene un botón circular para marcarla, el texto de la tarea (tachado cuando está completada) y un icono de papelera. Cada mutación llama a queryClient.invalidateQueries({ queryKey: ["todos"] }) al tener éxito, lo que vuelve a pedir los datos a través de la función de servidor y mantiene la interfaz fiel a lo que hay realmente en la base de datos.
src/routes/index.tsx completo
// src/routes/index.tsx
import { useState } from "react"
import { createFileRoute } from "@tanstack/react-router"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { Check, Plus, Trash2 } from "lucide-react"
import { getTodos, addTodo, toggleTodo, deleteTodo } from "@/db/queries"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
export const Route = createFileRoute("/")({ component: TodoApp })
function TodoApp() {
const queryClient = useQueryClient()
const [newTitle, setNewTitle] = useState("")
const { data: todos = [], isLoading } = useQuery({
queryKey: ["todos"],
queryFn: () => getTodos(),
})
const addMutation = useMutation({
mutationFn: (title: string) => addTodo({ data: title }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] })
setNewTitle("")
},
})
const toggleMutation = useMutation({
mutationFn: (vars: { id: number; completed: boolean }) =>
toggleTodo({ data: vars }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["todos"] }),
})
const deleteMutation = useMutation({
mutationFn: (id: number) => deleteTodo({ data: id }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["todos"] }),
})
return (
<div className="flex min-h-svh justify-center p-6">
<div className="flex w-full max-w-md flex-col gap-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Todos</h1>
<p className="text-sm text-muted-foreground">
Bunny Database + Drizzle + TanStack Start
</p>
</div>
<form
className="flex gap-2"
onSubmit={(e) => {
e.preventDefault()
const title = newTitle.trim()
if (!title) return
addMutation.mutate(title)
}}
>
<input
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="What needs to be done?"
className="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
/>
<Button type="submit" disabled={addMutation.isPending} size="icon">
<Plus className="size-4" />
</Button>
</form>
{isLoading ? (
<p className="text-sm text-muted-foreground">Loading...</p>
) : todos.length === 0 ? (
<p className="text-sm text-muted-foreground">
No todos yet. Add one above.
</p>
) : (
<ul className="flex flex-col gap-1">
{todos.map((todo) => (
<li
key={todo.id}
className="flex items-center gap-3 rounded-md border border-border px-3 py-2"
>
<button
type="button"
onClick={() =>
toggleMutation.mutate({
id: todo.id,
completed: !todo.completed,
})
}
className={cn(
"flex size-5 shrink-0 items-center justify-center rounded-full border transition-colors",
todo.completed
? "border-primary bg-primary text-primary-foreground"
: "border-input hover:border-primary",
)}
aria-label={todo.completed ? "Mark incomplete" : "Mark complete"}
>
{todo.completed && <Check className="size-3" />}
</button>
<span
className={cn(
"flex-1 text-sm",
todo.completed && "text-muted-foreground line-through",
)}
>
{todo.title}
</span>
<button
type="button"
onClick={() => deleteMutation.mutate(todo.id)}
className="text-muted-foreground hover:text-destructive"
aria-label="Delete todo"
>
<Trash2 className="size-4" />
</button>
</li>
))}
</ul>
)}
</div>
</div>
)
}Ejecutarlo en un VPS
Las funciones de servidor necesitan un runtime Node o Bun, así que no puedes subir esto a un hosting estático. Un VPS o una plataforma de contenedores es el hogar adecuado.
Cuando desarrollas en el propio VPS, el flag --host vincula Vite a 0.0.0.0 para que puedas acceder desde tu portátil:
bun run dev
# Local: http://localhost:3000/
# Network: http://your-vps-ip:3000/Para producción, compila y previsualiza:
bun run build
bun run previewPara cualquier cosa de larga duración, envuelve el comando de arranque en un gestor de procesos como PM2 o systemd, o mételo en un contenedor. Los Magic Containers de Bunny encajan muy bien aquí, en parte porque puedes adjuntar las credenciales de la base de datos como variables de entorno directamente desde el panel de la base de datos.
Vincular credenciales a un Magic Container
Despliega en un Magic Container de Bunny y puedes olvidarte del archivo .env. Desde el panel de la base de datos, haz clic en Add Secrets to Magic Container Apps, elige tu app contenedora y Bunny inyecta BUNNY_DATABASE_URL y BUNNY_DATABASE_AUTH_TOKEN por ti. Tu código ya lee process.env, así que nada cambia.
Por qué funciona este stack
Algunas razones por las que los tres encajan bien:
- Seguridad de tipos de extremo a extremo: el esquema Drizzle es la fuente de verdad y esos tipos fluyen a las funciones de servidor y a React. Renombrar una columna se convierte en un error de compilación, no en una sorpresa a las 2 de la madrugada
- SQL que ya conoces: Bunny Database es libSQL, compatible con SQLite, así que no hay ningún dialecto propietario que aprender
- En reposo cuando está inactiva: la base de datos se apaga cuando nada la consulta y factura por uso, lo que mantiene los proyectos personales baratos
- SSR de serie: TanStack Start renderiza el primer HTML en el servidor, así que la lista de tareas está ahí antes de la hidratación
- El código de la UI es tuyo: shadcn copia los componentes a tu proyecto en lugar de esconderlos detrás de un paquete
- Cambios de esquema baratos: edita
schema.ts, ejecutadb:generatey luegodb:migrate, y el cambio está en producción en segundos - Credenciales portables: el patrón
.enves estándar y los Magic Containers pueden inyectar las mismas variables, así que pasar de local a producción no requiere cambios de código
Solución de problemas
Algunas cosas me dieron problemas mientras lo construía:
«No QueryClient set» durante el SSR
Olvidaste envolver la app en QueryClientProvider dentro de __root.tsx. Añádelo como se muestra en el paso 10.
«Protocol libsql not supported» en libcurl
Aparece cuando intentas hacer curl directamente a la URL de la base de datos. El esquema libsql:// es solo para librerías cliente. Para llamadas HTTP directas, conviértelo a https:// y haz POST a /v2/pipeline, como en el paso 8.
Los tokens solo se muestran una vez
Si pierdes un token de acceso de Bunny, generas uno nuevo desde el panel. Regenerar invalida los tokens existentes, así que actualiza todas las apps que los usaban.
La migración falla con un error de esquema
Comprueba que dialect: "turso" está configurado en drizzle.config.ts. Bunny Database usa el dialecto libSQL/SQLite, no PostgreSQL.
Puerto ya en uso
Vite prueba el siguiente puerto libre (3001, 3002, etc.). Lee la salida de arranque para ver qué puerto eligió realmente.
Preguntas frecuentes
¿Bunny Database está lista para producción?
Está en vista previa pública, así que la trataría como cualquier producto en preview: genial para proyectos personales, herramientas internas y apps de mucha lectura, pero lee los términos actuales antes de poner cargas críticas en ella. Como es libSQL, tus datos y consultas siguen siendo portables a cualquier hosting compatible con SQLite si necesitas migrar. Mi reseña de Bunny.net explica mejor dónde está madura la plataforma y dónde sigue madurando.
¿Puedo usar Postgres en su lugar?
No con Bunny Database, que es solo libSQL. Si quieres Postgres con TanStack Start y Drizzle, mi guía de despliegue con Dokploy recorre exactamente eso en un VPS autoalojado.
¿Por qué funciones de servidor en lugar de una API REST?
Las funciones de servidor mantienen el cliente de base de datos y las credenciales en el servidor mientras las llamas como funciones async normales desde el cliente. Te ahorras escribir y mantener una capa de API aparte, y los tipos se propagan automáticamente. Para una app pequeña como esta, es muchísimo menos boilerplate.
¿Bunny cobra por consulta?
Bunny Database factura por uso (lecturas, escrituras y almacenamiento por región activa) y entra en reposo cuando nada la consulta, así que una app tranquila cuesta muy poco. Consulta la página de precios de Bunny para las tarifas actuales de la preview, ya que pueden cambiar mientras siga en vista previa.
¿Puedo añadir más tablas después?
Sí, esa es la parte buena. Añade una tabla a src/db/schema.ts, ejecuta bun run db:generate para crear la migración y luego bun run db:migrate para aplicarla. Los tipos nuevos están disponibles al instante y el cambio está activo en Bunny en segundos.
Para terminar
En aproximadamente una hora pasé de una carpeta vacía a una app de tareas tipada con SSR, respaldada por una base de datos gestionada que casi no cuesta nada en reposo, con migraciones versionadas y una interfaz que es toda mía. Ampliar el esquema es una operación de dos comandos, y el patrón de funciones de servidor mantiene las credenciales y la lógica de consultas donde deben estar.
Si quieres llevarla más lejos, los siguientes pasos obvios son autenticación de usuarios con tareas por usuario, fechas límite y filtros, actualizaciones optimistas en lugar de invalidación de queries, y un despliegue en Magic Container con credenciales inyectadas automáticamente. La documentación de Bunny Database y la guía libSQL de Drizzle cubren los rincones que yo no cubrí.
Prueba Bunny.net gratis durante 14 díasArtículos relacionados
- Reseña de Bunny.net - la plataforma completa tras un año en producción
- Astro DB con Bunny Database - la misma base de datos con el flujo local-first de Astro
- Despliega TanStack Start en un VPS con Dokploy - el mismo stack con Postgres, totalmente autoalojado
- Monta un bucket S3 como sistema de archivos - ZeroFS y JuiceFS en Bunny Storage
- Bunny Storage vs S3 vs Backblaze - comparativa de precios de almacenamiento en la nube
- Despliega un sitio Astro en Bunny.net - hosting estático en Bunny Storage y CDN
- Guía de Bunny Stream - alojamiento de vídeo en Bunny


