GenHTTP Lambda

Cómo funciona

Escribes un fragmento de C#. Lo que devuelva queda alojado en una dirección pública, con HTTPS, en unos segundos. Aquí tienes todo, en el orden en que te lo vas a encontrar.

Qué es una lambda

Una lambda es un fragmento de código que devuelve un handler de GenHTTP. La plataforma lo compila, lo carga y monta lo que devuelve en tu propia dirección. No hay proyecto, ni archivo de build, ni sentencias using. Todos los módulos de GenHTTP ya vienen importados.

return Content.From(Resource.FromString("hello"));

Eso ya es una lambda completa. Desplegada en /lambda/your-key/, responde a cada petición con la palabra «hello».

El fragmento son sentencias, no una clase. Lo último que hace es devolver algo que pueda atender peticiones: un handler o un builder que cree uno.

Tu primera lambda

  1. 1
    Haz clic en Crear mi lambda. Recibes una dirección pública y una clave de edición. La clave es la única forma de volver a entrar, así que guárdala. Nadie puede recuperarla por ti.
  2. 2
    Llegas a su centro de control, con un pequeño servicio REST ya escrito como primera versión. Es solo un punto de partida.
  3. 3
    Pásale la clave de edición a un agente y dile qué construir: escribe versiones nuevas a través de MCP. O abre Código y escríbelo tú: Comprobar compila sin guardar nada y te dice qué opina el compilador, con archivo y línea.
  4. 4
    Haz clic en Desplegar. Ya está en línea. Antes de eso no se puede acceder a nada, y cada vez que vuelves a desplegar alargas el tiempo que sigue en línea.

El centro de control

El enlace de edición abre un centro de control, no un cuadro de texto: aquí casi todo el código lo escriben agentes, así que lo primero que ves es cómo está tu lambda. La barra lateral muestra la lambda (si está en línea, su dirección y un botón cuando hay una versión más nueva esperando para ponerse en línea) y sus secciones. Lo que se hace pocas veces, como cambiar la dirección o eliminarla, está en el menú ⋯ de esa barra.

Resumen
Qué es la app, si está en línea, cuántas peticiones tuvo hoy y cuántas fallaron, el último cambio y cuánto espacio le queda.
Documentación
Qué es la app, para quién es y por qué, y por qué está hecha como está: la escriben los agentes y se guarda con cada versión.
Cambiar
Di qué debería ser distinto y el agente de este servidor lo hace mientras miras. Prueba el cambio en un borrador (una copia con una dirección propia) y lo pone en línea cuando funciona. Desactiva Ponerlo en línea al terminar para probar tú el borrador antes. Solo trabaja en tu app: si lo que pides no tiene que ver con ella o busca hacer daño, lo rechaza y te dice por qué.
Borradores
Cambios que se prueban antes de ponerlos en línea, cada uno en su propia dirección y con sus propios datos de prueba. Al abrirlo, un borrador tiene su propio código, datos de prueba y logs. La sección aparece en cuanto hay un borrador.
Archivos
Los archivos de una versión: su código y sus recursos, el programa en sí. Un candado o un globo indica si el público puede acceder a ellos.
Datos
Lo que la lambda guarda mientras se ejecuta, compartido por todas las versiones: la base de datos, el workspace y los secretos, cada uno en su pestaña. Mira las tablas y los archivos, sube archivos, define secretos o activa y desactiva un tipo. La vista simple lo muestra en cuanto la app guarda algo.
Versiones
Qué cambió cada versión, qué se pidió y la diferencia con la anterior. Desde aquí despliegas o vuelves atrás, o empiezas un borrador a partir de cualquiera de ellas.
Despliegues
Qué estuvo en línea y cuándo, y qué lo desconectó.
Estadísticas
Peticiones, fallos, tiempos de respuesta y las rutas más pedidas, en la última hora o el último día.
Logs
Sus peticiones, lo que imprimió y el stack trace de cualquier error, en tiempo real.
Código
Para escribirlo a mano. Comprobar compila, Guardar crea una versión y Desplegar la pone en línea. En un borrador, Guardar lo guarda en el borrador y lo muestra en la dirección del borrador. Ctrl-S guarda; F12 va a una declaración.
Pruebas
Cómo se prueba la app automáticamente, con los scripts y los datos de prueba necesarios. Solo en la vista completa.

