How this works
You write a snippet of C#. Whatever it returns is hosted at a public address, over HTTPS, in a few seconds. This is the whole of it, in the order you will meet it.
What a lambda is
A lambda is a snippet that returns a GenHTTP handler. The platform compiles it, loads it, and mounts whatever it returned under your own address. There is no project, no build file and no using statement. Every GenHTTP module is already imported for you.
return Content.From(Resource.FromString("hello"));
That is a complete lambda. Deployed at /lambda/your-key/, it answers every request with the word hello.
The snippet is statements, not a class. The last thing it does is return something that can serve requests: a handler, or a builder for one.
Your first one
- 1Press Create my lambda. You get a public address and an editor key. The key is the only way back in, so keep it. Nobody can recover it for you.
- 2You land in its control center, with a small REST service already written as the first version. It is only a starting point.
- 3Hand the editor key to an agent and tell it what to build - it writes new versions through MCP. Or open Code and write it yourself: Check compiles without storing anything and tells you what the compiler thinks, file and line.
- 4Press Deploy. Now it is online. Nothing is reachable before that, and deploying again extends how long it stays.
The control center
The editor link opens a control center rather than a text box: most of the code here is written by agents, so the first thing on the screen is how your lambda is doing. The sidebar holds the lambda - whether it is online, its address, and a button when a newer version is waiting to go online - and its sections. Anything done rarely, like changing the address or deleting it, is behind the ⋯ menu there.
- Overview
- What the app is, whether it is online, how many requests it had today and how many failed, the latest change, and how much room is left.
- Documentation
- What the app is, who it is for and why, and why it is built the way it is - written by agents, kept with each version.
- Change
- Say what should be different, and the agent on this server does it while you watch. It tries the change on a draft - a copy with an address of its own - and puts it online once it works. Switch off Put it online when it is done to try the draft yourself first. It only works on your app: a request that is not about it, or that is meant to do harm, is turned down, and it says why.
- Drafts
- Changes being tried before they go online, each at an address of its own and on test data of its own. Opened, a draft has its own code, test data and logs. The section is there once there is a draft.
- Files
- The files of a version: its code and assets, the program itself. A lock or a globe says whether the public can reach them.
- Data
- What the lambda keeps while it runs, shared by every version: the database, the workspace and the secrets, each a pill of its own. Look into the tables and files, upload files, set secrets, or switch a kind on or off. The simple view shows it once the app keeps something.
- Versions
- What each version changed and what was asked for, and the difference to the one before. Deploy or roll back from here, or start a draft from any of them.
- Deployments
- What was online when, and what took it down.
- Stats
- Requests, failures, response times and the most asked-for paths, over the last hour or day.
- Logs
- Its requests, what it printed, and the stack trace of anything that went wrong, as it happens.
- Code
- Writing it by hand. Check compiles, Save makes a version, Deploy puts it online. In a draft, Save keeps it in the draft and shows it at the draft's address.
Ctrl-Ssaves;F12goes to a declaration. - Tests
- How the app is tested automatically, with the scripts and test data for it. In the full view only.
Every section works the same way: its title, an ⓘ that explains it, its actions on the right, and - where it has more than one view - a row of pills underneath. The pills of the code are its files. The full view gathers the sections in groups: how people find it, where a change is made, the program and its data, and how it runs.
The traffic and the log are held in memory, for watching rather than keeping: a restart of the server begins them again. Versions and the deployment history are stored.
Saying why
A version is the code, and optionally two notes about it: the specification, what the user wants and why, in their words where possible, and the change, one line on what the version does. They are shown beside the diff in the version history, so the why survives next to the what - for you, and for the next agent that reads the history before changing anything.
POST /api/v1/lambdas/{editorKey}/versions { "files": [ { "name": "lambda.cs", "code": "..." } ], "specification": "A guest book people can sign; entries must survive a restart", "change": "Keeps entries in the database so they survive a restart" }
Agents pass the same two fields to write_code. In Code, saving asks for the change. Both are optional; a long specification is cut at 4000 characters and a change at 500 rather than refused. A draft keeps its own two, and the version it becomes takes them over.
Documentation and tests
Every version keeps what is written about it beside its program: its documentation - what the app is, who it is for and why, and why it is built the way it is - and its tests: how to check automatically that it works, with the scripts and test data for it. Agents write them with a new lambda and keep them up to date with every change. The next agent to change the lambda reads them first, so it knows what the app is for and what has to keep working - which the code alone does not say.
- .lambda/docs/product.md
- what the app is, who it is for, what people do with it and why
- .lambda/docs/decisions.md
- the technical decisions, and why they were made
- .lambda/tests/README.md
- how the app is tested automatically, and how to run the tests
- .lambda/tests/…
- the scripts and test data the tests use
They are files of the version like any other, in the folder .lambda: the history shows what a version changed in them, rolling back brings back the documentation that was true of that version, and a draft has a copy of its own that goes online with it. They are never compiled and never served, and count towards what the assets of a version may come to.
In the control center, Documentation shows the pages to read, and Tests how the app is tested and the files beside it; the version is picked as it is for its files. A page can be edited there too, which saves the next version. The simple view calls the documentation About and shows only what the app is for - to correct it, tell the agent.
They are written in the language you use with the agent, for whoever changes the app next - a person or an agent. Not a copy of the code: what it is for, and why.
Changing it safely
A version never changes once it is saved - which is what makes every one worth keeping: any of them can be compared with, and put back online exactly as it was. To change a lambda that people use, try the change in a draft first.
- 1Start it from any version under Versions, or let the agent start one. It is a copy of that version's code, assets, documentation and tests, and of the lambda's data.
- 2Change it as often as it takes - in Code, or by asking the agent. Its preview answers at an address of its own,
/features/…/, against test data of its own. Visitors of the lambda see none of it, and nothing it writes reaches the lambda's data. - 3Put online once it is right: it becomes the next version, with its notes, and goes online. The draft goes with it - its preview and its test data.
POST /api/v1/lambdas/{editorKey}/features { "name": "Leaderboard" } PUT /api/v1/lambdas/{editorKey}/features/{feature}/files?deploy=true POST /api/v1/lambdas/{editorKey}/features/{feature}/merge { "deploy": true }
Several drafts can be worked on at once. Only one that is up to date with the newest version can go online, so that it never undoes a version saved after the draft began. When another went online first, bring its changes in - or ask the agent to - and mark the draft as up to date. Nothing goes online on its own; that is deliberate. The API calls a draft a feature, and putting it online a merge.
More than one file
Types do not have to sit underneath the code that uses them. In Code, press + beside the files and it is compiled beside the snippet, in the same namespace, so nothing has to be imported to be reached. A name with no extension is taken to be C#.
var shelf = new Shelf(); return Inline.Create() .Get(() => shelf.All()) .Post((Book book) => shelf.Add(book));
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);
Serving a page
There are two ways to serve a page, and one more for what people upload beside it.
One page, written inline
Fine for something small. The page is part of the snippet.
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);
A folder of real files
What you want for anything with a stylesheet and a script. The files are added the same way a C# file is, and served exactly as written. Nothing compiles them.
return Layout.Create() .Add("api", api) .Add(Assets.App("site"));
Uploaded files, from the data
For what people upload or the lambda creates - pictures, documents - served beside the app. Not for the pages of the app itself: those belong in a folder of files, where they are versioned with the code that needs them.
return Layout.Create() .Add("api", api) .Add("uploads", Workspace.Files("uploads")) .Add(Assets.App("site"));
A front end, step by step
The second of those, in full. Every demo serves its page this way from a folder called web - open demo-crud to read one. Demos are read only; their editor key is their name.
- 1In Code, press + beside the files and type
site/index.html. A name with a slash in it puts the file in a folder; a name with an extension is taken as the file it says it is. - 2Add
site/app.cssandsite/app.jsthe same way. Your page refers to them by name, as inhref="app.css", because the folder is the root of what gets served rather than part of the address. - 3For anything that is not text, like an image or a font, open a file in
siteand press the upload button beside the files: it lands in the same folder. A PNG cannot be typed into a text editor, so that is the way in. - 4In
lambda.cs, serve the folder:return Layout.Create().Add(Assets.App("site"));
- 5Press Deploy.
site/index.htmlanswers at/,site/app.cssat/app.css, and any address matching no file is answered with the page, so a front end that does its own routing still works when somebody reloads on a deep link. - 6Add an API beside it and the page has something to talk to:
var api = Inline.Create().Get("notes", () => notes); return Layout.Create() .Add("api", api) .Add(Assets.App("site"));
The two places files live
A lambda keeps files in two places, and the editor shows them apart: Files holds the files of a version - the program - and Data holds the workspace - what the program keeps. The difference is whose they are. The files of a version belong to that version; the data belongs to the lambda, and every version shares it.
| In a version | In the data | |
|---|---|---|
| what it holds | the code and assets: the program, front end included - and its documentation and tests | whatever the lambda writes, or somebody uploads |
| when it changes | never - a change is a new version | the moment something is written to it |
| a deploy | puts exactly these files online | never touches it |
| rolling back | brings the old files back | no effect: every version shares it |
| a draft | starts as a copy of them | works on a copy of it |
| when it goes | with old versions, past the limit | with the lambda, or when you switch it off |
| reached from code as | Assets | Workspace |
They cannot be one place. If they were, a deploy would either wipe everything your lambda had written since, or nothing could ever be removed from what it ships. A game that keeps a leaderboard wants the second; the page it serves wants the first. So the page goes in the version, and the leaderboard in the data.
Keeping records
Records - entries, accounts, orders, votes - belong in the database: a SQLite database of the lambda's own, switched on under Data. The code opens a connection with Database.GetConnection() and reads and writes it through Entity Framework Core, with a context of its own that maps the tables:
// 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"); }
Its tables are made by migrations: SQL files shipped with the version in migrations/, applied in order by Evolve as the lambda starts - each once, so a new version only ever runs what is new. Never change a migration that was applied; a change to a table is the next file.
Like all data, the database is shared by every version, left alone by deploys and rollbacks, and a draft works on a copy of it. Under Data you see its tables and what is in them - the simple view calls them records. Download carries it along as an ordinary SQLite file.
Make a context where you need it and dispose of it, and use it synchronously - ToList and SaveChanges, not ToListAsync and SaveChangesAsync. The tables are the migrations' to make, never Entity Framework's. The demo-crud demo does all of it.
Keeping files
Workspace is a private directory your lambda may read and write: the place for files - pictures somebody uploads, a document it makes, a model it loads. Records belong in the database, and what is known about a file - who uploaded it, when - is a record too.
// 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()); }));
There is also ReadBytes, WriteBytes, Delete, List, CreateFolder, and Tree/Files/App for serving it. Nothing else on the file system is reachable.
Keys and passwords
An API key, a password or a token belongs in the secrets, not in the code - where every version, every download and everybody reading the history would have it. The code reads one by name:
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"));
Switch secrets on under Data and set the value there. Once saved, it is never shown again - not to you, not to an agent; you can only replace it. The list says which names the code reads that have no value yet, and the overview asks for them. Secret.Exists says whether one is set, for code that works without it. Like all data, secrets are shared by every version, and a draft works on a copy of them.
They are stored encrypted, with a key that is not in the database. In a downloaded project, Secret.Read("NAME") reads the environment variable NAME - the values themselves stay here.
Websockets
Supported, and not an afterthought. The demo-game demo pairs players and runs every game on the server. The simplest form is three 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);
When the page only listens - a count, a feed, a scoreboard - server-sent events are simpler: one long response the server keeps writing to, which the browser reconnects by itself. The demo-live demo sends every vote to everybody watching that way. Either way the server pushes what changed. A page that asks again every few seconds sends a request each time, whether anything changed or not, and is still late.
One thing catches everybody: a browser cannot set headers on a websocket handshake. Pass what the handler needs in the query, where it reads it from connection.Request.Header.Query, or send secrets as the first message.
What it will not let you do
Your code runs on a shared server, so some of C# is refused before it compiles: starting processes, opening sockets of your own, loading assemblies, reaching the file system outside your workspace, and reflection used to get around any of that. So is waiting for a task with .Result or .Wait() instead of awaiting it: requests run on one thread per core, and the task would have to finish on the very thread that is waiting for it.
Everything else is there, including the whole of the GenHTTP module API. If something is refused you are told which line and why, not simply that it failed.
Taking it away
Download in the editor gives you the whole thing as a .NET project: a solution you can open, dotnet run, and keep. It needs nothing but the GenHTTP package, and comes with a Dockerfile to build and run it as a container.
Your snippet becomes Project.cs, and Program.cs serves what it returns. Your other files come across exactly as you wrote them. Workspace and Assets become two folders beside the program, with the same methods, kept apart in a Platform folder - so nothing in your code has to change. Secret reads environment variables of the same name there; the values stay here. The documentation and the tests come along in docs and tests. Database opens database/database.db, which the download carries with the records your app kept.
Worth knowing before you build anything here: what you write is yours and it leaves whole. Nothing about running it on this machine locks it to this machine.
Publishing the code
If what you built could help somebody else, publish its code: open Open source in the control center, pick a license - MIT, unless you want another - and switch it on. Its code gets a page of its own among the open source apps, where anybody can read it, star it, and download any version as the same project Download gives you, with the license beside it.
Every version is published, the earlier ones too, with its documentation, its tests and the change each one made. What the app keeps is never published - its records, the files it saved, the values of its keys and passwords - and neither is what you asked for in your own words, or who uses the app. Switch it off and the page is gone; its stars are kept for when you publish it again.
Everything in the code becomes public, the earlier versions included. A key or a password belongs with the keys and passwords under Data, never in the code - published or not.
Letting an agent do it
There is an MCP endpoint at /mcp. Point an agent at it and it can do everything the editor does: read the guide, read a demo in full, write files, compile them, and deploy. It is the same API underneath.
It says why as it goes - write_code takes the specification and the change - and it can look at what it deployed: read_logs answers with the lambda's recent requests, what it printed and the stack trace of anything it threw, which is how an agent finds out its code works rather than assuming it. You watch the same thing in the control center. It writes the documentation and the tests as it goes, reads them before it changes anything, and runs the tests against a draft's address before it puts the draft online. A page meant to be found gets a title, a description and an icon, and a preview for when its link is shared. At the foot of the pages it builds, it adds a small line saying they were made with GenHTTP Lambda - tell it if you would rather not have it, and it takes it out.