GenHTTP Lambda

Zo werkt het

Je schrijft een stukje C#. Wat dat teruggeeft, staat binnen een paar seconden online op een openbaar adres, via HTTPS. Hier staat alles, in de volgorde waarin je het tegenkomt.

Wat een lambda is

Een lambda is een snippet die een GenHTTP-handler teruggeeft. Het platform compileert hem, laadt hem en hangt wat hij teruggeeft onder je eigen adres. Er is geen project, geen buildbestand en geen using nodig. Alle GenHTTP-modules zijn al voor je geïmporteerd.

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

Dat is een complete lambda. Gedeployd op /lambda/your-key/ beantwoordt hij elk request met het woord hello.

De snippet bestaat uit statements, niet uit een class. Het laatste wat hij doet, is iets teruggeven dat requests kan afhandelen: een handler, of een builder daarvoor.

Je eerste lambda

  1. 1
    Klik op Lambda aanmaken. Je krijgt een openbaar adres en een editorsleutel. De sleutel is de enige weg terug, dus bewaar hem goed. Niemand kan hem voor je herstellen.
  2. 2
    Je komt terecht in het dashboard, waar als eerste versie al een kleine REST-service klaarstaat. Dat is maar een beginpunt.
  3. 3
    Geef de editorsleutel aan een agent en vertel wat hij moet bouwen. Hij schrijft nieuwe versies via MCP. Of open Code en schrijf het zelf: Controleren compileert zonder iets op te slaan en laat zien wat de compiler ervan vindt, met bestand en regel.
  4. 4
    Klik op Deployen. Nu staat hij online. Tot dan is er niets bereikbaar. Opnieuw deployen verlengt hoe lang hij online blijft.

Het dashboard

De editorlink opent geen tekstvak maar een dashboard. De meeste code hier schrijven agents, dus het eerste wat je ziet, is hoe het met je lambda gaat. In de zijbalk staat de lambda zelf: of hij online is, zijn adres, en een knop als er een nieuwere versie klaarstaat om online te gaan. Daaronder staan de onderdelen. Wat je zelden doet, zoals het adres wijzigen of de lambda verwijderen, zit daar achter het menu ⋯.

Overzicht
Wat de app is, of hij online is, hoeveel requests hij vandaag had en hoeveel daarvan misgingen, de laatste wijziging, en hoeveel ruimte er nog over is.
Documentatie
Wat de app is, voor wie hij is en waarom, en waarom hij gebouwd is zoals hij is – geschreven door agents, bewaard bij elke versie.
Aanpassen
Zeg wat er anders moet, en de agent op deze server doet het terwijl jij meekijkt. Hij probeert de wijziging uit in een concept – een kopie met een eigen adres – en zet het online zodra het werkt. Zet Online zetten als het klaar is uit als je het concept eerst zelf wilt uitproberen. Hij werkt alleen aan je app: een verzoek dat er niets mee te maken heeft, of dat schade moet aanrichten, wijst hij af, en hij zegt waarom.
Concepten
Wijzigingen die worden uitgeprobeerd voordat ze online gaan, elk op een eigen adres en met eigen testdata. Open je een concept, dan heeft het zijn eigen code, testdata en logs. Het onderdeel verschijnt zodra er een concept is.
Bestanden
De bestanden van een versie: de code en assets, het programma zelf. Een slotje of een wereldbol laat zien of ze openbaar bereikbaar zijn.
Data
Wat de lambda bewaart terwijl hij draait, gedeeld door elke versie: de database, de workspace en de sleutels en wachtwoorden, elk met een eigen tabblad. Bekijk de tabellen en bestanden, upload bestanden, stel sleutels en wachtwoorden in of zet een soort aan of uit. De eenvoudige weergave toont het zodra de app iets bewaart.
Versies
Wat elke versie veranderde en wat er gevraagd werd, en het verschil met de vorige. Van hieruit deploy je of zet je een versie terug, en vanuit elke versie kun je een concept starten.
Deployments
Wat wanneer online stond, en waardoor het offline ging.
Statistieken
Requests, fouten, responstijden en de meest opgevraagde paden, over het afgelopen uur of de afgelopen dag.
Logs
Requests, output en de stacktrace van alles wat misging, live.
Code
Zelf schrijven. Controleren compileert, Opslaan maakt een versie, Deployen zet hem online. In een concept bewaart Opslaan de code in het concept en toont die op het adres van het concept. Ctrl-S slaat op; F12 springt naar een declaratie.
Tests
Hoe de app automatisch getest wordt, met de scripts en testdata daarvoor. Alleen in de volledige weergave.