Todas las secciones funcionan igual: su título, un ⓘ que la explica, sus acciones a la derecha y, si tiene más de una vista, una fila de pestañas debajo. En el código, las pestañas son sus archivos. La vista completa agrupa las secciones: cómo la encuentra la gente, dónde se hace un cambio, el programa y sus datos, y cómo se ejecuta.

El tráfico y los logs se guardan en memoria, para mirarlos, no para conservarlos: si el servidor se reinicia, empiezan de cero. Las versiones y el historial de despliegues sí se guardan.

Explicar el porqué

Una versión es el código y, si quieres, dos notas sobre él: la especificación, qué quiere el usuario y por qué, con sus palabras si se puede, y el cambio, una línea sobre lo que hace la versión. Se muestran junto al diff en el historial de versiones, así que el porqué se queda junto al qué: para ti y para el próximo agente que lea el historial antes de tocar nada.

POST /api/v1/lambdas/{editorKey}/versions
{
  "files": [ { "name": "lambda.cs", "code": "..." } ],
  "specification": "Un libro de visitas que la gente pueda firmar; las entradas tienen que sobrevivir a un reinicio",
  "change": "Guarda las entradas en la base de datos para que sobrevivan a un reinicio"
}

Los agentes pasan esos mismos dos campos a write_code. En Código, al guardar se te pide el cambio. Los dos son opcionales; una especificación larga se recorta a 4000 caracteres y un cambio a 500, en vez de rechazarse. Un borrador tiene sus propios dos campos, y la versión en la que se fusiona se queda con ellos.

Documentación y pruebas

Cada versión guarda, junto a su programa, lo que está escrito sobre ella: su documentación (qué es la app, para quién es y por qué, y por qué está hecha como está) y sus pruebas: cómo comprobar automáticamente que funciona, con los scripts y los datos de prueba necesarios. Los agentes las escriben con una lambda nueva y las mantienen al día con cada cambio. El próximo agente que cambie la lambda las lee primero, así que sabe para qué sirve la app y qué tiene que seguir funcionando, algo que el código por sí solo no dice.

.lambda/docs/product.md
qué es la app, para quién es, qué hace la gente con ella y por qué
.lambda/docs/decisions.md
las decisiones técnicas, y por qué se tomaron
.lambda/tests/README.md
cómo se prueba la app automáticamente, y cómo ejecutar las pruebas
.lambda/tests/…
los scripts y los datos de prueba que usan las pruebas

Son archivos de la versión como cualquier otro, en la carpeta .lambda: el historial muestra qué cambió en ellos una versión, volver atrás trae de vuelta la documentación que era cierta para esa versión, y un borrador tiene su propia copia, que se pone en línea con él. Nunca se compilan ni se sirven, y cuentan para el límite de los recursos de una versión.

En el centro de control, Documentación muestra las páginas para leer, y Pruebas cómo se prueba la app y los archivos que la acompañan; la versión se elige igual que para sus archivos. Ahí también se puede editar una página, lo que guarda la siguiente versión. La vista sencilla llama a la documentación Acerca de y solo muestra para qué sirve la app: para corregirla, díselo al agente.

Se escriben en el idioma que usas con el agente, para quien cambie la app después, sea una persona o un agente. No son una copia del código: dicen para qué sirve la app, y por qué.

Cambiarla sin riesgo

