Write, deploy and host small C# web services and sites at a public address.
One address for any MCP client. No sign-in needed on this server.
5 read5 act3 other
Grouped by tool name. At most: moves money, runs code or commands, deletes data, or sends messages for you.
Odel charges nothing per call. The publisher may charge for its own service.
Write a C# snippet that returns a GenHTTP IHandler, press deploy, and get a
public URL that serves it. The snippet is compiled with Roslyn at runtime and
the resulting handler is hosted by the same [GenHTTP][genhttp] server that
serves this application.
return Inline.Create()
.Get(() => "Hello from my lambda!");
docker compose up --build
The application is then available at http://localhost:8080/. Its data - the
SQLite database, the stored code, and the workspaces and databases of the
lambdas - lives in a named volume mounted at /data.
Two processes: the .NET server, and Vite serving the frontend with hot reload.
# the server, on http://localhost:8080/
dotnet run --project src/GenHTTP.Lambda
# the frontend, on http://localhost:5173/ (proxies /api, /lambda and /features to the server)
cd src/Frontend && npm install && npm run dev
Work against http://localhost:5173/ while developing. The server on its own
answers with a placeholder page until a frontend has been built into its web
root, which is what npm run build does:
cd src/Frontend && npm run build # writes src/GenHTTP.Lambda/wwwroot
English is the source of every page; the other languages are translated by script and agent, so that only what changed is read and written:
cd src/Frontend
npm run translations -- extract # a request per language in .translations/
# one `translator` agent per request (.claude/agents/translator.md, on Sonnet)
npm run translations -- apply # the answers into the catalogs, checked
npm run translations -- check # every language against English
CLAUDE.md ("How a change is translated") has the rules.
Build and test everything the way CI does:
dotnet build GenHTTP.Lambda.slnx -c Release
dotnet test GenHTTP.Lambda.slnx -c Release
The tests compile and execute real lambdas against a real server on a random port, each against its own temporary data directory.
| Route | Serves |
|---|---|
/ | the landing page |
/build | the text box for somebody who wants an app built, no code |
/ship | for developers: connect an agent and publish what it built |
/docs | the guide for owners |
/showcase | the lambdas their owners chose to show |
/source | the lambdas whose owners published their code |
/source/:publicKey | the published code of one: its files, documentation, tests and changes, per version |
/source/:publicKey.git | the published code as a git repository, read only |
/enterprise, /terms, /privacy, /imprint | the company pages |
/editor/create | the creation assistant |
/editor/:privateKey | the editor for one lambda |
/editor/:privateKey/:name.git | the lambda as a git repository, to clone and push to (any name; the editor offers the public key) |
/lambda/:publicKey | the deployed handler |
/features/:feature | the preview of a feature, while it is online |
| any path, at a lambda's own domain | the deployed handler of a premium lambda with that domain |
/api/v1/ | everything the editor calls, see below |
/mcp | the same, for agents |
/admin | every lambda on the server, for whoever runs it |
Browsable at /api/v1/scalar/. A lambda is addressed by its editor key, which
is also what authorizes the call. Things that can be listed, read, created or
changed are resources; what is a process rather than a thing is a verb in the
path.
| Endpoint | Does |
|---|---|
POST /lambdas | creates a lambda (optionally with view: Full or Simple) |
GET / PATCH / DELETE /lambdas/:privateKey | reads, changes (its key, its view), removes it |
GET /lambdas/:privateKey/export | the newest version as a runnable .NET 10 project with a Dockerfile (zip), with its database, see below |
GET / POST /lambdas/:privateKey/versions | lists versions, saves a new one (optionally with specification and change) |
GET /lambdas/:privateKey/versions/:version | reads one version (?folder=.lambda/docs/ for its documentation alone, .lambda/build/ for its build folder) |
GET /lambdas/:privateKey/versions/:version/zip | one version's files as a zip (?layout=project laid out as a clone) |
POST /lambdas/:privateKey/versions/zip | saves a zip of all files as a new version (?layout=project for one laid out as a clone) |
POST /lambdas/:privateKey/versions/changes | changes some files of the newest version, as a new one |
GET /lambdas/:privateKey/deployment | what is online, and until when |
POST /lambdas/:privateKey/deployment/start / stop | puts a version online, takes it off |
GET /lambdas/:privateKey/deployment/history | every stretch it was online, and what ended it |
GET /lambdas/:privateKey/summary | the dashboard: state, traffic, problems, storage, what the documentation says |
GET /lambdas/:privateKey/traffic | requests by minute and quarter hour, statuses, paths |
GET /lambdas/:privateKey/logs | its own log, followed with ?since=; no visitor addresses |
GET /lambdas/:privateKey/agent | the agent's change of it - under way, or the last one - and how many are left today |
POST /lambdas/:privateKey/agent/start / stop | asks the agent for a change, stops it |
GET /lambdas/:privateKey/data | the kinds of data it keeps, and how full each is |
GET / PUT / DELETE /lambdas/:privateKey/data/:kind | one kind (database, workspace, secrets); switches it on; switches it off and deletes what it held |
GET /lambdas/:privateKey/database | its database: tables and views, their columns and rows, how full it is, whether the code connects |
GET /lambdas/:privateKey/database/tables/:table | a page of one table's rows, newest first (offset, limit, order, descending) |
GET /lambdas/:privateKey/secrets | the secrets by name, which names the code reads, and which of those have no value |
PUT / DELETE /lambdas/:privateKey/secrets/:name | stores a value ({ "value": … }), never read back; removes one |
GET /lambdas/:privateKey/files | lists the workspace |
GET / PUT / DELETE /lambdas/:privateKey/files/:path | one file, its path encoded (a%2Fb.txt), as base64 up to 32 MB |
GET / PUT /lambdas/:privateKey/files/:path/content | one file as it is, streamed, however large |
PUT /lambdas/:privateKey/folders/:path | makes a folder |
GET / POST /lambdas/:privateKey/features | lists the features, starts one (name, specification, base) |
GET / PATCH / DELETE /lambdas/:privateKey/features/:feature | one feature with its files (?folder= as for a version); changes its name, notes or base; deletes it |
PUT /lambdas/:privateKey/features/:feature/files | replaces its files (?deploy=true puts its preview online) |
POST /lambdas/:privateKey/features/:feature/changes | changes some of its files |
GET / PUT /lambdas/:privateKey/features/:feature/zip | its files as a zip; a zip put back into it (both with ?layout=project) |
POST /lambdas/:privateKey/features/:feature/preview/start / stop | puts its preview online, takes it off |
POST /lambdas/:privateKey/features/:feature/merge | makes it the next version and deletes it (deploy to put that online) |
GET /lambdas/:privateKey/features/:feature/logs | what its preview has been doing |
GET /lambdas/:privateKey/features/:feature/data, POST …/data/refresh | its copy of the data; a fresh copy of the lambda's |
…/features/:feature/workspace[/:path[/content]], …/folders/:path | its copy of the workspace, as files and folders are the lambda's |
…/features/:feature/secrets[/:name] | its copy of the secrets, as secrets are the lambda's |
…/features/:feature/database[/tables/:table] | its copy of the database, as database is the lambda's |
POST /lambdas/:privateKey/code/check | compiles without saving |
POST /lambdas/:privateKey/code/semantics, completions, definition | what the editor asks the compiler |
GET /keys/:publicKey | whether a key is free, and if not, online |
GET /demos | the demos, and the keys to read them with |
GET / PUT / DELETE /lambdas/:privateKey/showcase | its entry on the showcase: read, list it (title, description, picture), take it off |
GET /lambdas/:privateKey/domain, PUT / DELETE | the domain of a premium lambda, whether it is served, and what it resolves to |
GET /showcases, GET /showcases/:publicKey/image | what /showcase lists, and a picture |
GET / PUT / DELETE /lambdas/:privateKey/source | whether its code is published and under which license, the licenses on offer; publishes it or changes the license (license, author); takes it down |
GET /sources | what /source lists (search, order: stars, updated, published, skip, take) |
GET /sources/:publicKey | a published source: what it is, its license, where it runs, every version and what it changed, a ticket to star it with |
GET /sources/:publicKey/versions/:version | the files of a version as its project has them, packed on the first request |
GET /sources/:publicKey/versions/:version/files/:path, …/raw/:path | one file, its path encoded: its text; as it is (?download=1 to save it) |
GET /sources/:publicKey/versions/:version/zip | a version as a project to download |
POST /sources/:publicKey/star | stars it, or takes the star back (ticket, starred) |
POST /builds, GET /builds/:id | the text box on /build |
GET /system | terms, limits, starters, build agent |
GET /telemetry, /logs, /admin/... | for whoever runs the installation |
Creating a lambda asks what somebody would like to build and offers the demos
in those words - "Keep track of things", "Let people sign up" - next to an empty
lambda. Picking a demo starts the new lambda as a copy of it, which is theirs to
change. The files are in Resources/Templates and listed in TemplateCatalog.
A lambda has two keys. The public one is part of its URL and may be changed; the private one is the editor link and is shown only to whoever created the lambda - anyone holding it can edit, deploy and delete.
A lambda may also return a websocket rather than a document, in any of the
three flavours the module offers - Websocket.Functional(), .Reactive() and
.Imperative(). The handshake is an ordinary request and is bounded by the
execution timeout like any other; everything after it belongs to the
connection, which is why a socket may stay open far longer than a lambda is
given to answer. FrameType, which the imperative flavour reads off every
frame, is one of the names a lambda is given, so no import is needed.
The editor says when both free tier timers run out: a hint under the public URL for the deployment, and a chip beside the buttons for the lambda itself, which opens an explanation of how each one is extended.
A lambda keeps three kinds of things, and they live differently.
A version is the program: every C# file and every asset, the front end
included - and, beside it in .lambda/, its documentation and its tests, and
where a build tool makes its assets or code, what they are made from: its
build folder (see below). 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. Saving files (POST …/versions, write_code) makes a new
one.
A feature is a change worked on beside the lambda. It branches off a
version - the newest, unless another is named - with a copy of its files and a
copy of the lambda's data, and is changed in place as often as it takes; it has
no versions of its own. Its preview is built from its files against its copy of
the data and answers at /features/{feature}/, a random key nobody guesses,
told to search engines with X-Robots-Tag: noindex and a Disallow in
robots.txt. It serves what was deployed until it is deployed again, a restart
included, is counted neither in the lambda's traffic nor in its log, and on the
free tier goes offline like a deployment does when nobody works on it. Once it
does what was asked it is merged: its files become the next version, with
its notes, and the feature goes - preview, copy of the data and all; the
lambda's own data is not touched. Only a feature based on the newest version
can be merged, so that a merge never undoes a version saved after the feature
began - by the owner, an agent, or the merge of another feature. There is no
merging or rebasing machinery: whoever works on the feature brings a newer
version's changes in and then says so by moving its base (PATCH, or
update_feature), and the merge takes the lambda's lock and checks the base
once more, so two merges cannot both win. A lambda may have ten features open
(a limit of its tier, set in the panel); a demo has none. Each holds a full copy of the
workspace and of the database, taken on a thread of its own and swapped in
whole, so ten features of a premium lambda with a full workspace take ten times
its room on disk.
Data is what the program keeps: the database, a SQLite database for its
records; the workspace, a private directory of files; and the secrets. It
belongs to the lambda rather than to a version - every version reads and
writes the same data, and deploying, rolling back or merging never touches it.
It goes with the lambda, or when that kind of data is switched off, which
deletes what it held - the copies features work on included. Each kind is
switched on before the lambda can use it (PUT …/data/:kind, enable_data):
the workspace is on unless it was switched off, and a lambda whose workspace
is off is compiled with one that refuses every call and says why; the database
and the secrets are off until somebody switches them on. An agent may switch a kind on when what it builds
needs one; switching off deletes, so only the owner does that. The editor has
a Data section of its own, beside Files, which holds the files of a
version. Every kind is one view of it, picked from a row of pills under its
title, and each is shown the same way - what it is, whether it is on, how full
it is - with only what it holds drawn its own way.
Deploying picks a version (the latest by default), builds it and makes it live, and a lambda has at most one deployment at a time. A deployment that does not compile leaves the one before it standing - its assets included, which are written back after the failed attempt replaced them.
The editor is written for people who had an app built rather than for developers, so it calls a feature a draft, its copy of the data test data, and merging it putting it online - which merges it and deploys the version it becomes in one step. Nothing in it says merge, base or branch; a feature behind the newest version is out of date, and the agent is offered to bring it up to date. The drafts have a section of their own, shown once there is one, and opened on one the editor becomes the draft's: the sidebar holds it, with the buttons that try it and put it online, and its views are its documentation, its code, its tests, its test data and its preview's log. Showcase, domain, figures and deployments stay with the lambda. A version offers to start a draft from it, the save dialog of the code offers to save what was typed as a new draft instead of a version, and a change the agent leaves as a draft can be tried and put online from the Change section.
Every version keeps what is written about it beside its program, so that whoever changes it next - an agent, most of the time - starts from what the app is for and what has to keep working, which the code does not say:
| File | What it holds |
|---|---|
.lambda/docs/product.md | what the app is, who it is for, what people do with it and why - in the terms of whoever asked for it; its first paragraph is the app in a sentence or two |
.lambda/docs/decisions.md | the technical decisions, and why they were made |
.lambda/docs/… | more pages, and pictures the pages show |
.lambda/tests/README.md | how the app is tested automatically: what has to keep working, how each of it is checked, how to run the scripts |
.lambda/tests/… | the scripts and the test data they use |
They are files of the version rather than fields in the database, because
they describe that version of the program: saved, compared in the history,
rolled back, copied into a feature and merged with it like any other file, in
the zip and in the export, with no table and no migration. Rolling back brings
back the documentation that was true of the version. A dot folder, because the
root of a version is the root its assets are served from, where a leading dot
means "not served" - and no asset has ever been allowed a name starting with
one, so no version saved before could have meant anything else by it. The
documentation and the tests are one kind of file, context
(LambdaSource.IsContext), whichever of the two they are: never compiled,
whatever they are called (a test in C# is not code), never served, and left out
of what identifies a build, so a version that only changes them builds to the
same assembly. Their names are held to docs/ and tests/ below .lambda/,
with the rules of an asset but no extension needed; a page is text. They count
towards the allowance of the assets, since every version carries its own copy
of them as it does of its assets.
A zip of a version or a feature holds them, which is how an agent working
locally reads and writes them without further calls - .lambda is a hidden
folder, so a zip is made of the folder's contents (zip -r ../f.zip .), not of
*. The agents are told everywhere they decide something - the instructions,
the tool descriptions, the answers and platform_guide
(documentationAndTests) - to read them before changing a lambda, to write
all three pages with a new lambda, to update what a change affects in the same
save, and to run the tests against a feature's preview before merging it -
all of it in proportion to the lambda: a small one gets a few lines a page and
one quick check, not a test suite. read_lambda hands them over first and apart from the program's files, within
a budget of their own, and every save, deployment and preview that leaves a
page out says which. Every demo has all three and a script its tests run,
checked by a test.
In the editor they are two sections. Documentation is second in the sidebar, after the overview, in both views; Tests is in the full view only. Both are one component over their folder: the pages rendered to be read, each a pill, with the other files behind a Files pill, and the version picked as it is for its files. A page the version changed is marked, and shows the difference on request. On the newest version or in a draft a page can be edited, beside a preview of it; saved, it is the next version (a draft saves in place), put online with it when the newest version was online. The simple view calls the documentation About and shows the product page alone, with a way to have the agent correct it rather than to edit it. The overview of either view opens with the product page's first paragraph, and the full one says which of the three pages the version has.
With fifteen sections and more, the full view's sidebar is in groups: the overview and the documentation on their own, then how people find it and what they get to see of it (showcase, open source, domain) - what the lambda is to everybody else, right under what it is - then where a change is made, under Develop (Change, the drafts, the code, Build where a version keeps one, the tests), the program and what it keeps (files, data, versions), and how it runs (deployments, stats, logs). On a phone the groups are a rule apart in the row of sections.
Some of a lambda may be made by a build tool rather than written as it is
served or compiled: a front end that is compiled or bundled, styles compiled
from another language, code generated from a description. What the tool makes
is part of the program like anything else - assets, or code. What it makes
them from is kept beside them, in the version's build folder:
.lambda/build/, and build/ in a clone, an export and a published source.
Nothing about those files is assumed: they are whatever the tool works from,
in whatever layout it wants.
build/ what it is built from, in whatever shape the tool wants
build/README.md how it is built, and where the build goes
build/.gitignore what the build installs or keeps for itself, left out
assets/ what the build wrote, if it makes assets
*.cs what it wrote, if it makes code
The platform builds nothing. It never runs a build tool or installs what one needs, on a push or anywhere else, and never checks that the program is what the build folder builds to. Whoever changes it - an agent with a shell, in a clone most of the time - changes those files, runs the build where it works, and saves both together, in one version or one feature. A change in the build folder that is not built changes nothing; a change made in what the build wrote is undone by the next build. This keeps the platform what it is - a server that hosts what it is given - and the tools the agent's, whichever they are.
Otherwise it is kept like the documentation and the tests: a file of the
version, saved, compared in the history, rolled back, copied into a feature
and merged with it, in the zip of a version, the clone, the export and a
published source - never compiled, never served, left out of what identifies
a build, and counted towards the allowance of the assets. It is a kind of its
own (LambdaSource.IsBuild) rather than more context, because it is files
for a tool rather than pages: dot files are allowed, and brackets,
parentheses and plus signs some tools read meaning off, with no spaces and no
.git folder, up to sixteen folders deep.
What a build installs, caches or writes for itself - node_modules,
target or .venv, for example - is no part of a version, and every tool
names it differently, so the folder says what it is, in its own .gitignore
files. A clone follows them because git does; a zip put back through the API
follows them too, the way git add would (IgnoredPaths, git's rules:
patterns at any depth or anchored, folders only, !, **, the deeper file
having the last word, nothing coming back from a folder left out) - but
keeps what the lambda already has, as git keeps what it tracks. A zip is
counted as it is sent, before any .gitignore leaves something out, so it
holds what a commit would - never what a build installed, which is no part
of it and would only make the upload large. A save
that names a file keeps it, as git keeps a file added on purpose; one that
runs over the allowance says how large the build folder is, and that its
.gitignore keeps out what a build installed.
One layout to build in. A build writes by paths relative to itself -
from build/web, into ../../assets/web - so it has to find the files laid
out the same way wherever it runs. A clone lays them out as a project; the
zip of a version or a feature names them as the lambda does
(.lambda/build/, the assets at the root), where such a path leads
elsewhere. So the zip can be had in the clone's layout too, ?layout=project
on the download and on the upload: the lambda's files where a clone has
them, without the platform's, and read back as a commit of the clone would
be - the platform's files skipped, what the repository's .gitignore and
the build folder's leave out left out (bin/, obj/, a build's installs),
anything with no place in a lambda refused, Project.cs that was not
touched kept as the snippet it was, to the byte, and the files named as the
lambda named them. Agents that build are steered to a clone or to that
layout; agents with only the tools cannot build, and never need it.
Many read a folder called build/ as output, to ignore or to delete. So a
clone's AGENTS.md says what it is, and the repository's .gitignore takes
it back in (!/build/), which overrides an ignore of build/ in somebody's
global git configuration.
In the editor it is called Build, and read, never edited - the
asymmetric interface: the agent builds it, the owner reviews it. The full
view has a Build section, in the group where a change is made
(Develop), once the newest version (or the one online) keeps one, and
it shows what can be said of any build without knowing the tool: how it is
built, as its README.md says, rendered; its files; and what the version
changed in it against its code and assets, with a warning where files
changed here and nothing of the code or the assets did. A draft has it as a
view of its own. Files shows it as a fourth group beside the code, the
assets and the documentation; the code shows it as one pill, and keeps it as
it is when it saves; the difference between two versions lists the files
kind by kind, the code first and the assets - which may hold what a build
wrote - after what they were built from. The simple view has none of it, nor
does /build: the build agent has no shell, writes its front ends as plain
HTML, CSS and JavaScript, and leaves a build folder it finds alone.
The demos have none: they teach how a lambda is put together, with front ends of plain files a reader follows without a build tool.
Records - entries, accounts, orders, votes - go in the database: a SQLite file of the lambda's own, which its code opens a connection to and reads and writes through [Entity Framework Core][efcore] - or in plain SQL, where that is simpler. Its schema is migrations shipped with the version and applied by [Evolve][evolve] as the lambda starts, never Entity Framework's own:
// 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"], IsEraseDisabled = true }.Migrate();
}
return Inline.Create().Get("count", () =>
{
using var db = new Notes(Database.GetConnection());
return db.Entries.Count();
});
public class Note
{
public long Id { get; set; }
public string Text { get; set; }
}
// maps the table the migration made, on the connection it is handed
public 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");
}
Database.GetConnection() hands out an open SqliteConnection of
Microsoft.Data.Sqlite, taken from a pool, which the caller disposes of - or a
context it is handed, with contextOwnsConnection: true - one per request,
never one shared between requests. The connection comes into the context
from outside, because inside a DbContext, Database is the context's own
(the compiler's message says so, and names LambdaEnvironment.Database).
Microsoft.Data.Sqlite, Microsoft.EntityFrameworkCore and EvolveDb are
imported in every file, like the GenHTTP modules. Agents are told to use it
synchronously - ToList and SaveChanges, never ToListAsync or
SaveChangesAsync - because SQLite answers synchronously whatever the method
is called, and the engine has an asynchronous model of its own that a blocking
call here does not disturb; this is said, not enforced. The demos all keep
their records like this, demo-crud the plainest of them.
PUT …/data/database,
enable_data) makes it: an empty file, in write-ahead logging, so readers
never wait for a writer. Switching it off deletes it, the copies of the
features included. Either way the lambda and its previews are started again
on their next request, so what they do as they start - migrating - is done
against what is there now. A lambda created as a copy of a demo that keeps
one starts with its own, switched on.new SqliteConnection(...) - also as a target-typed
new(...), through an alias or a class derived from it, which is asked of
the compiler rather than read off the text - and the names that would point
a connection elsewhere or load native code into it (ConnectionString,
SetConnectionString, LoadExtension, DbProviderFactories, …). Of Entity
Framework, a lambda reaches the namespace a context is written in and the
three describing a model takes (Metadata.Builders, ChangeTracking,
Storage.ValueConversion), not its Infrastructure, Storage, Internal
or the rest, where a context's services and options are. UseSqlite is
refused with anything but a connection, and so are EnsureCreated,
EnsureDeleted and Entity Framework's Migrate - the schema is Evolve's -
all asked of the compiler. dynamic is refused too: it binds members at
runtime, which is everything the guard reads the code for, skipped. The
connection it is handed carries an authorizer SQLite asks about every
statement: ATTACH and VACUUM INTO, which reach other files, are refused,
as are the pragmas that would point it at other directories or lift its
quota, and SQLite's defensive mode is on. The handle underneath is a type
the code cannot name.max_page_count on
every connection: past it a write fails with database or disk is full. A
limit the operator lowers holds from the next connection; nothing stored is
removed.GET …/database); read_database does
the same for an agent. Nothing but the lambda writes to it.The databases are not encrypted. SQLite has no encryption of its own: it
would take replacing the SQLite of the whole process with a build that has
(SQLite3 Multiple Ciphers was tried, and is maintained by one person for
.NET), and the key would sit in the platform's database on the same volume -
so it would protect a copy of /data/databases on its own and nothing more.
Protect the data volume and its backups instead.
API keys, passwords and tokens are data, never code: code and assets live in every version, every export and every reader of the history, so a key put there can never be taken back. A lambda reads a secret by name:
var stripe = new System.Net.Http.HttpClient();
stripe.DefaultRequestHeaders.Authorization = new("Bearer", Secret.Read("STRIPE_KEY"));
Secret.Read throws, saying how to set it, when there is no such secret;
Secret.Exists answers whether there is one, and false while secrets are
switched off, for code that works without. Secret is there in every file,
like Workspace.
The rules are the usual ones for secrets:
PUT …/secrets/:name) or MCP (set_secret), and nothing gives it out
again - not the editor, not the API, not an agent. Only the lambda reads
it. So a secret is replaced rather than edited, and whoever holds the editor
link can use a key without being able to copy it. Answers repeat the name,
never the value, and the log records the name only.Secret.Read("STRIPE_KEY") from $STRIPE_KEY. Up to 100 per lambda, 32 KB
each - a private key fits, a file belongs in the workspace.Secret.Read("…") or Secret.Exists("…") as a literal - in the version
online and in the newest one, or in a feature's files - are listed beside
what is stored: a name read and not stored is missing (reading it fails),
one only asked about with Exists is optional. The editor offers to set
what is missing, the overview asks for it, and read_lambda and
list_secrets tell an agent. That is how the two sides meet: an agent
writes Secret.Read("WEATHER_API_KEY") and switches secrets on, and the
owner enters the value without ever reading the code - and without the
value passing through the agent.They are stored in the SQLite database, table secrets, sealed with AES-256-GCM.
The key is made with HKDF from two halves: the installation's, which is not in
the database - LAMBDA_SECRETS_KEY (32 random bytes in base64, or a passphrase
of at least 32 characters), or else a random key written on first use to
secrets.key in the data directory, readable by the server's user only - and
the lambda's own random salt (lambdas.secret_salt), made with its first
secret and dropped when its secrets are switched off. The name is bound to the
value as associated data. So the database alone opens nothing, a value copied
under another name or into another lambda does not open either, and a
feature's copy is copied sealed, without being opened.
Backing up and moving follow from that: the database and the installation's
key are all it takes. Copy the data volume as a whole - secrets.key goes
with it - or set LAMBDA_SECRETS_KEY on the new server to the key the
database was written with. Keep a copy of the key apart from database
backups; without it the secrets are gone, while everything else restores. The
server remembers a fingerprint of the key in settings and logs an error
when it is started with another one, and a secret that does not open says why.
None of this keeps a secret from the lambda it belongs to, or from code running in the same process: the code guard is not a sandbox. The encryption is about the database and its backups; the isolation between lambdas is what the guard makes expensive, and no more than that.
The demo demo-registration peppers its password hashes with a
PASSWORD_PEPPER that the seeder gives it as a random secret nobody knows,
and a copy of it works without one (Secret.Exists). Its accounts remember
whether they were peppered, so accounts made before still sign in.
Where the installation runs the build agent, the editor has a Change
section: the owner says what should be different, and the same agent that
builds things on /build changes the lambda. It works in a feature - a new
one, or one the owner picks to go on with - reads the code and the history,
changes only what was asked with change_code, and tries it at the feature's
preview until it works. Then it merges the feature into the next version and
puts that online; switched off, it leaves the feature with its preview online
for the owner to try and merge. The next request defaults to the feature the
last one left open, so asking for a tweak after trying the preview goes on
with the same feature. It is told to leave every other feature alone.
The section shows it happen: what the agent says it is doing, each tool it calls with what came of it (the feature started, its preview online, errors from the compiler, the version merged and online), and a clock against its time limit. Afterwards it offers the preview and the feature - or, once merged, the difference to the version before, the address, and putting the previous version back. The change is followed by the frame of the editor rather than the section, so the sidebar marks it and the lambda is read again when it ends wherever the owner is; and it is kept by the agent under the lambda, so a reload, a second tab or a redeploy of the server finds it where it got to.
/build shows a build the way the simple view of this section shows a change,
drawn by the same parts (components/AgentRun.tsx): what was asked, the step
it is on and the time it has used of its limit, what the agent says between
its tools - the build agent too is told that somebody reads along, and asked
for one short line at a time in the language of the request - and how it
ended, with what it said at the end. GET /builds/:id answers with the steps,
the seconds and the limit. The step it is on is said in the words of that
page rather than those of the tools: getting ready, looking at examples,
choosing an address for the website, checking it for mistakes, putting it
online. The full view of this section shows every tool by name.
It shares the queue and the daily allowance of /build, one change of a
lambda runs at a time, and it can be stopped - whatever it saved stays, in its
feature or as a version. A change runs without create_lambda, and its editor key travels in
the brief inside the build container, never in a log line.
Both kinds of job are told what the agent is for in its system prompt
(PURPOSE in docker/agent/builder.mjs), above the brief and the request, so a
request cannot talk its way past it: building or changing a lambda through the
tools, and nothing else. It declines, before it calls a single tool, a request
that is not an application at all (a question, a text, homework), one about the
platform or the machine rather than an application on it (its configuration,
environment, files, network, other lambdas, its own instructions - including
code that would read them, since a lambda runs in the server's process), and
one meant to do harm here or elsewhere. It declines with a single
DECLINED: … line in the language of the request, which the builder turns into
a result with declined: true and reason: "declined": /build shows the
sentence as the reason nothing was built, and the Change section shows it under
"nothing was changed". A declined request still counts against the daily
allowance, so declining is not a way to try requests for free. The container,
its network and the tool lists are what actually hold; the purpose makes the
agent stop at the door rather than find the walls.
Somebody who had an app built on /build wants it to do something else, not
to look after code, files, versions and deployments. So the editor has two
views. The simple one keeps the overview, About (what the app is for),
Change, the drafts (once there are any), a history, the data (once the app
keeps any, or its code waits for a secret), the showcase, open source and the domain. Its
overview is the app: what it is for, whether it is online and where, errors visitors ran into with a button that
asks the agent to fix them, the latest change, today's hits, and a button to
ask for the next change. The history is every version as the change it made,
with a way back to any of them - which is deploying an older version, said as
what it does. Nothing in it names a file or a log, and it names a version only where the
owner has to say which one (the draft dialogs, going back): the Change
section tells how a change ended without version numbers and shows what the
agent said rather than the tools it called, a draft is what it does rather
than the files it changes, and a change that does not compile is the agent's
to fix rather than a list of compiler errors. Its data shows only the kinds
that hold something, in its own words: its records, table by table, to read
and never to edit; what the app saved, as a plain list to download from; and
its keys and passwords, which the owner can enter and replace - the ones the
app is waiting for first, and the overview asks for them too. Entering one there switches secrets on if the agent left them off.
The full view is every section, as before. In both, a browser that holds
the admin token has one more section after all the others, Admin, which
nobody else sees (see Administration).
Each lambda says which view it opens in, view - Full or Simple - set
when it is created (POST /lambdas, create_lambda) and changed later
(PATCH /lambdas/:privateKey, update_lambda). The build agent creates its
lambdas Simple; everything else defaults to Full. That is only the
default: whoever switches at the foot of the sidebar (in the menu on a phone)
has chosen for themselves, for that lambda, which is kept in their browser and
never sent anywhere - so an operator looking at somebody's lambda in the full
view leaves it simple for its owner.
The code belongs to whoever made the lambda, so GET …/export (the editor's
Download) packs its newest version into an ordinary .NET 10 project that runs
without this platform (Services/Deployment/ProjectPacker.cs):
| File | What it is |
|---|---|
Program.cs | the default GenHTTP host, Host.Create().Handler(Project.Create()).Defaults().RunAsync(), under a header naming the lambda, its version and change, the export date, where to read about GenHTTP, and the environment variables its secrets become (as -e flags in the docker run line too) |
Project.cs | lambda.cs: its statements are the body of Project.Create() (CreateAsync() when they await), its types sit beside the class, made public as on the platform |
*.cs | the other files, their code unchanged, the first letter of the name capitalized |
Platform/ | what the platform provided: Workspace and Assets as folders, Secret reading environment variables of the same name (the values are never exported), Database opening database/database.db where the code uses one, the switch that turns what Project returns into a handler, and the imports every lambda gets as global usings |
assets/ | the files the version ships, copied beside the program on build |
docs/, tests/ | its documentation and its tests, from .lambda/; neither compiled nor copied into the container |
build/ | its build folder - what its assets or code are built from - from .lambda/build/; the build does not look into it (DefaultItemExcludes), nor is it copied into the container |
database/database.db | the lambda's database, an ordinary SQLite file - its records go with it |
Dockerfile | builds and runs it; the workspace is /app/workspace, the database folder is mounted at /app/database |
It references GenHTTP.Full - the internal engine, which runs wherever .NET
does - at the version this server runs, so it behaves as the lambda did, and
Microsoft.Data.Sqlite, Microsoft.EntityFrameworkCore.Sqlite and Evolve
only where the code uses them - Entity Framework where it has a DbContext,
at the release of the SQLite library for .NET 10. Of the data,
the database comes along - copied with SQLite's backup, so it is whole even
while the lambda writes to it, and any SQLite tool reads it - while the
workspace starts empty and the values of the secrets stay here. database/ is
in .gitignore and .dockerignore: records are nobody's source, and the image
mounts them rather than carrying them. The archive is packed into a temporary
file and streamed from there, since a database may be as large as the tier
allows. A test builds the export of every demo with the .NET SDK, so it needs
the package feed.
An owner may publish the code of a lambda as open source: under Open
source in the editor, in both views, right after the showcase, or with the
open_source tool - and only with the editor key, like everything else about
a lambda. Nothing is published until they ask, and taking it down again is one
switch. Publishing asks for a license, MIT unless another is picked from a
short list - MIT, Apache 2.0, BSD 3-Clause, MPL 2.0, GPL 3.0 or later, AGPL
3.0 or later, the Unlicense (SourceLicenses) - and, optionally, the name the
license gives as the holder of the copyright; without one it names "the
authors of" the lambda, since there are no accounts to take a name from.
What is published is the program and what is written about it: every
version, as the export packs it - Project.cs, the other files, assets/,
docs/, tests/, build/, Platform/, the project file, the Dockerfile - with the
license in LICENSE and named in the header of Program.cs. The history says
what each version changed, in the line it was saved with. What is never
published:
ProjectPacker.Publish takes no database to pack, so there is
nothing to leave in by mistake, and a test looks for a record, a saved file
and a secret's value in every file of the download./source lists every published source, searched by its key, what its
documentation says it is and what the showcase says about it where it is
there, ordered by stars, by the newest version or by when it was published.
/source/:publicKey is one: what it is, its license, who holds it, where it
runs - its own domain while it has one - and four views of the version being
read: its code, a tree of the files marked by what each is to the lambda
with the file open beside it, coloured by highlight.js with the editor's
colours, and lines to link to (#L12-L20); its documentation, the pages
of docs/ rendered as the editor renders them, pictures beside them
included; its tests, tests/README.md with the scripts and data beside
it; and its changes, every version newest first. The newest version is
read unless another is picked, which the address keeps (?version=7) from one
view to the next. Any version downloads as a zip. There are no issues, pull
requests or wikis, and no way to act on the lambda from there; somebody who
wants to build on it downloads it, and platform_guide tells an agent how a
download becomes a lambda again. A small notice says what a lambda is and
leads to /build. The pages are a part of the frontend of their own
(src/Frontend/src/source), in a frame of their own without the site's menu,
fetched with their words and highlighter only by whoever opens them, in every
language under the same addresses as every other public page. They are not
prerendered: the server names each after its lambda, describes it with the
first paragraph of its documentation, marks it up as SoftwareSourceCode for
search engines, and lists every published source in the sitemap in every
language. Since what a page shows is fetched from /api/v1/sources/,
robots.txt allows crawlers that part of the API (and the showcase pictures),
while it keeps them out of the rest of it and out of the zips - otherwise a
crawler that renders the page would find it named, and empty. A source that is
not published is not found, the same as a key nobody has.
A version is packed once, on the first request for it, and kept below
/data/sources/{lambda} - a cache and nothing more, since the versions are
what the source is. What it is packed with besides the version - the license,
the holder, the key, the build of the packer - is hashed into the name of the
file, so changing the license packs again and the old one goes. Packing reads
and compresses a whole version, so one version is packed once however many ask
for it at the same moment, two at a time for the whole server, and the cache
is held to LAMBDA_SOURCE_CACHE_BYTES by letting go of the projects read least
recently. The page shows that a version is being packed, and a download waits
for it before it starts. A file of a version is read out of its zip as it is
sent; one sent as it is (…/raw/) comes from the site's origin, so it is sent
as text or bytes - a picture as a picture - never sniffed, and under a
sandboxing content security policy, so a page a lambda ships cannot run here.
Anybody may star a source. A star is a POST with a ticket the page was handed
with the source, signed by the server and at least a second old, so a link, a
crawler or a script posting blind stars nothing. An address stars a source
once and changes only so many stars in ten minutes; which address starred what
is kept in memory as a keyed hash, never on disk, and forgotten on a restart.
The count is in the database (sources.stars) and survives taking the source
down and publishing it again.
The export of a lambda whose source is published carries the same LICENSE.
The demos are published under MIT by the installation, being there to be read
and built on; nobody else can change that, and their editor shows no Open
source section.
Every lambda is a git repository as well, so whoever works on it with their own tools and agent clones it, changes it and pushes - and whoever reads a published source clones every version of it. Both are served by the [GenHTTP.Modules.Git][git-module] package from what the platform already keeps: there is no repository on disk, only the commits the versions and the features are.
git clone https://genhttp.dev/editor/<editor key>/my-app.git
cd my-app && dotnet run # the app on http://localhost:8080/
# change it, commit
git push -o deploy # the next version, online
The owner's address is the editor's with a name added, which only names the
folder a clone makes: any name answers, so a clone keeps working once the
public key changes. It holds the editor key like the editor's address does -
there are no accounts, and the key is what lets a push in. The published
source is /source/<public key>.git, read only, while it is published. Both
answer without the .git, and opened in a browser they lead to their page.
The editor offers the address under Clone - in the full view only, on the
overview and beside the code, and in a draft with its branch - and /source
in the Code menu beside the star, where the zip is too.
What is in it. The project the lambda is exported as, made for a
repository: Project.cs is the snippet as the body of BuildAsync() - the
method the platform runs it in - with its types beside the class, the other
.cs files keep their code and are named the .NET way, assets/ is what it
ships, docs/ and tests/ are its .lambda/docs/ and .lambda/tests/, and
build/ its build folder.
Around them are the platform's files: Program.cs, the project file,
Platform/, the Dockerfile, .gitignore, .dockerignore, AGENTS.md -
how an agent works in the repository, with the rules of the platform in git's
terms - CLAUDE.md, which points Claude Code to it, and LICENSE while the
source is published. Those are the same in every commit until the platform
changes them - nothing in them depends on the version, which is why the
project references SQLite, Entity Framework and Evolve whether the code uses
them or not, keeps build/ out of the build whether there is one or not, and
makes the snippet asynchronous whether it awaits or not - so a push that
leaves them alone is always right. The .gitignore at the root is the
platform's; what a project in build/ installs and builds is kept out by its
own. The data is never in it: no
database, no workspace, no secret. Nor is what only the owner may know - no
specification, no editor key - so the commits the owner reads are the commits
a published source shows.
The history. main is the newest version, each version a commit tagged
v1, v2 and so on, its message the change line; a version pruned is gone
from the history too, which a clone is told is shallow from there. Each
feature is a branch, named once when it starts - after the branch that was
pushed, or after its name in the words git allows (dark-mode) - and kept
when it is renamed (features.branch); a save of the feature is a commit on
it, and moving its base past what it holds is a merge commit bringing that
version in. The commits the platform makes are made when the repository is
read, not when a version is saved, so saving stays what it was and a lambda
nobody clones costs nothing; each is made once from what it stands for - the
files, the time it was saved, its change - and kept below
/data/git/{lambda} with the files it holds, by the id git gives them, so it
is the same commit for everybody who reads it later, a newer platform
included. A commit whose code did not change keeps the Project.cs of the
one before it, so a class somebody pushed in a way of their own is not
written over by a save in the editor. The published source is the same
history without the features: their branches are not there, and their
commits are not found by id either.
Pushing to main. Every commit becomes the next version, in order, with
the origin git, the first line of its message as the change and the rest
as the specification. main only moves forward one commit after another - no
force push, no merge commit, no deletion - and a version saved meanwhile in
the editor or by an agent makes the push "fetch first". A push holding more
commits than the tier keeps versions is refused, asking for them to be
squashed. The newest is compiled first, the code guard included, and a push
that does not compile is refused - its problems come back with the file and
line in the repository, Project.cs(14,9). -o deploy puts the newest
online. A feature whose branch is now in main is merged and goes, as
merging it would have done.
Pushing a branch. A new branch starts a feature from the newest version
among its commits, with a copy of the lambda's data; pushing it again
replaces its files, a force push included, and a branch that holds none of
the versions is refused. Every push puts its preview online - nothing a
visitor sees - and answers with its address. -o merge with a push merges
it as one version, -o deploy beside it puts that online, and deleting the
branch deletes the feature. A feature changed in the editor since the client
fetched refuses the push rather than losing that change.
What is refused. A push that changes, adds or removes one of the
platform's files, or adds a file that has no place in a lambda - a README.md
at the root, an editor's settings - is refused and says which and how to undo
it: changing them would change nothing of the lambda, and silently dropping
the change would leave the commit saying something the lambda does not. So is
an executable file or a link, a tag, Project.cs with something in class Project besides BuildAsync(), and anything the services refuse a save -
names, sizes, a demo. Everything a push does goes through the services the
editor and the agents use, under the same rules.
What it says. A push is answered as it goes, in the lines git prints after
remote:: the versions or the feature it became, problems the compiler
found, what went online and where, and what to do next. They never hold the
editor key - whoever pushed has it, and whoever reads their build log should
not. Options travel only with a push that changes something, so a version
already pushed is deployed in the editor or with
POST /api/v1/lambdas/:privateKey/deployment/start, which the answer names.
Threads and limits. Reading a lambda for the first time reads every
version, so the commits are made on the pool, in the lambda's turn - a
semaphore, one request at a time per lambda. The server packs what a clone
asks for, and applies a push, while it writes its answer, so that answer is
written on the pool into a pipe the reactor copies to the connection
(OffloadedContent). A push is unpacked in memory, up to
LAMBDA_GIT_MAX_PUSH_BYTES, and only two are applied at once.
A single .NET 11 project hosted by GenHTTP.Full.Ioxide, wired up in
Application.cs and divided into
services, a folder each below Services/. From outside its folder a service
is taken only by its interfaces - by the API resources, the MCP tools and the
other services alike - and the classes a service is made of stay in its
folder; ServiceBoundaryTests fails on a constructor or method elsewhere that
takes one of them. Where other services work through a lower layer of a
folder rather than the service the API calls, that layer has an interface of
its own (IDatabaseVault, ISecretVault, IDomainRegistry, ILogBook,
IRunLog).
Services/Meta) - lambdas and their versions, the only component
that speaks to the database. Its public surface is DTOs, mapped by hand.
MetaService is what the API, MCP and the other services call. It finds
the lambda, checks it may be changed, takes the lambda's turn for a change,
and leaves the rest to units: LambdaHosting changes the key, the domain
and the tier, LambdaHistory writes the versions, the stretches online and
the events and keeps each to its bound, LambdaLifetime says when a lambda
goes offline and away and runs the sweep that does it, LambdaRemoval
removes a lambda with everything that hangs on it, and LambdaDescriber
maps the DTOs. LambdaGuard (who may change a lambda - not a demo) and
VersionInput (what a version may hold) are rules other services apply too.Services/Storage) - the code itself, on the file system, one
file per version. Never in the database.Services/Data) - which kinds of data a lambda keeps, switched on
and off by its owner. Only the choice is stored: a lambda without a row for a
kind has its default, so a kind added to DataKinds needs no migration to
exist. Switching a kind off deletes what it held.Services/Workspace) - the private directory of a lambda,
reached from the editor. The same directory the generated Workspace class
writes to from inside a lambda, under the same limits, so a file put there by
hand behaves like one the lambda wrote itself. Only the room it takes is
limited - 256 MB for most lambdas and 2 GB for a premium one, in any number
of files of any size - counted in blocks of 4 KB as the disk counts it, so
an empty file or a folder is not free. The quota is compiled into the
lambda, so moving it to another tier builds it again on its next request.Services/Secrets) - the secrets of the lambdas, sealed in the
database. SecretCipher holds the installation's key and seals and opens
values, SecretVault stores them and is what a running lambda reads through
and the other services keep in step with a lambda (ISecretVault), and
SecretService is what the API and MCP call - by name, never by value.Services/Databases) - the database of each lambda, a SQLite
file below /data/databases/{lambda}, and a copy per feature beside its
files. DatabaseVault makes, copies, exports and deletes them and is what a
running lambda connects through (IDatabaseVault for the other services),
ConnectionGuard is the authorizer and the
limits every connection it hands out carries, and DatabaseService is what
the API and MCP read tables and rows with.Services/Features) - changes worked on beside a lambda. A
row per feature says what it is based on and whether its preview is online;
its files (files.json), what its preview serves (preview.json), its copy
of the workspace and its preview's assets live below
/data/features/{lambda}/{feature}, so deleting a feature is deleting a
folder. Merging goes through Meta, under the same lock every save takes.Services/Source) - whether the owner published the code of a
lambda and under which license, and its stars (sources); SourceCache
packs a version into the project a visitor reads and downloads, once, below
/data/sources/{lambda}, and StarGuard hands out the tickets a star is
given with and remembers who starred what. Kept apart from Meta like the
showcase: nothing about building a lambda reads or changes it.Services/Git) - every lambda as a git repository (see
Working with git). GitService finds the lambda and
takes its turn, GitHistory makes the commits the versions and features
are, GitPushes makes versions and features of what is pushed, GitStore
keeps the commits and their files below /data/git/{lambda}, and
ProjectTree says what a lambda is as a tree and back. The door is
Api/Git/GitRoutes.cs, beside the pages below /editor/ and /source/.Services/Building) - the jobs of the build agent: builds
asked for on /build and changes asked for in the editor's Change section.
BuildService decides what may be asked for and in which order that is
checked, and leaves the rest to units of their own: AgentClient is the
only thing that talks to the agent's runner and holds the shared secret,
BuildAllowance counts the jobs of each address per day, ModelGate says
who may have the second model, and AgentInput checks the prompt and the
language a page sends.Services/Deployment) - wraps a snippet in a method body,
compiles it with Roslyn, loads the assembly and calls PrepareAsync() on
the resulting handler. Compiled once, then cached - one handler for what a
lambda has online, and one for the preview of each of its features. An
assembly stays loaded for the life of the process, so a build is known by
its code: a version that changes only assets, documentation or tests gets a
new handler from the assembly already loaded rather than being compiled
again. Lambdas are compiled against the metadata of the platform's
assemblies, read once and kept - the metadata alone, not the whole files.Services/Execution) - an IHandler, not a web service: it
looks up the handler for a request and runs it.Services/Protection) - concerns in front of execution that
resolve the lambda, rate limit per client and cap concurrency. A lambda is
resolved from memory (Services/Meta/ResolutionCache.cs), which every write
to the database makes stale (Data/DatabaseChanges.cs), so serving a lam