Elk onderdeel werkt hetzelfde: een titel, een ⓘ met uitleg, acties rechts en, als er meer dan één weergave is, een rij tabs eronder. Bij de code zijn de tabs de bestanden. De volledige weergave deelt de onderdelen in groepen in: hoe mensen hem vinden, waar een wijziging gemaakt wordt, het programma en zijn data, en hoe hij draait.

Het verkeer en de logs staan in het geheugen. Ze zijn om mee te kijken, niet om te bewaren: na een herstart van de server beginnen ze opnieuw. Versies en de deploygeschiedenis worden wel opgeslagen.

Uitleggen waarom

Een versie is de code, plus eventueel twee notities: de specificatie (wat de gebruiker wil en waarom, zo veel mogelijk in eigen woorden) en de wijziging (één regel over wat de versie doet). Ze staan naast de diff in de versiegeschiedenis. Zo blijft het waarom bewaard naast het wat, voor jou en voor de volgende agent die de geschiedenis leest voordat hij iets verandert.

POST /api/v1/lambdas/{editorKey}/versions
{
  "files": [ { "name": "lambda.cs", "code": "..." } ],
  "specification": "Een gastenboek dat mensen kunnen tekenen; berichten moeten een herstart overleven",
  "change": "Bewaart berichten in de database, zodat ze een herstart overleven"
}

Agents geven dezelfde twee velden mee aan write_code. In Code wordt bij het opslaan om de wijziging gevraagd. Beide zijn optioneel. Een lange specificatie wordt afgekapt op 4000 tekens en een wijziging op 500, in plaats van geweigerd. Een concept heeft zijn eigen twee, en de versie waarin het wordt samengevoegd, neemt ze over.

Documentatie en tests

Bij elke versie hoort, naast het programma, wat erover geschreven is: de documentatie – wat de app is, voor wie hij is en waarom, en waarom hij gebouwd is zoals hij is – en de tests: hoe je automatisch controleert dat hij werkt, met de scripts en testdata daarvoor. Agents schrijven ze bij een nieuwe lambda en houden ze bij met elke wijziging. De volgende agent die de lambda aanpast, leest ze eerst, zodat hij weet waar de app voor is en wat moet blijven werken – wat de code alleen niet vertelt.

.lambda/docs/product.md
wat de app is, voor wie hij is, wat mensen ermee doen en waarom
.lambda/docs/decisions.md
de technische beslissingen, en waarom ze genomen zijn
.lambda/tests/README.md
hoe de app automatisch getest wordt, en hoe je de tests uitvoert
.lambda/tests/…
de scripts en testdata die de tests gebruiken

Het zijn bestanden van de versie zoals alle andere, in de map .lambda: de geschiedenis laat zien wat een versie erin veranderde, terugzetten haalt de documentatie terug die voor die versie gold, en een concept heeft een eigen kopie die met het concept mee online gaat. Ze worden nooit gecompileerd en nooit geserveerd, en tellen mee voor de ruimte die de assets van een versie mogen innemen.

In het dashboard toont Documentatie de pagina's om te lezen, en Tests hoe de app getest wordt en de bestanden ernaast; de versie kies je net als bij de bestanden. Je kunt een pagina daar ook bewerken; dat slaat de volgende versie op. De eenvoudige weergave noemt de documentatie Over de app en toont alleen waar de app voor is – wil je dat verbeteren, zeg het dan tegen de agent.

Ze worden geschreven in de taal die je met de agent gebruikt, voor wie de app hierna aanpast – een mens of een agent. Geen kopie van de code: waar hij voor is, en waarom.

Veilig aanpassen