Una versión nunca cambia una vez guardada, y eso es lo que hace que valga la pena conservarlas todas: cualquiera de ellas se puede comparar y volver a poner en línea exactamente como estaba. Para cambiar una lambda que la gente usa, prueba antes el cambio en un borrador.

  1. 1
    Empiézalo desde cualquier versión en Versiones, o deja que lo empiece el agente. Es una copia del código, los recursos, la documentación y las pruebas de esa versión, y de los datos de la lambda.
  2. 2
    Cámbialo tantas veces como haga falta, en Código o pidiéndoselo al agente. Su vista previa responde en una dirección propia, /features/…/, con datos de prueba propios. Los visitantes de la lambda no ven nada de esto, y nada de lo que escribe llega a los datos de la lambda.
  3. 3
    Poner en línea cuando esté bien: se convierte en la siguiente versión, con sus notas, y queda en línea. El borrador desaparece con ello, junto con su vista previa y sus datos de prueba.
POST /api/v1/lambdas/{editorKey}/features
{ "name": "Ranking" }

PUT  /api/v1/lambdas/{editorKey}/features/{feature}/files?deploy=true
POST /api/v1/lambdas/{editorKey}/features/{feature}/merge
{ "deploy": true }

Se puede trabajar en varios borradores a la vez. Solo uno que esté al día con la versión más nueva puede ponerse en línea, para que nunca deshaga una versión guardada después de que empezara el borrador. Si antes se puso otro en línea, lleva sus cambios a este (o pídeselo al agente) y marca el borrador como al día. Nada se pone en línea solo; es a propósito. La API llama feature a un borrador y merge a ponerlo en línea.

Más de un archivo

Los tipos no tienen por qué estar debajo del código que los usa. En Código, haz clic en + junto a los archivos: el archivo nuevo se compila junto al fragmento, en el mismo namespace, así que no hay que importar nada para usarlo. Un nombre sin extensión se toma como C#.

lambda.cs
var shelf = new Shelf();

return Inline.Create()
             .Get(() => shelf.All())
             .Post((Book book) => shelf.Add(book));
Shelf.cs
public sealed class Shelf
{
    private readonly List<Book> _books = [];

    public IEnumerable<Book> All() => _books;

    public Book Add(Book book)
    {
        _books.Add(book);
        return book;
    }
}

public record Book(string Title, string Author);

Servir una página

Hay dos formas de servir una página, y una más para lo que la gente sube junto a ella.

Una página escrita en el código

Sirve para algo pequeño. La página forma parte del fragmento.

var page = Resource.FromString("""
                              <!doctype html>
                              <title>Mine</title>
                              <h1>It works</h1>
                              """)
                   .Type(new ContentType("text/html; charset=utf-8"));

return Content.From(page);

Una carpeta de archivos reales

Lo que necesitas para cualquier cosa con hoja de estilos y script. Los archivos se añaden igual que un archivo C# y se sirven tal como están escritos. Nada los compila.

return Layout.Create()
             .Add("api", api)
             .Add(Assets.App("site"));

Archivos subidos, desde los datos

Para lo que la gente sube o la lambda crea (fotos, documentos), servido junto a la app. No para las páginas de la propia app: esas van en una carpeta de archivos, donde se versionan junto con el código que las necesita.

return Layout.Create()
             .Add("api", api)
             .Add("uploads", Workspace.Files("uploads"))
             .Add(Assets.App("site"));

Un frontend, paso a paso

