Explorar la documentación

Primeros pasos

Guía para un proyecto pequeño

Añade ForgeCMS a un proyecto pequeño de Angular/Analog —usuarios, entradas, autenticación con cookies y un panel de administración que funciona— de principio a fin.

Tienes un proyecto pequeño de Angular/Analog. Quieres usuarios, un inicio de sesión, un panel de administración protegido y un par de tipos de contenido. Esta página es ese camino, de principio a fin, y aquí nada es una aspiración: cada fragmento es el mismo código que demuestra apps/tiny-project en este repositorio (spec 055), un consumidor deliberadamente diminuto, con el estilo de un proyecto externo, construido solo sobre las exportaciones públicas de @forge-cms/* que aparecen abajo.

1. Instalación

pnpm add @forge-cms/core @forge-cms/runtime @forge-cms/auth @forge-cms/db @forge-cms/storage \
  @forge-cms/angular @forge-cms/admin

Añade los adaptadores de tu perfil duradero (consulta el §9):

pnpm add @forge-cms/cloudflare   # Cloudflare: D1 + R2
pnpm add @forge-cms/s3           # portable: libSQL (in @forge-cms/db) + S3

2. Definir usuarios y entradas

// server/collections.ts
import { defineCollection, defineField } from '@forge-cms/core';
import { defineUsersCollection } from '@forge-cms/auth';

export const users = defineUsersCollection();

export const posts = defineCollection({
  slug: 'posts',
  drafts: true,
  admin: { useAsTitle: 'title', defaultColumns: ['title', 'author'] },
  fields: {
    title: defineField.text({ required: true }),
    slug: defineField.slug({
      required: true,
      unique: true,
      autoGenerate: true,
      sourceField: 'title'
    }),
    body: defineField.richtext(),
    author: defineField.relation({ collection: 'users', required: true })
  },
  access: {
    read: () => true,
    create: ({ user }) => user?.role === 'admin' || user?.role === 'editor',
    update: ({ user }) => user?.role === 'admin' || user?.role === 'editor',
    delete: ({ user }) => user?.role === 'admin'
  }
});

export const collections = [users, posts];

defineUsersCollection() es la forma recomendada para users: email, nombre, un select de role (admin | editor | viewer, con viewer por defecto) y passwordHash ya conectado. No estás obligado a usarla —withAuthFields() combina los mismos campos del adaptador de autenticación con una colección que escribas a mano—, pero es el camino más rápido y el que usa esta guía de principio a fin.

3. Elegir los adaptadores y crear el runtime

Esta es la factoría de desarrollo: adaptadores en memoria, nada duradero. Es lo que quieres mientras aprendes y en las pruebas. Una compilación de producción debe usar en su lugar una factoría de perfil duradero (D1 + R2, o libSQL + S3) que lance un error ante una configuración incompleta; consulta el §9 y Deployment (en inglés).

// server/runtime.ts
import { InMemoryDatabaseAdapter } from '@forge-cms/db';
import { UsersCollectionAuthAdapter } from '@forge-cms/auth';
import { InMemoryStorageAdapter } from '@forge-cms/storage';
import { D1DatabaseAdapter, type D1Database } from '@forge-cms/cloudflare';
import { ForgeCmsRuntime } from '@forge-cms/runtime';
import { collections } from './collections';

export interface ServerEnv {
  DB?: D1Database;
  AUTH_SECRET?: string;
}

let runtimePromise: Promise<ForgeCmsRuntime<ServerEnv>> | undefined;

export function getServerRuntime(env?: ServerEnv): Promise<ForgeCmsRuntime<ServerEnv>> {
  runtimePromise ??= buildRuntime(env);
  return runtimePromise;
}

async function buildRuntime(env?: ServerEnv): Promise<ForgeCmsRuntime<ServerEnv>> {
  // DEVELOPMENT ONLY — never ship this selector: a missing binding silently becomes in-memory.
  const database = env?.DB ? new D1DatabaseAdapter() : new InMemoryDatabaseAdapter();
  // Explicit dev mode (never inferred from a missing secret): `true` only under the dev server.
  const auth = new UsersCollectionAuthAdapter({ devMode: import.meta.dev === true }).init({
    ...env,
    userDatabase: database
  });

  const runtime = new ForgeCmsRuntime<ServerEnv>({
    collections,
    adapters: { database, auth, storage: new InMemoryStorageAdapter() },
    ...(env !== undefined && { env })
  });

  runtime.init();
  await runtime.syncSchema();
  return runtime;
}

Constrúyelo de forma diferida, en la primera petición, y no al cargar el módulo: un Cloudflare Worker prohíbe la E/S asíncrona en el ámbito del módulo, y esta estructura funciona exactamente igual fuera de Cloudflare. En esta factoría de desarrollo, env?.DB elige D1 cuando existe el binding y la memoria en caso contrario, y precisamente por eso no sirve para elegir en producción: un binding ausente o mal nombrado perdería datos sin avisar. En producción se usa la factoría de perfil del §9.

4. Montar la API de servidor

Cada ruta es un envoltorio fino: la lógica real vive en el manejador, no en el archivo de la ruta. En Analog/h3:

// server/routes/api/auth/login.post.ts
import { defineEventHandler, toWebRequest } from 'h3';
import { handleLogin } from '@forge-cms/runtime';
import { getServerRuntime } from '../../../runtime';

export default defineEventHandler(async (event) => {
  const runtime = await getServerRuntime(event.context.cloudflare?.env);
  return handleLogin(
    { request: toWebRequest(event), env: event.context.cloudflare?.env },
    { runtime, cookie: { secure: !!event.context.cloudflare?.env } }
  );
});

Repite el mismo patrón de cuatro líneas para signup.post.ts (handleSignup, protegido tras tu propio indicador enabled, desactivado por defecto), logout.post.ts (handleLogout) y me.get.ts (handleMe). Después vienen las rutas CRUD genéricas de colecciones, con la misma forma y un archivo por verbo: GET/POST /api/v1/[collection] (handleList/handleCreate) y GET/PUT/DELETE /api/v1/[collection]/[id] (handleRead/handleUpdate/handleDelete). Consulta REST API (en inglés) para ver el contrato completo que producen.

5. Montar el panel de administración

// app/admin.routes.ts
import type { Routes } from '@angular/router';
import {
  ForgeAdminLayoutComponent,
  ForgeUsersWorkspaceComponent,
  forgeAdminAuthRoutes,
  forgeAdminContentRoutes
} from '@forge-cms/admin';
import { forgeAuthGuard } from '@forge-cms/angular';

export const ADMIN_ROUTES: Routes = [
  ...forgeAdminAuthRoutes({ signup: true }),
  {
    path: '',
    component: ForgeAdminLayoutComponent,
    canActivate: [forgeAuthGuard()],
    children: [
      { path: '', pathMatch: 'full', redirectTo: 'collections' },
      ...forgeAdminContentRoutes(),
      {
        path: 'users',
        component: ForgeUsersWorkspaceComponent,
        canActivate: [forgeAuthGuard({ roles: ['admin'] })]
      }
    ]
  }
];

Ese es todo el panel: inicio de sesión y registro, un contenedor protegido, el índice reutilizable de colecciones y el editor de documentos, y un espacio de usuarios reservado a los administradores. Sin ninguna página CRUD escrita por la aplicación anfitriona. En app.config.ts, añade provideForgeCms({ baseUrl: '/api/v1' }), sin authToken: la sesión del navegador es la cookie HttpOnly forge_session, y CmsApiService la envía automáticamente.

¿Lo quieres en /studio en lugar de /admin, o tus APIs en otro sitio que no sea /api/v1 y /api/auth? Es configuración, no código: basePath en forgeAdminAuthRoutes() y en la configuración del layout, signInPath/forbiddenPath en el guard, baseUrl/authBaseUrl en provideForgeCms(); consulta Admin UI (en inglés).

Algo a tener en cuenta: @forge-cms/angular y @forge-cms/admin incluyen código Ivy parcial, que necesita el linker de Angular en el momento de compilar tu aplicación; de lo contrario te encontrarás con un fallo JIT compiler unavailable que solo aparece en producción. Si usas Vite (Analog.js), añade el plugin que ForgeCMS incluye exactamente para esto (@forge-cms/admin/vite reexporta el mismo plugin):

// vite.config.ts
import { angularLinker } from '@forge-cms/angular/vite';

export default defineConfig({
  plugins: [angularLinker() /* your other plugins, e.g. analog() */]
});