Een versie verandert nooit meer als hij eenmaal is opgeslagen, en juist daardoor is elke versie het bewaren waard: je kunt ze allemaal vergelijken en precies zoals ze waren weer online zetten. Wil je een lambda aanpassen die mensen gebruiken, probeer de wijziging dan eerst uit in een concept.

  1. 1
    Start het vanuit een willekeurige versie onder Versies, of laat de agent er een starten. Het is een kopie van de code, assets, documentatie en tests van die versie, en van de data van de lambda.
  2. 2
    Pas het zo vaak aan als nodig, in Code of door het aan de agent te vragen. De voorvertoning draait op een eigen adres, /features/…/, met eigen testdata. Bezoekers van de lambda zien er niets van, en niets wat het wegschrijft, komt in de data van de lambda terecht.
  3. 3
    Klik op Online zetten zodra het goed is: het wordt de volgende versie, met zijn notities, en gaat online. Het concept verdwijnt dan, met zijn voorvertoning en zijn testdata.
POST /api/v1/lambdas/{editorKey}/features
{ "name": "Ranglijst" }

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

Je kunt aan meerdere concepten tegelijk werken. Alleen een concept dat up-to-date is met de nieuwste versie kan online gaan, zodat het nooit een versie ongedaan maakt die is opgeslagen nadat het concept begon. Is er eerst een ander online gezet, haal dan de wijzigingen daarvan binnen - of vraag de agent dat te doen - en markeer het concept als bijgewerkt. Niets gaat vanzelf online; dat is met opzet. De API noemt een concept een feature, en het online zetten een merge.

Meer dan één bestand

Types hoeven niet onder de code te staan die ze gebruikt. Klik in Code op + naast de bestanden. Het nieuwe bestand wordt naast de snippet gecompileerd, in dezelfde namespace, dus je hoeft niets te importeren om erbij te kunnen. Een naam zonder extensie wordt als C# gezien.

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);

Een pagina serveren

Er zijn twee manieren om een pagina te serveren, en nog een voor wat mensen ernaast uploaden.

Eén pagina, inline geschreven

Prima voor iets kleins. De pagina zit in de snippet zelf.

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);

Een map met echte bestanden

De juiste keuze voor alles met een stylesheet en een script. Je voegt de bestanden toe zoals een C#-bestand, en ze worden precies zo geserveerd als je ze schreef. Er wordt niets gecompileerd.

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

Geüploade bestanden, uit de data

Voor wat mensen uploaden of wat de lambda aanmaakt, zoals foto's en documenten, geserveerd naast de app. Niet voor de pagina's van de app zelf: die horen in een map met bestanden, zodat ze in dezelfde versie zitten als de code die ze nodig heeft.

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

Een frontend, stap voor stap

De tweede manier, helemaal uitgewerkt. Elke demo serveert zijn pagina zo, vanuit een map die web heet. Open demo-crud om er een te bekijken. Demo's zijn alleen-lezen; hun editorsleutel is hun naam.

  1. 1
    Klik in Code op + naast de bestanden en typ site/index.html. Een slash in de naam zet het bestand in een map; de extensie bepaalt wat voor bestand het is.
  2. 2
    Voeg site/app.css en site/app.js op dezelfde manier toe. Je pagina verwijst ernaar met alleen de naam, zoals href="app.css". De map is namelijk de root van wat er geserveerd wordt, geen deel van het adres.
  3. 3
    Voor alles wat geen tekst is, zoals een afbeelding of een font, open je een bestand in site en klik je op de uploadknop naast de bestanden: het komt in dezelfde map terecht. Een PNG kun je niet in een teksteditor typen, dus zo krijg je hem erin.
  4. 4
    Serveer de map in lambda.cs:
    return Layout.Create().Add(Assets.App("site"));
  5. 5
    Klik op Deployen. site/index.html antwoordt op /, site/app.css op /app.css, en elk adres dat bij geen enkel bestand past, krijgt de pagina als antwoord. Zo blijft een frontend met eigen routing werken als iemand een deeplink herlaadt.
  6. 6
    Zet er een API naast, dan heeft de pagina iets om mee te praten:
    var api = Inline.Create().Get("notes", () => notes);
    
    return Layout.Create()
                 .Add("api", api)
                 .Add(Assets.App("site"));

De twee plekken voor bestanden

Een lambda bewaart bestanden op twee plekken, en de editor toont ze apart: Bestanden bevat de bestanden van een versie (het programma), en Data bevat de workspace (wat het programma bewaart). Het verschil zit in van wie ze zijn. De bestanden van een versie horen bij die versie; de data hoort bij de lambda, en elke versie deelt die.