La segunda forma, completa. Todas las demos sirven su página así, desde una carpeta llamada web: abre demo-crud para ver una. Las demos son de solo lectura; su clave de edición es su nombre.

  1. 1
    En Código, haz clic en + junto a los archivos y escribe site/index.html. Un nombre con barra pone el archivo en una carpeta; un nombre con extensión se toma como el tipo de archivo que indica.
  2. 2
    Añade site/app.css y site/app.js de la misma forma. Tu página los enlaza por su nombre, como en href="app.css", porque la carpeta es la raíz de lo que se sirve, no parte de la dirección.
  3. 3
    Para todo lo que no sea texto, como una imagen o una fuente, abre un archivo de site y usa el botón de subir que está junto a los archivos: va a parar a la misma carpeta. Un PNG no se puede escribir en un editor de texto, así que esa es la forma de meterlo.
  4. 4
    En lambda.cs, sirve la carpeta:
    return Layout.Create().Add(Assets.App("site"));
  5. 5
    Haz clic en Desplegar. site/index.html responde en /, site/app.css en /app.css, y cualquier dirección que no coincida con un archivo se responde con la página. Así, un frontend con su propio enrutamiento sigue funcionando cuando alguien recarga en un enlace profundo.
  6. 6
    Añade una API al lado y la página tendrá con quién hablar:
    var api = Inline.Create().Get("notes", () => notes);
    
    return Layout.Create()
                 .Add("api", api)
                 .Add(Assets.App("site"));

Los dos lugares donde viven los archivos

Una lambda guarda archivos en dos lugares, y el editor los muestra por separado: Archivos contiene los archivos de una versión (el programa) y Datos contiene el workspace (lo que el programa guarda). La diferencia está en de quién son. Los archivos de una versión pertenecen a esa versión; los datos pertenecen a la lambda, y todas las versiones los comparten.

En una versiónEn los datos
qué contieneel código y los recursos: el programa, frontend incluido, y su documentación y sus pruebaslo que escribe la lambda o sube alguien
cuándo cambianunca: un cambio es una versión nuevaen cuanto se escribe algo en ellos
un desplieguepone en línea exactamente estos archivosnunca los toca
volver atrástrae de vuelta los archivos anterioresno les afecta: todas las versiones los comparten
un borradorempieza como una copia de ellostrabaja con una copia de ellos
cuándo desaparececon las versiones antiguas, al pasar el límitecon la lambda, o cuando desactivas el workspace
cómo se accede desde el códigoAssetsWorkspace

No pueden estar en un solo lugar. Si lo estuvieran, un despliegue borraría todo lo que tu lambda haya escrito desde entonces, o nunca se podría quitar nada de lo que publica. Un juego con ranking quiere lo segundo; la página que sirve, lo primero. Por eso la página va en la versión y el ranking, en los datos.

Guardar registros

Los registros (entradas, cuentas, pedidos, votos) van en la base de datos: una base de datos SQLite propia de la lambda, que se activa en Datos. El código abre una conexión con Database.GetConnection() y lee y escribe los datos mediante Entity Framework Core, con un contexto propio que mapea las tablas:

// migrations/V1__Create_notes.sql:
//   CREATE TABLE notes (id INTEGER PRIMARY KEY, text TEXT NOT NULL);

using (var connection = Database.GetConnection())
{
    new Evolve(connection) { Locations = [Assets.Root + "migrations"] }.Migrate();
}

return Inline.Create()
             .Get("notes", () =>
             {
                 using var db = new Notes(Database.GetConnection());

                 return db.Entries.OrderBy(n => n.Id).Select(n => n.Text).ToList();
             })
             .Post("notes", (NoteInput input) =>
             {
                 using var db = new Notes(Database.GetConnection());

                 db.Entries.Add(new Note { Text = input.Text });

                 return db.SaveChanges();
             });

record NoteInput(string Text);

class Note
{
    public long Id { get; set; }

    public string Text { get; set; }
}

// maps the table the migration made, on the connection it is handed
class Notes(SqliteConnection connection) : DbContext
{
    public DbSet<Note> Entries => Set<Note>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite(connection, contextOwnsConnection: true);

    protected override void OnModelCreating(ModelBuilder model)
        => model.Entity<Note>().ToTable("notes");
}

Sus tablas las crean las migraciones: archivos SQL que van con la versión en migrations/ y que Evolve aplica en orden cuando arranca la lambda, cada uno una sola vez, así que una versión nueva solo ejecuta lo que es nuevo. Nunca cambies una migración que ya se aplicó; un cambio en una tabla es el siguiente archivo.

