¿Qué hace Patrones de Appwrite?
Guía rápida: Utiliza Appwrite como tu BaaS de código abierto para autenticación, datos estructurados (TablesDB), almacenamiento de archivos, funciones serverless y suscripciones en tiempo real. Siempre inicializa las clases de servicio desde una instancia compartida de
Client, establece permisos explícitamente en cada fila (nada es accesible por defecto) y usa el SDK de servidor (node-appwrite) con autenticación mediante clave API solo en el backend.
<critical_requirements>
CRÍTICO: Antes de Usar Este Skill
Todo el código debe seguir las convenciones del proyecto en CLAUDE.md (kebab-case, exportaciones nombradas, orden de importaciones,
import type, constantes nombradas)
(DEBES establecer permisos explícitamente en cada fila y archivo — Appwrite no concede acceso por defecto, por lo que omitir permisos hace que los datos sean inaccesibles)
(DEBES usar node-appwrite con autenticación mediante clave API en el servidor y appwrite con autenticación de sesión en el cliente — NUNCA expongas claves API en paquetes de cliente)
(DEBES siempre verificar AppwriteException en cada llamada al SDK — Appwrite lanza excepciones, NO devuelve tuplas { data, error })
(DEBES usar ID.unique() para IDs generados automáticamente — pasar una cadena de texto crea un ID personalizado, no uno generado automáticamente)
(DEBES usar la API de TablesDB para proyectos nuevos — la API heredada de Bases de datos/colecciones/documentos está obsoleta y solo recibe parches de seguridad)
</critical_requirements>
Detección automática: Appwrite, appwrite, node-appwrite, TablesDB, Account, Databases, Permission, Role, ID.unique, Query.equal, createEmailPasswordSession, realtime.subscribe, Channel.files, Channel.tablesdb, APPWRITE_FUNCTION
Cuándo usarlo:
- Configurar el SDK de cliente o servidor de Appwrite con TypeScript
- Implementar autenticación (email/contraseña, OAuth, URL mágica, OTP telefónico, sesiones anónimas)
- Realizar operaciones CRUD en tablas de TablesDB (filas, consultas, permisos)
- Subir y servir archivos desde buckets de Storage con control de acceso
- Construir funciones serverless desencadenadas por eventos, programaciones o HTTP
- Suscribirse a cambios en tiempo real en filas, archivos o eventos de cuenta
- Gestionar membresías de equipos y permisos basados en roles
Patrones clave cubiertos:
- Inicialización de SDK de cliente y servidor con clases de servicio tipadas
- Flujos de autenticación: registro, inicio de sesión, OAuth, URL mágica, gestión de sesiones
- Operaciones de TablesDB con la clase
Querypara filtrado, paginación, ordenación - Modelo de permisos:
Permission.read(Role.any()),Role.user(),Role.team() - Almacenamiento: subida, descarga, vista previa con transformaciones de imagen, permisos de bucket
- Funciones serverless:
export default async ({ req, res, log, error }) => {} - Suscripciones en tiempo real mediante ayudantes
Channelyrealtime.subscribe()
Cuándo NO usarlo:
- Conexiones directas a bases de datos o consultas SQL (Appwrite solo es API, no SQL directo)
- Uniones relacionales complejas entre muchas tablas (Appwrite soporta relaciones pero no uniones SQL arbitrarias)
- Aplicaciones que requieren tiempo real del lado del servidor (Appwrite Realtime solo es SDK de cliente)
Recursos detallados:
- Para marcos de decisión y antipatrones, consulta reference.md
Cliente y Consultas:
- examples/core.md — Configuración del cliente, CRUD de TablesDB, manejo de errores, permisos
Autenticación:
- examples/auth.md — Flujos completos de autenticación, OAuth, URL mágica, sesiones, equipos
Almacenamiento y Funciones:
- examples/storage.md — Subida de archivos, descarga, vistas previas, permisos de bucket
- examples/functions.md — Funciones serverless, desencadenadores, uso del SDK dentro de funciones
Tiempo Real:
- examples/realtime.md — Suscripciones a canales, filtrado de eventos, limpieza
Filosofía
Appwrite es un backend-como-servicio de código abierto que proporciona autenticación, bases de datos (TablesDB), almacenamiento de archivos, funciones serverless y tiempo real, todo a través de un SDK unificado. Puede ser auto-alojado o usado a través de Appwrite Cloud.
Principios fundamentales:
- Seguro por defecto — Nada es accesible sin permisos explícitos. Cada fila y archivo debe tener permisos establecidos, o será invisible para todos los usuarios. Esto es lo opuesto a "abierto por defecto" — trátalo como un paso obligatorio, no una ocurrencia tardía.
- Arquitectura de clases de servicio — Todas las interacciones con el SDK pasan a través de clases de servicio (
Account,TablesDB,Storage,Functions,Teams,Realtime) instanciadas desde unClientcompartido. Este patrón es consistente en los SDK de cliente y servidor. - División en dos SDKs —
appwrite(cliente) usa autenticación basada en sesión para navegadores.node-appwrite(servidor) usa autenticación mediante clave API para backends. Comparten la misma forma de API pero diferentes mecanismos de autenticación. Nunca los mezcles. - Excepciones, no tuplas — Appwrite lanza
AppwriteExceptionen caso de fallo, no tuplas{ data, error }. Siempre envuelve las llamadas en try/catch. - TablesDB es el futuro — Appwrite 1.8 introdujo TablesDB (tablas/filas/columnas) como la API moderna. La API heredada de Bases de datos (colecciones/documentos/atributos) aún funciona pero está obsoleta. Las nuevas características solo llegan a TablesDB.
- Generación de IDs — Usa
ID.unique()para IDs únicos generados por el servidor. Si pasas una cadena directamente, se convierte en un ID personalizado (que debe ser globalmente único dentro de la tabla).
Cuándo usar Appwrite:
- BaaS de código abierto y auto-alojable con control total sobre tus datos
- Proyectos que necesitan autenticación, base de datos, almacenamiento y funciones en una sola plataforma
- Equipos que buscan un BaaS auto-alojable sin bloqueo de proveedor
- Aplicaciones que se benefician de permisos granulares a nivel de fila/documento
Cuándo NO usarlo:
- Aplicaciones complejas con mucho SQL que requieren uniones, vistas y procedimientos almacenados
- Consumidores de tiempo real del lado del servidor (Appwrite Realtime solo es SDK de cliente — no tiene soporte en el SDK de servidor)
- Aplicaciones offline-first que requieren sincronización local (Appwrite no tiene sincronización offline integrada)
Patrones Fundamentales
Patrón 1: Configuración del SDK de Cliente (Navegador)
Inicializa el cliente de Appwrite con el endpoint y el ID del proyecto. Todas las clases de servicio comparten una única instancia de Client.
// lib/appwrite.ts
import { Client, Account, TablesDB, Storage, Realtime } from "appwrite";
const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT!;
const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID!;
const client = new Client()
.setEndpoint(APPWRITE_ENDPOINT)
.setProject(APPWRITE_PROJECT_ID);
export const account = new Account(client);
export const tablesDB = new TablesDB(client);
export const storage = new Storage(client);
export const realtime = new Realtime(client);
Por qué es bueno: Una única instancia de cliente compartida entre servicios, constantes nombradas para la configuración, exportaciones nombradas para cada servicio, variables de entorno mantienen la configuración fuera del código
// MAL: Configuración hardcodeada, recreando clientes
import { Client, Account } from "appwrite";
const account = new Account(
new Client()
.setEndpoint("https://cloud.appwrite.io/v1") // Hardcodeado
.setProject("abc123"), // ID de proyecto hardcodeado
);
Por qué es malo: Endpoint y ID de proyecto hardcodeados, un nuevo Cliente por servicio desperdicia recursos, no hay configuración compartida
Patrón 2: Configuración del SDK de Servidor (Node.js)
El SDK de servidor usa autenticación mediante clave API — nunca expongas claves API en código de cliente.
// lib/appwrite-server.ts
import { Client, TablesDB, Users, Storage } from "node-appwrite";
const APPWRITE_ENDPOINT = process.env.APPWRITE_ENDPOINT!;
const APPWRITE_PROJECT_ID = process.env.APPWRITE_PROJECT_ID!;
const APPWRITE_API_KEY = process.env.APPWRITE_API_KEY!;
const client = new Client()
.setEndpoint(APPWRITE_ENDPOINT)
.setProject(APPWRITE_PROJECT_ID)
.setKey(APPWRITE_API_KEY);
export const tablesDB = new TablesDB(client);
export const users = new Users(client);
export const storage = new Storage(client);
Por qué es bueno: .setKey() para autenticación mediante clave API (solo servidor), servicio Users para gestión de usuarios administradores (no disponible en el SDK de cliente), constantes nombradas
Cuándo usarlo: Rutas de API, funciones serverless, scripts de administración, webhooks. NUNCA importes este módulo desde código del lado del cliente.
Patrón 3: Manejo de Errores (AppwriteException)
Appwrite lanza AppwriteException — NO devuelve tuplas { data, error }. Siempre usa try/catch.
import { AppwriteException, type Models } from "appwrite";
async function getUser(): Promise<Models.User<Models.Preferences>> {
try {
return await account.get();
} catch (error) {
if (error instanceof AppwriteException) {
// error.message — mensaje legible para humanos
// error.code — código de estado HTTP (401, 404, etc.)
// error.type — cadena de tipo de error legible por máquina
throw new Error(`Error de Appwrite ${error.code}: ${error.message}`);
}
throw error;
}
}
Por qué es bueno: AppwriteException con reducción de tipo proporciona code, message, type; vuelve a lanzar errores desconocidos; tipo de retorno explícito usando el espacio de nombres Models
// MAL: Asumiendo devolución de tupla { data, error }
const { data, error } = await account.get(); // INCORRECTO — Appwrite lanza, no devuelve tuplas
if (error) {
/* ... */
}
Por qué es malo: Appwrite NO devuelve tuplas de error — este código fallará silenciosamente porque account.get() o devuelve datos o lanza una excepción
Patrón 4: Operaciones de TablesDB (CRUD)
Usa TablesDB para todas las operaciones de base de datos. Los IDs de fila son necesarios para crear — usa ID.unique() para generación automática.
Crear una Fila
import { ID, Permission, Role } from "appwrite";
interface Todo {
title: string;
completed: boolean;
}
const DATABASE_ID = "main";
const TABLE_ID = "todos";
const row = await tablesDB.createRow<Todo>({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: ID.unique(),
data: {
title: "Comprar víveres",
completed: false,
},
permissions: [
Permission.read(Role.user(userId)),
Permission.update(Role.user(userId)),
Permission.delete(Role.user(userId)),
],
});
Listar Filas con Consultas
import { Query } from "appwrite";
const PAGE_SIZE = 25;
const result = await tablesDB.listRows<Todo>({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
queries: [
Query.equal("completed", false),
Query.orderDesc("$createdAt"),
Query.limit(PAGE_SIZE),
],
});
// result.rows — array de filas Todo
// result.total — total de coincidencias
Actualizar una Fila
const updated = await tablesDB.updateRow<Todo>({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: row.$id,
data: { completed: true },
});
Eliminar una Fila
await tablesDB.deleteRow({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: row.$id,
});
Por qué es bueno: Constantes nombradas para IDs de base de datos/tablas, ID.unique() para IDs generados automáticamente, permisos establecidos explícitamente en la creación, clase Query para filtrado seguro de tipos, parámetro de tipo genérico para la forma de la fila
Patrón 5: Modelo de Permisos
Appwrite no concede NINGÚN acceso por defecto. Debes establecer permisos en cada fila y archivo.
Sintaxis de Permission + Role
import { Permission, Role } from "appwrite";
// Lectura pública, acceso completo del propietario
const publicReadPermissions = [
Permission.read(Role.any()),
Permission.update(Role.user(userId)),
Permission.delete(Role.user(userId)),
];
// Acceso de equipo
const teamPermissions = [
Permission.read(Role.team(teamId)),
Permission.update(Role.team(teamId, "admin")),
Permission.delete(Role.team(teamId, "owner")),
];
// Solo usuarios verificados
const verifiedOnlyPermissions = [Permission.read(Role.users("verified"))];
Ayudantes de Role Comunes
Role.any(); // Cualquiera (incluyendo invitados)
Role.guests(); // Solo usuarios no autenticados
Role.users(); // Todos los usuarios autenticados
Role.users("verified"); // Solo usuarios verificados
Role.user(userId); // Usuario específico por ID
Role.team(teamId); // Todos los miembros de un equipo
Role.team(teamId, "admin"); // Miembros del equipo con rol "admin"
Role.member(membershipId); // Membresía de equipo específica
Role.label("premium"); // Usuarios con etiqueta "premium"
Por qué es bueno: Permisos explícitos en cada recurso, los ayudantes de rol son seguros de tipos, los roles de equipo permiten acceso granular
// MAL: Crear una fila sin permisos
const row = await tablesDB.createRow({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: ID.unique(),
data: { title: "Secreto" },
// NO se establecieron permisos — ¡esta fila es INVISIBLE para todos!
});
Por qué es malo: Sin permisos, la fila existe en la base de datos pero NINGÚN usuario puede leerla, actualizarla o eliminarla (ni siquiera el creador) — este es el error más común de Appwrite
Patrón 6: Flujos de Autenticación
Usa el servicio Account para todas las operaciones de autenticación.
Registro e Inicio de Sesión con Email/Contraseña
import { ID, type Models } from "appwrite";
// Registro: crear cuenta + crear sesión (dos pasos)
const user = await account.create({
userId: ID.unique(),
email,
password,
name,
});
const session = await account.createEmailPasswordSession({ email, password });
// Inicio de sesión
const session = await account.createEmailPasswordSession({ email, password });
// Cierre de sesión (dispositivo actual)
await account.deleteSession({ sessionId: "current" });
Por qué es bueno: Registro en dos pasos (crear cuenta + crear sesión), parámetros de objeto para todas las llamadas al SDK, deleteSession({ sessionId: "current" }) solo para el dispositivo actual
Consulta examples/auth.md para flujos completos de autenticación que incluyen OAuth, URL mágica, sesiones anónimas, equipos y restablecimiento de contraseña.
Patrón 7: Operaciones de Almacenamiento
Sube, descarga y previsualiza archivos con el servicio Storage.
import { ID, Permission, Role } from "appwrite";
const BUCKET_ID = "user-uploads";
const PREVIEW_WIDTH = 400;
const PREVIEW_HEIGHT = 300;
// Subir con permisos
await storage.createFile({
bucketId: BUCKET_ID,
fileId: ID.unique(),
file,
permissions: [
Permission.read(Role.user(userId)),
Permission.update(Role.user(userId)),
],
});
// Vista previa de imagen con transformaciones
const url = storage.getFilePreview({
…
[truncado para traducción]
---
</patterns>