6. Crear el primer administrador

No hay ningún script de semillas ni ningún endpoint de arranque aparte al que llamar: el primer usuario que se crea en una instalación nueva siempre se promueve a admin, sea cual sea el rol que indiques:

const admin = await auth.createUser({ email: '[email protected]', password: 'a-real-password' });
// admin.user.role === 'admin', even though no role was requested

Una ruta que proteja esto tras la condición «todavía no existe ningún usuario» son unas pocas líneas de tu propio código de servidor, no una nueva capacidad de Forge:

// server/routes/api/bootstrap-admin.post.ts
export default defineEventHandler(async (event) => {
  const runtime = await getServerRuntime(event.context.cloudflare?.env);
  const auth = runtime.adapters.auth as UsersCollectionAuthAdapter;

  if ((await auth.listUsers()).length > 0) {
    throw createError({ statusCode: 409, statusMessage: 'Already initialized' });
  }

  const body = await readBody(event);
  const created = await auth.createUser({ email: body.email, password: body.password });
  if (!created.ok) throw createError({ statusCode: 400, statusMessage: created.reason });
  return { data: { user: created.user } };
});

Pon un pequeño formulario delante (o simplemente llámala una vez con curl) y tendrás un flujo real de primer arranque: cero filas, cero usuarios, una petición y un panel de administración que funciona.

7. Iniciar sesión