In een versieIn de data
wat erin staatde code en assets: het programma, frontend inbegrepen – en de documentatie en tests ervanalles wat de lambda wegschrijft of iemand uploadt
wanneer het verandertnooit: een wijziging is een nieuwe versiezodra er iets naar wordt geschreven
een deployzet precies deze bestanden onlineraakt het nooit aan
terugzettenhaalt de oude bestanden teruggeen effect: elke versie deelt het
een conceptbegint als kopie ervanwerkt met een kopie ervan
wanneer het verdwijntmet oude versies, boven de limietmet de lambda, of als je het uitzet
in code te bereiken alsAssetsWorkspace

Het kan niet één en dezelfde plek zijn. Dan zou een deploy alles wissen wat je lambda sindsdien had weggeschreven, of zou er nooit iets weg kunnen uit wat hij meelevert. Een spel met een ranglijst wil het tweede; de pagina die het serveert wil het eerste. Dus de pagina gaat in de versie, en de ranglijst in de data.

Records bewaren

Records – berichten, accounts, bestellingen, stemmen – horen in de database: een eigen SQLite-database van de lambda, die je aanzet onder Data. De code opent een verbinding met Database.GetConnection() en leest en schrijft erin via Entity Framework Core, met een eigen context die de tabellen mapt:

// 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");
}

De tabellen worden gemaakt door migraties: SQL-bestanden die met de versie meekomen in migrations/, en die Evolve op volgorde toepast als de lambda start – elk één keer, dus een nieuwe versie voert alleen uit wat nieuw is. Verander nooit een migratie die al is toegepast; een wijziging aan een tabel is het volgende bestand.

Zoals alle data wordt de database gedeeld door elke versie, laten deploys en terugzetten hem met rust, en werkt een concept op een kopie. Onder Data zie je de tabellen en wat erin staat – de eenvoudige weergave noemt ze items. Downloaden als .NET-project neemt hem mee als gewoon SQLite-bestand.

Maak een context waar je hem nodig hebt en ruim hem daarna weer op, en gebruik hem synchroon: ToList en SaveChanges, niet ToListAsync en SaveChangesAsync. De tabellen worden gemaakt door de migraties, nooit door Entity Framework. De demo demo-crud doet het allemaal voor.

Bestanden bewaren

Workspace is een privémap waarin je lambda mag lezen en schrijven: de plek voor bestanden – foto's die iemand uploadt, een document dat hij maakt, een model dat hij laadt. Records horen in de database, en wat je over een bestand weet – wie het uploadde, en wanneer – is ook een record.

// 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());
             }));

Er zijn ook ReadBytes, WriteBytes, Delete, List, CreateFolder, en Tree/Files/App om hem te serveren. Verder is niets op het bestandssysteem bereikbaar.

Sleutels en wachtwoorden

Een API-sleutel, een wachtwoord of een token hoort in de secrets, niet in de code – waar elke versie, elke download en iedereen die de geschiedenis leest hem zou hebben. De code leest een secret op naam:

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"));

Zet secrets aan onder Data en stel de waarde daar in. Eenmaal opgeslagen wordt hij nooit meer getoond – niet aan jou en niet aan een agent; je kunt hem alleen vervangen. De lijst laat zien welke namen de code leest waarvoor nog geen waarde is ingesteld, en het overzicht vraagt erom. Secret.Exists zegt of er een is ingesteld, voor code die ook zonder kan. Zoals alle data delen alle versies de secrets, en een concept werkt op een kopie.

Ze worden versleuteld opgeslagen, met een sleutel die niet in de database staat. In een gedownload project leest Secret.Read("NAME") de omgevingsvariabele NAME – de waarden zelf blijven hier.

Websockets

Ondersteund, en niet als bijzaak. De demo demo-game koppelt spelers aan elkaar en laat elk spel op de server draaien. De simpelste vorm is drie 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);

Als de pagina alleen luistert - een teller, een feed, een scorebord - zijn server-sent events eenvoudiger: één lang antwoord waar de server steeds in schrijft en waarmee de browser zelf opnieuw verbindt. De demo-live-demo stuurt zo elke stem naar iedereen die meekijkt. In beide gevallen pusht de server wat er veranderd is. Een pagina die om de paar seconden opnieuw vraagt, stuurt elke keer een request, of er nu iets veranderd is of niet, en loopt toch achter.