Como todos los datos, la base de datos la comparten todas las versiones, desplegar o volver atrás no la toca, y un borrador trabaja con una copia. En Datos ves sus tablas y sus filas, que la vista simple llama registros. Descargar como proyecto .NET la incluye como un archivo SQLite normal.

Crea un contexto donde lo necesites y libéralo después, y úsalo de forma síncrona: ToList y SaveChanges, no ToListAsync y SaveChangesAsync. Las tablas las crean las migraciones, nunca Entity Framework. La demo demo-crud hace todo esto.

Guardar archivos

Workspace es un directorio privado que tu lambda puede leer y escribir: el lugar para archivos, como las fotos que sube alguien, un documento que genera o un modelo que carga. Los registros van en la base de datos, y lo que se sabe de un archivo (quién lo subió, cuándo) también es un registro.

// uploads go into a folder of their own, served as they are
Workspace.CreateFolder("photos");

return Layout.Create()
             .Add("photos", Workspace.Files("photos"))
             .Add("upload", Inline.Create().Post(async (Stream body) =>
             {
                 using var content = new MemoryStream();

                 await body.CopyToAsync(content);

                 Workspace.WriteBytes($"photos/{Guid.NewGuid():N}.jpg", content.ToArray());
             }));

También tienes ReadBytes, WriteBytes, Delete, List, CreateFolder y Tree/Files/App para servirlo. No se puede acceder a nada más del sistema de archivos.

Claves y contraseñas

Una clave de API, una contraseña o un token va en los secretos, no en el código, donde lo tendrían cada versión, cada descarga y cualquiera que lea el historial. El código lee un secreto por su nombre:

var weather = new System.Net.Http.HttpClient();

weather.DefaultRequestHeaders.Add("X-Api-Key", Secret.Read("WEATHER_API_KEY"));

return Inline.Create()
             .Get("today", async () => await weather.GetStringAsync("https://weather.example/today"));

Activa los secretos en Datos y define ahí el valor. Una vez guardado, no se vuelve a mostrar, ni a ti ni a un agente: solo puedes reemplazarlo. La lista dice qué nombres lee el código sin que haya todavía un valor, y el resumen los pide. Secret.Exists dice si un secreto está definido, para el código que funciona sin él. Como todos los datos, los secretos los comparten todas las versiones, y un borrador trabaja con una copia.

Se guardan cifrados, con una clave que no está en la base de datos. En un proyecto descargado, Secret.Read("NAME") lee la variable de entorno NAME: los valores se quedan aquí.

WebSockets

Funcionan, y no son un añadido de última hora. La demo demo-game empareja jugadores y ejecuta cada partida en el servidor. La forma más simple son tres callbacks:

var room = new ConcurrentDictionary<IReactiveConnection, string>();

var socket = Websocket.Functional()
                      .OnOpen(c => { room[c] = "someone"; return ValueTask.CompletedTask; })
                      .OnMessage(async (c, text) =>
                      {
                          foreach (var other in room.Keys)
                          {
                              await other.WritePayloadAsync(text);
                          }
                      })
                      .OnClose((c, _) => { room.TryRemove(c, out _); return ValueTask.CompletedTask; });

return Layout.Create().Add("chat", socket);

Cuando la página solo escucha (un contador, un feed, un marcador), los server-sent events son más sencillos: una única respuesta larga en la que el servidor sigue escribiendo y que el navegador reconecta por sí solo. La demo demo-live envía así cada voto a todos los que la están viendo. De un modo u otro, el servidor envía lo que cambió. Una página que vuelve a preguntar cada pocos segundos envía una petición cada vez, haya cambiado algo o no, y aun así llega tarde.

