A backend language where the worst case is known before it ships.

Write your routes, data, and rules in TypeScript. Aleph counts what every route can read, write, and compute, rejects what’s unbounded, and runs the rest on shared infrastructure.

Built for agents. Run on Rust.

a typical backenduserswith alephyour codealeph checkserverdb
aleph · app.ts
app.ts
accepted
24a.route("GET /posts")
25 .query({ cursor: t.cursor() })
26 .handle((_, { query }) => {
27 const page = Post.page({ limit: 20, after: query.cursor });
28 return {
29 next: page.cursor,
30 posts: page.items.map((post) => ({
31 post,
32 comments: Comment.query({ prefix: [post.postId], limit: 5 }),
33 })),
34 };
35 });
typical node + express
server.ts
accepted
1app.get("/posts", async (req, res) => {
2 const { rows: posts } = await db.query("SELECT * FROM posts");
3 const out = [];
4 for (const post of posts) {
5 const { rows } = await db.query("SELECT * FROM comments WHERE post_id = $1", [post.id]);
6 out.push({ post, comments: rows });
7 }
8 res.json(out);
9});
two roads to production
general-purposeCPU /memoryalertDBloadtests,CI,deploysslowresponseshugebillaleph✔ auth✔ transactions✔ retries✔ scaling✔ migrations✔ API docs✔ isolation
General-purpose code steers around what can go wrong in production. Aleph rules it out and builds the rest into the road.

Why Aleph

Less flexibility, on purpose.

General-purpose languages let an app do anything. So every app needs its own isolated runtime, like a Lambda function, a Docker container, or a VPS, plus its own review and its own guardrails, and someone has to set up and maintain all of it.

Aleph makes the opposite trade. It gives up some flexibility for shared building blocks for what every backend needs, predictable behavior, and code that’s trusted by default. When agents write the code, that trade matters more than ever: a narrow language is easy to check and hard to get wrong. How Aleph programs work

Usage limits

Aleph knows what your code can use before it runs

Aleph code is declarative. A route isn’t a program that can do anything. It’s a fixed plan of queries, loops, and writes, each with a known maximum size. So Aleph can count, before a single request runs, the most work any piece of logic can ever do: the rows it reads, the rows it writes, the operations it runs, and the memory it uses.

  1. CountedEvery route, trigger, and job gets its worst-case usage, computed from its code.
  2. CheckedCode that could use too much, like a query that reads too many rows or a loop with no end, doesn’t build. A meter enforces the same limits while it runs.
  3. SharedNo app can take the CPU, memory, or database that other apps need, so many apps can run safely in one process, on one database.
  4. CheaperServers and databases aren’t set aside for each app and left idle, so each app costs far less to run.
worst case of every route
app.ts
24a.route("GET /posts")
25 .query({ cursor: t.cursor() })
26 .handle((_, { query }) => {
27 const page = Post.page({ limit: 20, after: query.cursor });
28 return {
29 next: page.cursor,
30 posts: page.items.map((post) => ({
31 post,
32 comments: Comment.query({ prefix: [post.postId], limit: 5 }),
33 })),
34 };
35 });
$ aleph check app.ts
✔ valid. The most each route and handler can use in one run:
route reads writes ops bytes read effects calls memory
limit 250 50 2,000 2 MB 50 3 8 MB
add_comment 1 <1% 1 2% 8 <1% 3.5 KB <1% 1 2% 0 – 14.2 KB <1%
create_post 1 <1% 1 2% 489 24% 3.1 KB <1% 0 – 0 – 138.0 KB 2%
get_posts 120 48% 0 – 192 10% 416.1 KB 20% 0 – 0 – 3.02 MB 38%
A page of 20 posts, 5 comments each, and a cursor for the next page: at most 120 reads. Ask for 10,000 at once and it won't build.

Guardrails

No query can read too much

Every query has a limit, and Aleph multiplies them out: a page of 20 posts with 5 comments each is at most 120 reads, before a single request runs.

So the natural way to return a long list is the right one: a page and a cursor. Ask for everything at once and it’s rejected before it ships, with the line to change. The same limits are enforced while it runs, so a limit is a guarantee, not a guess.

a loop inside a loop
app.ts
37a.route("POST /posts")
38 .id("create_post")
39 .body({
40 title: t.string().min(1).max(200),
41 tags: t.array(t.string().max(32), 100),
42 })
43 .status(201)
44 .handle((_, { body }) =>
45 Post.insert({
46 title: body.title,
47 tags: body.tags.filter((tag) =>
48 body.tags.filter((other) => other.eq(tag)).length.eq(1)),
49 }));
$ aleph check app.ts
✖ over_budget at app.ts:44:4 (routes[1].plan)
route "create_post" can use up to 40809 ops in the worst case, over the limit of 2000. Lower the bounds that drive it (query limits, list sizes) or split the work.
2 problems
Comparing every tag with every other: 10 tags is 489 ops. Allow 100 and it's 40,809, over the limit of 2,000.

Guardrails

No loop can run away

