Inicio rápido de embedding
Tu primer host con el kernel embebido — en 10 minutos.
Tabla de contenidos
- Objetivo
- Prerrequisitos
- 1. Nuevo módulo Go
- 2. Conectar main.go
- 3. Storage y migraciones
- 4. Levantar el plano de addons
- 5. Instalar tu primer addon
- 6. Verificar los endpoints de CRUD dinámico
- 7. Combinar con un frontend
- Próximos pasos
Objetivo
Levantar un servidor HTTP basado en Fiber que:
- expone
auth + metadata + CRUD dinámico + hub de WebSocket(víahost.App), - corre el plano de ciclo de vida de addons (vía
host.Host), - acepta un bundle de addon de ejemplo y convierte sus
model_definitions[]en endpoints CRUD vivos, - aplica capabilities de usuario y capabilities de addon.
Si solo querés la capa de CRUD dinámico sin el plano de addons, salteá la sección 4.
Prerrequisitos
| Tool | Versión |
|---|---|
| Go | 1.25+ |
| Postgres | 14+ |
El kernel es público, así que un simple go get github.com/asteby/metacore-kernel funciona sin más. Si tu host depende de módulos Go privados tuyos, ver consumer-guide.md para el setup de GOPRIVATE.
1. Nuevo módulo Go
mkdir my-host && cd my-host
go mod init example.com/my-host
go get github.com/asteby/metacore-kernel@latest
go get github.com/gofiber/fiber/v2 gorm.io/gorm gorm.io/driver/postgres github.com/google/uuid2. Conectar main.go
package main
import (
"log"
"os"
"github.com/gofiber/fiber/v2"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"github.com/asteby/metacore-kernel/host"
"github.com/asteby/metacore-kernel/permission"
)
func main() {
db, err := gorm.Open(
postgres.Open(os.Getenv("DATABASE_URL")),
&gorm.Config{},
)
if err != nil {
log.Fatalf("db: %v", err)
}
// GORM-backed permission store. Production default.
permStore, err := permission.NewGormStore(db)
if err != nil {
log.Fatalf("permission store: %v", err)
}
app := host.NewApp(host.AppConfig{
DB: db,
JWTSecret: []byte(host.MustGetenv("JWT_SECRET")),
RunMigrations: true, // versioned SQL via migrations.Runner
EnableMetrics: true, // exposes /api/metrics
EnableWebhooks: true,
PermissionStore: permStore, // turn on user-level CRUD gates
})
defer app.Stop()
fiberApp := fiber.New()
api := app.Mount(fiberApp.Group("/api"))
// Layer your own domain endpoints on top of the kernel's.
api.Get("/me", whoAmI)
log.Fatal(fiberApp.Listen(":3000"))
}
func whoAmI(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"ok": true})
}Lo que esto te da gratis, sin escribir ni un solo handler:
| Mount point | Origen |
|---|---|
POST /api/auth/login | auth/ |
POST /api/auth/refresh | auth/ |
GET /api/metadata/table/:model | metadata/ |
GET /api/metadata/modal/:model | metadata/ |
GET /api/metadata/all | metadata/ |
GET/POST/PUT/DELETE /api/dynamic/:model | dynamic/ (auto-montado) |
GET /api/options/:model | dynamic/ (el host llama MountOptions para habilitarlo) |
GET /api/search/:model | dynamic/ (el host llama MountOptions para habilitarlo) |
GET /api/webhooks/* | webhooks/ |
GET /api/ws?token=… | ws/ |
GET /api/metrics | metrics/ (montado en el mismo router pasado a Mount) |
La lista completa de routes y los knobs de configuración están en host/app.go.
3. Storage y migraciones
RunMigrations: true invoca el runner basado en Goose (migrations/runner.go) en cada boot — idempotente, estado trackeado en la tabla goose_db_version. Este es el path recomendado para producción.
Setearlo a false cae en AutoMigrate de GORM para las tablas propias del kernel — está bien localmente, inseguro entre upgrades del kernel.
PostgreSQL es el driver soportado en producción; SQLite se usa solo en tests.
4. Levantar el plano de addons
Si tu host debería aceptar bundles de addons (install / enable / disable / uninstall, hooks de ciclo de vida, merge de navegación, schema dinámico), construí un host.Host al lado del host.App. Comparten el mismo *gorm.DB.
import "github.com/asteby/metacore-kernel/host"
h, err := host.New(host.Config{
DB: db,
KernelVersion: "0.2.0",
Services: map[string]any{
// Anything addon Boot() hooks need.
// "eventbus": eventBus,
},
})
if err != nil {
log.Fatalf("host.New: %v", err)
}
if err := h.Boot(); err != nil {
log.Fatalf("Boot: %v", err)
}host.Host (host/host.go) es dueño del Installer, Lifecycles e Interceptors. Los addons compilados in-process se registran antes de Boot:
h.RegisterCompiled("billing", &billing.Addon{})5. Instalar tu primer addon
Leé un bundle tickets.tgz (producido por metacore build) desde disco y pasáselo al installer:
import (
"os"
"github.com/asteby/metacore-kernel/bundle"
"github.com/google/uuid"
)
f, err := os.Open("/var/addons/tickets.tgz")
if err != nil {
log.Fatalf("open bundle: %v", err)
}
defer f.Close()
b, err := bundle.Read(f, 64<<20) // 64 MiB max decompressed
if err != nil {
log.Fatalf("read bundle: %v", err)
}
orgID := uuid.MustParse("11111111-1111-1111-1111-111111111111")
inst, secret, err := h.Installer.Install(orgID, b)
if err != nil {
log.Fatalf("install: %v", err)
}
log.Printf("installed %s@%s id=%s (secret len=%d)", inst.AddonKey, inst.Version, inst.ID, len(secret))Installer.Install (installer/installer.go):
- Valida el manifest contra la
KernelVersioncorriendo. - Crea el schema Postgres del addon (
addon_tickets). - Aplica cualquier migración SQL versionada incluida en el bundle.
- Para cada entrada
model_definitions[]:CREATE TABLE IF NOT EXISTSyADD COLUMN IF NOT EXISTS(sync aditivo). - Dispara el ciclo de vida
OnInstally luegoOnEnable. - Persiste la fila
metacore_installationscon un secret HMAC fresco por instalación (devuelto al caller, hasheado en reposo).
No hay un comando metacore migrate separado — instalar es el trigger de migración. Re-correr el install sobre el mismo bundle es seguro.
Para los modelos que el host necesita direccionar por short key desde URLs de CRUD, registrá la factory después de instalar:
import (
"github.com/asteby/metacore-kernel/modelbase"
)
app.RegisterModel("tickets", func() modelbase.ModelDefiner {
// Return a fresh instance that satisfies modelbase.ModelDefiner.
// Compiled-in models implement the interface directly; for purely
// declarative addons, hosts typically synthesize an instance from
// the manifest (dynamic.BuildStructType + a small ModelDefiner shim).
return &tickets.Ticket{}
})Mirá dynamic-system.md para el walkthrough completo del installer y cómo el registry alimenta la capa de CRUD dinámico.
6. Verificar los endpoints de CRUD dinámico
# Authenticate (replace with your auth flow).
JWT="$(curl -s -X POST -H 'Content-Type: application/json' \
-d '{"email":"alice@example.com","password":"secret"}' \
http://localhost:3000/api/auth/login | jq -r .data.token)"
# Probe metadata.
curl -s -H "Authorization: Bearer $JWT" \
http://localhost:3000/api/metadata/table/tickets | jq
# Create.
curl -s -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
-d '{"subject":"Test","status":"open","priority":"normal"}' \
http://localhost:3000/api/dynamic/tickets | jq
# List.
curl -s -H "Authorization: Bearer $JWT" \
"http://localhost:3000/api/dynamic/tickets?per_page=10&sortBy=created_at&order=desc" | jqRespuesta esperada de list:
{
"success": true,
"data": [ /* tickets */ ],
"meta": { "total": 1, "page": 1, "per_page": 10, "last_page": 1 }
}La referencia completa de request/response está en dynamic-api.md.
Si recibís {"success": false, "message": "permission denied: ..."}, al usuario le falta la capability relevante — sembrá un grant de role:
_ = permStore.GrantRole(ctx, permission.RoleAdmin, permission.Cap("tickets", "create"))
_ = permStore.GrantRole(ctx, permission.RoleAdmin, permission.Cap("tickets", "read"))
_ = permStore.GrantRole(ctx, permission.RoleAdmin, permission.Cap("tickets", "update"))
_ = permStore.GrantRole(ctx, permission.RoleAdmin, permission.Cap("tickets", "delete"))Mirá permissions.md para el modelo completo de capabilities.
7. Combinar con un frontend
Los frontends que corren @asteby/metacore-runtime-react consumen los endpoints de metadata + CRUD sin código por modelo:
import { DynamicTable } from "@asteby/metacore-runtime-react";
export default function TicketsPage() {
return <DynamicTable model="tickets" />;
}Conectá el runtime a la base URL de tu host y al JWT — la Guía del consumidor del SDK cubre la integración con React de punta a punta. El contrato entre este kernel y el SDK es la shape JSON de TableMetadata, ModalMetadata, el envelope de respuesta de CRUD dinámico y el formato de mensajes de WebSocket — todo estable entre versiones minor.
Próximos pasos
dynamic-system.md— qué pasa realmente cuando un addon publicamodel_definitions[].dynamic-api.md— cada endpoint, cada parámetro.permissions.md— gates de usuario, gates de addon, modos.consumer-guide.md— guía extensa de embedding.dev-setup.md— cómo contribuir al kernel mismo.