Waar iedereen in trapt: een browser kan geen headers meesturen bij een websocket-handshake. Geef wat de handler nodig heeft mee in de query, waar hij het leest uit connection.Request.Header.Query, of stuur geheimen als eerste bericht.

Wat niet mag

Je code draait op een gedeelde server, dus een deel van C# wordt al vóór het compileren geweigerd: processen starten, eigen sockets openen, assemblies laden, het bestandssysteem buiten je workspace benaderen, en reflection om daar omheen te komen. Net als wachten op een task met .Result of .Wait() in plaats van await: requests draaien op één thread per core, en de task zou moeten afronden op precies de thread die erop wacht.

Al het andere is er, inclusief de hele module-API van GenHTTP. Wordt er iets geweigerd, dan hoor je welke regel en waarom, niet alleen dat het misging.

Alles meenemen

Met Downloaden als .NET-project in de editor krijg je alles mee: een solution die je kunt openen, kunt draaien met dotnet run en mag houden. Hij heeft alleen het GenHTTP-package nodig, en er zit een Dockerfile bij om hem als container te bouwen en te draaien.

Je snippet wordt Project.cs, en Program.cs serveert wat hij teruggeeft. Je andere bestanden komen precies mee zoals je ze schreef. Workspace en Assets worden twee mappen naast het programma, met dezelfde methodes, apart in een map Platform - dus er hoeft niets in je code te veranderen. Secret leest daar omgevingsvariabelen met dezelfde naam; de waarden blijven hier. De documentatie en de tests komen mee in docs en tests. Database opent database/database.db, dat de download meelevert met de records die je app bewaarde.

Goed om te weten voordat je hier iets bouwt: wat je schrijft is van jou, en je neemt het in zijn geheel mee. Dat je code hier draait, betekent niet dat hij hier vastzit.

De code publiceren

Kan wat je gebouwd hebt iemand anders helpen, publiceer dan de code: open Open source in het dashboard, kies een licentie – MIT, tenzij je een andere wilt – en zet het aan. De code krijgt een eigen pagina tussen de open-source-apps, waar iedereen hem kan lezen, een ster kan geven en elke versie kan downloaden als hetzelfde project dat Downloaden als .NET-project je geeft, met de licentie erbij.

Elke versie wordt gepubliceerd, ook de eerdere, met de documentatie, de tests en de wijziging die elke versie maakte. Wat de app bewaart, wordt nooit gepubliceerd – de records, de bestanden die hij opsloeg, de waarden van zijn sleutels en wachtwoorden – en ook niet wat je in je eigen woorden vroeg, of wie de app gebruikt. Zet je het uit, dan is de pagina weg; de sterren blijven bewaard voor als je de code opnieuw publiceert.

Alles in de code wordt openbaar, de eerdere versies inbegrepen. Een sleutel of wachtwoord hoort bij de sleutels en wachtwoorden onder Data, nooit in de code – gepubliceerd of niet.

Het aan een agent overlaten

Er is een MCP-endpoint op /mcp. Koppel er een agent aan en hij kan alles wat de editor kan: de handleiding lezen, een demo helemaal lezen, bestanden schrijven, ze compileren en deployen. Eronder zit dezelfde API.

Onderweg legt hij uit waarom: write_code krijgt de specificatie en de wijziging mee. En hij kan bekijken wat hij heeft gedeployd: read_logs geeft de recente requests van de lambda, de output en de stacktrace van elke exception. Zo controleert een agent of zijn code werkt, in plaats van het aan te nemen. Jij ziet hetzelfde in het dashboard. Hij schrijft de documentatie en de tests terwijl hij werkt, leest ze voordat hij iets verandert, en voert de tests uit op het adres van een concept voordat hij het concept online zet. Een pagina die gevonden moet worden, krijgt een titel, een beschrijving, een icoon en een preview voor als iemand de link deelt. Onderaan de pagina’s die hij bouwt, zet hij een klein regeltje dat ze met GenHTTP Lambda zijn gemaakt - zeg het hem als je dat liever niet wilt, dan haalt hij het weg.

Meer daarover →