There’s no unbounded while loop and no recursion. Every loop runs over a list with a known maximum size, or declares its own maximum, so even a loop inside a loop has a worst case Aleph can count.

That’s why one app can’t burn the CPU that other apps share: code that could doesn’t build.

more than routes
app.ts
1import { app, t } from "@aleph-lang/sdk";
2
3const a = app("blog"); // the app everything is declared on
4
5const Post = a.entity("Post", {
6 key: ["postId"],
7 fields: {
8 postId: t.id().generated(),
9 title: t.string().min(1).max(200),
10 tags: t.array(t.string().max(32), 10),
11 },
12});
13
14const Comment = a.entity("Comment", {
15 key: ["postId", "commentId"],
16 fields: {
17 postId: t.string().max(26),
18 commentId: t.id().generated(),
19 authorId: t.string().max(64),
20 text: t.string().min(1).max(500),
21 },
22});
23
24a.route("GET /posts")
25 .query({ cursor: t.cursor() })
26 .handle((_, { query }) => {
27 const page = Post.page({ limit: 20, after: query.cursor });
28 return {
29 next: page.cursor,
30 posts: page.items.map((post) => ({
31 post,
32 comments: Comment.query({ prefix: [post.postId], limit: 5 }),
33 })),
34 };
35 });
36
37a.route("POST /posts")
38 .id("create_post")
39 .body({
40 title: t.string().min(1).max(200),
41 tags: t.array(t.string().max(32), 10),
42 })
43 .status(201)
44 .handle((_, { body }) =>
45 Post.insert({
46 title: body.title,
47 tags: body.tags.filter((tag) =>
48 body.tags.filter((other) => other.eq(tag)).length.eq(1)),
49 }));
50
51a.route("POST /posts/:postId/comments")
52 .id("add_comment")
53 .params({ postId: t.string().max(26) })
54 .body({ text: t.string().min(1).max(500) })
55 .status(201)
56 .handle(($, { params, body }) =>
57 Comment.insert({
58 postId: params.postId,
59 authorId: $.caller,
60 text: body.text.trim(),
61 }));
62
63// An external HTTP API. "call" waits for the answer.
64// Each call counts toward the limit.
65const moderation = a.integration("moderation", {
66 provider: "http",
67 operations: {
68 check: {
69 mode: "call",
70 input: { text: t.string().max(500) },
71 output: { spam: t.bool() },
72 },
73 },
74});
75
76// Email. "effect" is sent after commit,
77// exactly once.
78const email = a.integration("email", {
79 provider: "email",
80 operations: {
81 send: {
82 mode: "effect",
83 input: { to: t.string().max(200), subject: t.string().max(200) },
84 },
85 },
86});
87
88// A live channel that clients subscribe to, per post.
89const live = a.channel("comments", {
90 key: { postId: t.string().max(26) },
91 message: { text: t.string().max(500) },
92});
93
94// A trigger: runs after each new comment commits,
95// in the background. The request doesn't wait.
96a.onInsert("moderate", Comment, ($, { payload }) => {
97 const comment = payload.after;
98 const verdict = moderation.call("check", { text: comment.text });
99 // Delete spam; push everything else live.
100 $.if(verdict.spam)
101 .then(() => comment.delete())
102 .else(() =>
103 live.publish({ postId: comment.postId }, { text: comment.text }));
104});
105
106// A schedule: every Monday at 09:00.
107a.every("weeklyDigest", "mon 09:00", ($) => {
108 const posts = Post.query({ limit: 10, order: "desc" });
109 email.send("send", {
110 to: "readers@example.com",
111 subject: $.fmt`${posts.length} new posts`,
112 });
113});
114
115export default a;
$ aleph check app.ts
✔ valid. The most each route and handler can use in one run:
handler reads writes ops bytes read effects calls memory
limit 500 200 5,000 8 MB 200 5 32 MB
moderate 1 <1% 1 <1% 14 <1% 3.5 KB <1% 1 <1% 1 20% 13.6 KB <1%
weeklyDigest 10 2% 0 – 11 <1% 31.4 KB <1% 1 <1% 0 – 64.2 KB <1%
An HTTP API, email, a live channel, a trigger, and a weekly schedule, all in the same app.ts, each with a worst case.

Still flexible

Bounded, not basic

The limits are on usage, not on what you can build. Beyond routes and queries, the same language calls external APIs, reacts to changes, pushes updates to clients in real time, sends email, and runs schedules, queues, webhooks, and multi-step workflows that can wait for days.

Every one of them is checked the same way: triggers and schedules get a worst-case usage, and their effects go out only after they commit.

How Aleph programs work: how state is managed, what the usage limits count, and what the language leaves out.

from app.ts to production
you writeapp.tsaleph buildsmanifest.jsondata, not codealso derived:OpenAPI · typed client · MCPplatformappappappappyour appappappappappone core · one database
The manifest is the only thing that ships. Everything else is derived from it.

How it works