Hay un detalle que sorprende a todo el mundo: un navegador no puede poner cabeceras en el handshake de un websocket. Pasa lo que necesite el handler en la query, donde lo lee desde connection.Request.Header.Query, o envía los secretos en el primer mensaje.

Lo que no te deja hacer

Tu código se ejecuta en un servidor compartido, así que parte de C# se rechaza antes de compilar: iniciar procesos, abrir tus propios sockets, cargar ensamblados, acceder al sistema de archivos fuera de tu workspace y usar reflexión para saltarte cualquiera de esas reglas. Lo mismo ocurre con esperar una tarea con .Result o .Wait() en lugar de usar await: las peticiones se ejecutan en un hilo por núcleo, y la tarea tendría que terminar en el mismo hilo que la está esperando.

Todo lo demás está disponible, incluida toda la API de módulos de GenHTTP. Si algo se rechaza, te decimos en qué línea y por qué, no solo que falló.

Llévate tu código

En el editor, Descargar como proyecto .NET te da todo listo para llevártelo: una solución que puedes abrir, ejecutar con dotnet run y conservar. Solo necesita el paquete de GenHTTP e incluye un Dockerfile para compilarla y ejecutarla como contenedor.

Tu fragmento pasa a ser Project.cs, y Program.cs sirve lo que devuelve. Tus otros archivos llegan exactamente como los escribiste. Workspace y Assets se convierten en dos carpetas junto al programa, con los mismos métodos, aparte en una carpeta Platform, así que no tienes que cambiar nada de tu código. Secret lee allí las variables de entorno con el mismo nombre; los valores se quedan aquí. La documentación y las pruebas también se van contigo, en docs y tests. Database abre database/database.db, que la descarga incluye con los registros que guardó tu app.

Conviene saberlo antes de crear nada aquí: lo que escribes es tuyo y te lo llevas entero. Ejecutarlo en esta máquina no te ata a esta máquina.

Publicar el código

Si lo que creaste puede ayudar a otras personas, publica su código: abre Código abierto en el centro de control, elige una licencia (MIT, salvo que quieras otra) y actívalo. Su código tendrá su propia página entre las apps de código abierto, donde cualquiera puede leerlo, darle una estrella y descargar cualquier versión como el mismo proyecto que te da Descargar como proyecto .NET, con la licencia al lado.

Se publican todas las versiones, también las anteriores, con su documentación, sus pruebas y el cambio que hizo cada una. Lo que conserva la app nunca se publica (sus registros, los archivos que guardó, los valores de sus claves y contraseñas), como tampoco lo que pediste con tus propias palabras ni quién usa la app. Si lo desactivas, la página desaparece; sus estrellas se conservan para cuando vuelvas a publicarlo.

Todo lo que hay en el código se hace público, incluidas las versiones anteriores. Una clave o una contraseña va en Claves y contraseñas, dentro de Datos, nunca en el código, esté publicado o no.

Que lo haga un agente

Hay un endpoint MCP en /mcp. Conecta un agente y podrá hacer todo lo que hace el editor: leer la guía, leer una demo completa, escribir archivos, compilarlos y desplegar. Por debajo es la misma API.

El agente explica el porqué sobre la marcha (write_code recibe la especificación y el cambio) y puede revisar lo que desplegó: read_logs devuelve las peticiones recientes de la lambda, lo que imprimió y el stack trace de cualquier excepción. Así un agente comprueba que su código funciona en vez de suponerlo. Tú ves lo mismo en el centro de control. Escribe la documentación y las pruebas a medida que trabaja, las lee antes de cambiar nada y ejecuta las pruebas contra la dirección de un borrador antes de ponerlo en línea. A una página pensada para que la encuentren le pone un título, una descripción, un icono y una vista previa para cuando alguien comparte su enlace. Al pie de las páginas que construye añade una línea pequeña que dice que se hicieron con GenHTTP Lambda; dile que prefieres no tenerla y la quita.

Más sobre esto →