Visita /admin/login. Si todo va bien, el servidor establece la cookie forge_session y la sesión de Angular (ForgeAuthSession, inyectada donde necesites session.user()/session.authenticated()) lo refleja de inmediato, sin necesidad de recargar la página; además, la sesión sobrevive a una recarga real porque es una cookie, no localStorage.

8. Gestionar el contenido

/admin/collections lista todas las colecciones registradas; /admin/collections/posts es un espacio de trabajo completo —búsqueda, ordenación, filtro por estado, paginación, crear/editar/publicar/borrar— generado a partir de la definición de posts del §2. El campo author se muestra como un selector con búsqueda sobre users, no como una caja de texto donde pegar un id. Nada de esto es código de la aplicación anfitriona.

9. Cloudflare frente a portable

Los dos perfiles duraderos ejecutan exactamente el mismo dominio del §2; solo cambian los adaptadores de base de datos y de archivos. Producción selecciona exactamente un perfil completo y, si no, se niega a arrancar (nunca recurre a la memoria). El profile.ts de apps/tiny-project es la referencia probada; Deployment (en inglés) contiene las recetas ejecutables.

Cloudflare — D1 + R2 (el camino mejor soportado):

import { D1DatabaseAdapter, R2StorageAdapter } from '@forge-cms/cloudflare';
if (!env.DB || !env.BUCKET)
  throw new Error('Incomplete Cloudflare profile: DB and BUCKET are required');
const database = new D1DatabaseAdapter(); // .init(env) happens inside runtime.init()
const storage = new R2StorageAdapter();

Configuración: un binding de D1 (DB) y otro de R2 (BUCKET) en wrangler.toml, y AUTH_SECRET (al menos 32 bytes) en todas las compilaciones; solo el servidor de desarrollo, mediante un devMode: true explícito, recurre al secreto público de desarrollo de Forge. Consulta Deployment y Browser auth (en inglés).

Portable — libSQL + S3 (sin cuenta de Cloudflare ni binding de ningún tipo):

import { LibSqlDatabaseAdapter } from '@forge-cms/db';
import { S3StorageAdapter } from '@forge-cms/s3';
// DATABASE_URL=file:/data/forge.db (a persistent volume!) or a libsql:// URL; S3_BUCKET, S3_REGION, …
const database = new LibSqlDatabaseAdapter(process.env['DATABASE_URL']!);
const storage = new S3StorageAdapter({ bucket, region, endpoint, forcePathStyle, credentials });

pnpm add @forge-cms/s3 lo añade. @forge-cms/db carga libSQL en la primera operación, así que init() no abre la base de datos (una URL incorrecta hace fallar la primera llamada). El bucket de S3 debe existir de antemano.

El mismo UsersCollectionAuthAdapter, las mismas collections, el mismo runtime y el mismo panel de administración. La sincronización del esquema, la creación del primer administrador, el inicio de sesión, los usuarios, el CRUD de entradas, los borradores, la relación y las subidas de archivos funcionan sin cambios.

Subidas y archivos

Añade una colección con upload: true (consulta Uploads, en inglés), monta POST /api/v1/[collection] y la ruta de archivos GET /api/media/[...key] → handleFile. Ambos perfiles almacenan los archivos de forma duradera (R2 o S3) y los sirven mediante handleFile, que aplica las reglas de acceso del documento propietario. Mantén los buckets privados.

Renderizado en el servidor y producción (S01–S03)

Las páginas públicas se renderizan en el servidor y se hidratan: establece ssr: true en analog(), proporciona provideForgeCmsServer({ origin }) en main.server.ts, lee el contenido público con un cliente con credentials: 'omit' y collectionResource(params, { transfer: 'public' }), y compila con el angularLinker() público de @forge-cms/angular/vite. La guía de SSR (en inglés) tiene cada fragmento y las dos configuraciones de producción (Cloudflare Pages + D1 + R2; node-server + libSQL + S3 con nitro: { externals: { trace: false } } y node_modules incluido junto a dist/). El camino de esta guía —instalación → colecciones → factoría de perfil → manejadores de autenticación → panel → primer administrador → SSR → cliente público → hidratación → subida → compilación → servir → reiniciar → recuperar— se ejercita de extremo a extremo, solo a partir de paquetes públicos empaquetados, con pnpm test:s3 profiles (Node compilado para producción + libSQL en disco + Garage S3, y la salida de Cloudflare Pages bajo workerd local + D1 local + R2 local: es evidencia con workerd local; el ensayo remoto aislado en Cloudflare se describe en Deployment) y pnpm release:ssr (el consumidor técnico de SSR).

Siguientes pasos

  • Inicio rápido: la Local API mínima, sin autenticación ni panel.
  • Browser auth (en inglés): el contrato de sesión con cookies, CSRF y registro en el que se apoya esta guía.
  • Admin UI (en inglés): todos los componentes que exporta @forge-cms/admin.
  • Deployment (en inglés): los dos perfiles duraderos al completo, además de la copia de seguridad y la recuperación.