You write the logic. The platform runs it.

  1. Describe your app in TypeScript. Routes, who can call them, the data, and the queries. No server, ORM, or auth library to set up.
  2. Aleph compiles it into a manifest. Data, not code: exactly what each route can do, and the most it can ever use.
  3. A platform runs the manifest. One small, open-source core interprets it, so your code never runs in production.

Because of those guardrails, many apps can run in the same process and share the same database, and a platform pays for capacity once instead of once per app.

Advantages

What the trade buys you

many apps, one runtime
a stack per appserver · db · cacheidleserver · db · cacheidleserver · db · cacheidleeach sized for its peak,paid for around the clockone shared runtimepooled capacity, used
Each request stays under its own ceiling, so no app can slow down the rest.
one database, many apps
app: shopshop/Note/…app: crmcrm/Note/…app: billingbilling/Note/…no expression namesanother app's keysone shared, ordered keyspace · each app reaches only its own prefix
Every key is built inside the app's own prefix. Plans never see raw keys.

Safe to share, so cheap to run

A backend usually gets its own isolated runtime (a Lambda, a container, or a VPS), its own database, and its own cache, sized for its busiest hour and idle the rest of the day. It needs them to itself because, in a shared setup, one runaway query or one bug can hurt everyone.

Aleph rules both out. It counts what every route can use and refuses any that could take more than its share, and there’s no expression that names another app’s data, so cross-app access can’t be written at all. Apps can safely share servers and a database, and you stop paying for idle capacity.

That changes the price, too. A platform doesn’t have to set aside servers and a database for every app it hosts, so it can offer apps built with Aleph much better pricing than a dedicated deployment. How sharing works

one manifest, any runtime
you writeapp.tsaleph buildsmanifest.jsonno runtime insidealeph-coreRust, native: the platformaleph-coreRust as WASM: CLI, aleph dev, testsyour engineGo, Zig, anything that reads the spec
TypeScript is only how you write the app. What ships is a manifest, and any engine that reads it can run it.

Write TypeScript, run on Rust.

Your TypeScript never runs in production. It compiles to a manifest, and the manifest doesn’t care what executes it. Today that’s aleph-core, a small engine written in Rust.

So you get the Rust runtime’s benefits without writing Rust: no garbage-collection pauses or JIT warm-up, steady latency under load, and memory that stays small enough to pack thousands of apps into one process. A new version of your app is a manifest loaded in milliseconds, not a container booting, so there are no cold starts.

The same core runs as WebAssembly in the CLI and in aleph dev, so what you test locally is exactly what runs in production. And because the manifest is an open spec, a platform can swap in a faster engine, or a different language, without you changing a line. Running Aleph

every request is one transaction
requestone transaction✔ commitevent×1email×1job×1requestone transaction✕ crashrolled back,nothing sentno half-written state, no retry logic to write
Effects are queued inside the transaction and sent only after it commits.

Correct by default

Every request runs as one serializable transaction. Events, emails, and jobs go out exactly once, and only after it commits.

There are no half-written states to clean up, no retry logic to write, and no message that was sent for a change that never happened.

compare with what's deployed
app.ts
14const Comment = a.entity("Comment", {
15 key: ["postId", "commentId"],
16 fields: {
17 postId: t.string().max(26),
18 commentId: t.id().generated(),
19 authorId: t.string().max(64),
20 text: t.string().min(1).max(200),
21 likes: t.int().min(0).default(0),
22 },
23});
$ aleph check app.ts --against deployed.json
✖ entities.Comment.fields.text: string length 1-500 narrowed to 1-200; existing values may not fit
✔ entities.Comment.fields.likes: new field likes
✖ not deployable: breaking changes need a deprecation in the deployed manifest first
Narrowing a field could break stored rows, so it's blocked. Adding one is fine.

Change it without fear

Every schema change is compared with what’s deployed and classified as safe, needing a backfill, or breaking. Breaking changes are blocked until they’re deprecated properly, whoever wrote them.

Additive changes ship right away. Anything that could break data already stored waits for a proper deprecation, so stored rows always fit the schema.

an agent at work
app.ts
24a.route("GET /posts")
25 .query({ cursor: t.cursor() })
26 .handle((_, { query }) => {
27 const page = Post.page({ limit: 20, after: query.cursor });
28 return {
29 next: page.cursor,
30 posts: page.items.map((post) => ({
31 post,
32 comments: Comment.query({ prefix: [post.postId], limit: 10 }),
33 })),
34 };
35 });
$ aleph check app.ts
✔ valid. The most each route and handler can use in one run:
route reads writes ops bytes read effects calls memory
limit 250 50 2,000 2 MB 50 3 8 MB
get_posts 220 88% 0 – 192 10% 769.6 KB 38% 0 – 0 – 5.05 MB 63%
The error names the route, the line, and the bound to lower. The agent does the rest.

Built for agents

A small, typed vocabulary means there are no architecture, infrastructure, or library choices for an agent to get wrong.

When it does slip, the checker names the route, the line, and the bound, so the agent fixes its own mistake before anyone reviews it.

Build a first app in five minutes

Write one file, check it, test it, and serve it locally.