Inicio rápido
Construí un addon CRUD en 5 minutos — declaralo, no lo programes.
Al terminar esta guía vas a tener:
- Un nuevo scaffold de addon con un
manifest.jsondeclarando un modelo. - El kernel auto-migrando la tabla al instalar y exponiendo endpoints CRUD.
- Una UI tabular funcional en tu app host — ordenable, filtrable, paginada, con diálogos de create/edit/delete — renderizada desde una sola línea
<DynamicTable model="..." />.
Sin código de pegamento. Sin controllers. Sin formularios. El contrato es el manifest.
Tabla de contenidos
- Prerequisitos
- Paso 1 — Scaffoldear un addon
- Paso 2 — Declará tu modelo
- Paso 3 — Instalalo en un host
- Paso 4 — Renderizá la UI
- Paso 5 — Agregá una acción custom
- Lo que conseguís gratis
- Próximos pasos
Prerequisitos
| Herramienta | Para qué |
|---|---|
| Node.js 20+ | Frontend del host, scaffolders. |
| pnpm 9+ | Package manager del workspace. |
| Go 1.22+ | Requerido si compilás el CLI del addon desde fuente o un backend WASM. |
| TinyGo 0.31+ | Solo si tu addon incluye un backend WASM (opcional para esta guía). |
| Un host Metacore corriendo | Cualquier app host embebiendo el kernel, o una app fresca de npx create-metacore-app. |
Si todavía no tenés un host, scaffoldealo en 30 segundos:
npx create-metacore-app my-host
cd my-host
pnpm devcreate-metacore-app cablea @asteby/metacore-starter-config, theme, UI, auth, i18n y el runtime — ver consumer-guide.md para la integración completa.
Paso 1 — Scaffoldear un addon
Instalá el CLI de developer y creá un nuevo directorio de addon:
go install github.com/asteby/metacore-sdk/cli@latest
metacore init tickets
cd ticketsEl scaffold deja:
tickets/
├── manifest.json # el contrato — cada host lee esto
├── migrations/
│ └── 0001_init.sql # DDL inicial, scoped al schema del addon
└── frontend/
└── src/
└── plugin.tsx # entry de UI federada (opcional)El manifest ya declara un modelo (tickets_items) con dos columnas. Reemplacémoslo con algo más interesante.
Paso 2 — Declará tu modelo
Abrí manifest.json y reemplazá model_definitions con:
"model_definitions": [
{
"table_name": "tickets",
"model_key": "tickets",
"label": "Tickets",
"org_scoped": true,
"soft_delete": true,
"columns": [
{ "name": "number", "type": "string", "size": 32, "required": true, "unique": true },
{ "name": "title", "type": "string", "size": 255, "required": true },
{ "name": "description", "type": "text" },
{ "name": "status", "type": "string", "size": 20, "required": true, "default": "'open'", "index": true },
{ "name": "priority", "type": "string", "size": 10, "default": "'normal'" },
{ "name": "due_at", "type": "timestamp" }
]
}
]Validá el manifest:
metacore validate
# ok: tickets@0.1.0 passes validation against kernel 2.0.0validate corre los mismos checks que el marketplace ejecuta al subir: regex de identificadores, whitelist de literales por defecto, scoping de capabilities, semver. Las fallas son ruidosas y específicas.
Buildeá el bundle ya que estás — vas a necesitar el .tar.gz para instalarlo en un host:
metacore build --strict
# built tickets-0.1.0.tar.gz (1 migration, 0 frontend files, 0 backend files, target=webhook)--strict rechaza warnings (capabilities sin scope, faltan razones, dist de frontend sin tag). Usalo para cualquier build de producción.
Paso 3 — Instalalo en un host
En dev, dropeá el directorio del addon en la carpeta de installations del host (o symlinkealo). El kernel observa instalaciones al bootear:
ln -s "$(pwd)" ../my-host/installations/ticketsReiniciá el host. El kernel:
- Parsea
manifest.jsony correAutoMigratecontra el schema Postgres aislado del addon (addon_tickets). - Agrega
org_id(porqueorg_scoped: true),deleted_at(porquesoft_delete: true) y las columnas estándarid/created_at/updated_at. - Registra
/data/tickets(CRUD) y/metadata/table/tickets(metadata de UI) bajo el namespace de routes/m/tickets.
Verificá que esté arriba:
curl http://localhost:8080/api/metadata/table/tickets | jq '.data.columns | length'
# 9Paso 4 — Renderizá la UI
En el frontend del host, montá un componente:
// src/routes/tickets.tsx
import { DynamicTable } from '@asteby/metacore-runtime-react'
export function TicketsPage() {
return (
<div className="h-full p-6">
<h1 className="text-2xl font-semibold mb-4">Tickets</h1>
<DynamicTable model="tickets" />
</div>
)
}Recargá el host. Deberías ver:
- Una tabla con columnas
number,title,status,priority,due_at. - Un buscador, filtros por columna, headers ordenables.
- Paginación con el default que declaró el manifest (o 10).
- Acciones de fila (
view,edit,delete) bajo el dropdown. - Un botón "Crear" que abre un modal manejado por la misma metadata.
Escribiste cero código de rendering. Cada tipo de columna, cada filtro, cada diálogo viene del documento de metadata que el kernel materializó desde tu manifest. Ver dynamic-ui.md para la superficie completa.
Paso 5 — Agregá una acción custom
Declará una acción bajo el modelo:
"actions": {
"tickets": [
{
"key": "resolve",
"label": "Resolve",
"icon": "CheckCircle2",
"confirm": true,
"confirmMessage": "Mark this ticket as resolved?",
"requiresState": ["open", "in_progress"]
}
]
}metacore validate && metacore build --strict — reiniciá el host. El dropdown de la fila ahora muestra una entrada "Resolve". Al clickearla aparece un diálogo de confirmación (<ActionModalDispatcher> decide qué UI renderizar según la forma de la acción) y hace POST a /data/tickets/<id>/action/resolve.
Cableá el lado del servidor vía hooks:
"hooks": {
"tickets::resolve": "/webhooks/resolve_ticket"
}El host postea un envelope firmado HMAC a tu webhook con el id del ticket y la identidad del operador. Ver addon-publishing.md para el formato del envelope.
Para UIs de acción que necesitan campos de formulario, agregá fields: [...] a la acción — <ActionModalDispatcher> va a renderizar un formulario dinámico desde ellos automáticamente. Para modales totalmente custom, registrá un componente:
import { actionRegistry } from '@asteby/metacore-sdk'
actionRegistry.register('tickets', 'resolve', MyResolveDialog)El dispatcher va a usar MyResolveDialog en vez de la confirmación genérica. Ver dynamic-ui.md.
Lo que conseguís gratis
Por aproximadamente 25 líneas de JSON y 1 línea de TSX:
| Capa | Lo que produjo el manifest |
|---|---|
| Base de datos | Tabla addon_tickets.tickets con constraints, índices, FK refs, RLS para org scoping, columna de soft delete. |
| HTTP | Listado paginado, fetch de un registro, create, update, delete, endpoints de acciones custom. |
| Metadata | /metadata/table/tickets, /metadata/modal/tickets, /metadata/all (el endpoint de prefetch). |
| Permisos | Checks de capabilities contra db:read/db:write en el schema propio del addon (implícito) y cualquier acceso cross-schema que declaraste. |
| Frontend | Tabla ordenable/filtrable/paginada, modal de create/edit/view, dispatcher de acciones custom, bulk delete con progreso, filtros sincronizables con URL, gates de capabilities. |
| Lifecycle | Hooks before_create, after_create, before_update, after_update, before_delete, after_delete si los cableás. |
Lo que no escribiste: un controller, un archivo de routes, una migración SQL, un componente de formulario, un renderer de columna, un diálogo de confirmación, una state machine para el botón de acción, un axios.delete, ni un middleware de permisos.
Próximos pasos
dynamic-ui.md— todos los componentes del runtime, con props y patrones de personalización.addon-cookbook.md— recetas: foreign keys, validaciones custom, soft delete, emisión de eventos, modales custom.manifest-spec.md— cada campo demanifest.json.capabilities.md— declarando permisos sandboxed.wasm-abi.md— cuando necesitás lógica server-side con un backend TinyGo.addon-publishing.md— firma, upload y el flujo de review del marketplace.consumer-guide.md— construyendo una app host que consume los packages del SDK.