toiljs 0.0.103 → 0.0.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/toil-docs.generated.js +8 -8
- package/docs/introduction/README.md +48 -25
- package/docs/introduction/design-principles.md +34 -32
- package/docs/introduction/distributed.md +37 -29
- package/docs/introduction/how-it-works.md +32 -32
- package/docs/introduction/hyperscale.md +23 -23
- package/docs/introduction/modern-stack.md +19 -15
- package/docs/introduction/vs-other-frameworks.md +17 -17
- package/docs/introduction/why-toil.md +29 -31
- package/package.json +1 -1
- package/src/compiler/toil-docs.generated.ts +8 -8
|
@@ -1,43 +1,66 @@
|
|
|
1
1
|
# Understanding toil
|
|
2
2
|
|
|
3
|
-
toil is one framework
|
|
3
|
+
toil is one framework for a whole web app: the frontend, the backend, and the database, in a single project, all running close to your users.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
You write React for the client and TypeScript for the server. toil compiles the server to WebAssembly and runs it at the edge, next to whoever is asking. Auth, database, email, realtime, and background jobs are already built in. Nothing to wire up, nothing to configure. A pizza site gets the same infrastructure a funded team would rent from ten separate vendors.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
This section explains what that means and why it holds up. Start here, then follow the links at the end.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## What toil is
|
|
10
10
|
|
|
11
|
-
toil
|
|
11
|
+
A toil project has three parts in one folder:
|
|
12
|
+
|
|
13
|
+
- `client/` is your React app: file-based routing, data loaders, and a typed client for calling the server.
|
|
14
|
+
- `server/` is your backend, plain TypeScript marked up with decorators like `@rest`, `@data`, and `@auth`. toil compiles it to a small sandboxed WebAssembly module.
|
|
15
|
+
- ToilDB is your database. It is already there. No connection string, no instance to spin up.
|
|
16
|
+
|
|
17
|
+
Types tie the three together. Change a field on the server and the client stops compiling until you fix it. Edge deployment, post-quantum login, and asset tamper-proofing are on by default, not features you bolt on later.
|
|
18
|
+
|
|
19
|
+
## The problem it solves
|
|
20
|
+
|
|
21
|
+
A good web app should be fast everywhere, secure by default, and able to grow without a rewrite. Getting there is the hard part, and most of it has nothing to do with your actual product.
|
|
22
|
+
|
|
23
|
+
You assemble it from rented parts: a host, serverless functions, a database, an auth provider, email, a cache, a queue, analytics, a realtime service. Each is its own account, bill, and SDK, and you keep them all in sync. Under that sits a decade of legacy tooling and a node_modules folder heavier than your app. Worse, the fast and secure version is never the default. A CDN, careful caching, region tuning, hardened auth, current crypto: all of it is extra work, so most apps ship slower and less safe than they should.
|
|
24
|
+
|
|
25
|
+
toil deletes that assembly. One framework runs your frontend, backend, and database at the edge, next to users. Auth, email, realtime, and background jobs are built in and owned. Login is post-quantum out of the box. There is nothing to wire up and no infrastructure to reason about. The good version is the only version.
|
|
26
|
+
|
|
27
|
+
Distributing writes worldwide is one of the hard problems it handles under the hood. Most stacks spread reads everywhere but pin every write to one region, so a user far from it waits for a round trip and that region is a single point of failure. ToilDB is built to distribute the writes too: every key has a home region that orders its writes while the data copies outward for fast local reads. You never set any of it up.
|
|
12
28
|
|
|
13
29
|
```mermaid
|
|
14
30
|
flowchart TB
|
|
15
|
-
subgraph
|
|
16
|
-
|
|
17
|
-
|
|
31
|
+
subgraph Assemble["The usual way: rent and wire the parts yourself"]
|
|
32
|
+
direction TB
|
|
33
|
+
A["Your app"] --> H["host"]
|
|
34
|
+
A --> F["functions"]
|
|
35
|
+
A --> D["database"]
|
|
36
|
+
A --> Au["auth"]
|
|
37
|
+
A --> Em["email"]
|
|
38
|
+
A --> Rt["realtime"]
|
|
39
|
+
A --> J["jobs"]
|
|
18
40
|
end
|
|
19
|
-
subgraph Toil["toil"]
|
|
20
|
-
|
|
41
|
+
subgraph Toil["toil: one framework, at the edge, by default"]
|
|
42
|
+
direction TB
|
|
43
|
+
A2["Your app"] --> T["frontend + backend + database + auth +<br/>email + realtime + jobs, built in and near users"]
|
|
21
44
|
end
|
|
22
45
|
```
|
|
23
46
|
|
|
24
|
-
|
|
47
|
+
## Where to go from here
|
|
25
48
|
|
|
26
|
-
|
|
49
|
+
Read these in order. Each one builds on the last.
|
|
27
50
|
|
|
28
|
-
1. **[Why toil
|
|
29
|
-
2. **[
|
|
30
|
-
3. **[How toil works](./how-it-works.md)** The whole
|
|
31
|
-
4. **[
|
|
32
|
-
5. **[How toil
|
|
33
|
-
6. **[toil
|
|
34
|
-
7. **[
|
|
51
|
+
1. **[Why toil, and who it is for](./why-toil.md)** What is wrong with a modern stack, who gains most from toil, and the honest cases against it.
|
|
52
|
+
2. **[What comes built in](./modern-stack.md)** The full list of what toil owns and runs for you, and what it does not.
|
|
53
|
+
3. **[How toil works](./how-it-works.md)** The whole path, from a React click through WebAssembly to ToilDB and back.
|
|
54
|
+
4. **[Why it scales cheaply](./hyperscale.md)** How one small program can serve the whole planet without a per-app server bill.
|
|
55
|
+
5. **[How toil distributes writes](./distributed.md)** One of the hardest problems in web infrastructure, and how ToilDB is built to solve it.
|
|
56
|
+
6. **[toil next to other stacks](./vs-other-frameworks.md)** A fair comparison with Next.js, Rails, serverless, and the rest, wins and losses both.
|
|
57
|
+
7. **[The bar toil holds itself to](./design-principles.md)** The RSG rubric, and its one rule: your grade is your weakest part.
|
|
35
58
|
|
|
36
|
-
## The short
|
|
59
|
+
## The short answer
|
|
37
60
|
|
|
38
|
-
- **Who it is for:**
|
|
39
|
-
- **Why it is fast:** the code runs next to the user, with no
|
|
40
|
-
- **Why it is different:**
|
|
41
|
-
- **Why it is safe:** the backend is
|
|
61
|
+
- **Who it is for:** anyone shipping a real product who wants global speed without a platform team or ten stitched-together services.
|
|
62
|
+
- **Why it is fast:** the code runs next to the user, with no trip to a distant origin.
|
|
63
|
+
- **Why it is different:** the whole stack is built in and owned, so there is nothing to assemble, and it distributes writes worldwide, not only reads.
|
|
64
|
+
- **Why it is safe:** the backend is sandboxed, passwords never reach the server in a usable form, secrets never ship in the code, and the browser checks every file it loads.
|
|
42
65
|
|
|
43
|
-
|
|
66
|
+
Ready to build? Jump to [Getting started](../getting-started/README.md).
|
|
@@ -1,58 +1,60 @@
|
|
|
1
|
-
# Why toil is built this way
|
|
1
|
+
# Why toil is built this way
|
|
2
2
|
|
|
3
|
-
toil is opinionated on purpose
|
|
3
|
+
toil is opinionated on purpose. The purpose is one thing: reach top-tier infrastructure and ship it as the **default**. AAA-grade tech is normally something you assemble from a dozen vendors and keep alive with a team that understands distributed systems. toil makes the good version the version you get out of the box. One framework, zero configuration. A solo builder starts from the same baseline as a funded team.
|
|
4
4
|
|
|
5
|
-
Every decision on this page traces back to that
|
|
5
|
+
Every decision on this page traces back to that goal. Below is the rubric toil grades itself against, and the choices that rubric forces.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## How RSG grades an app
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
RSG stands for Resilience and Scale Grade. It is toil's internal rubric for how resilient and scalable an app really is, scored as a single letter from **AAA** down to **D**. It grades nine axes. One rule runs the whole thing: **your grade is your weakest axis**. Never the average, never the best. To earn AAA, all nine axes must be AAA at once.
|
|
10
10
|
|
|
11
11
|
A globally edged frontend on a single-region database is capped by the database. A worldwide system running slow code is capped by latency. The lowest column sets the grade, every time.
|
|
12
12
|
|
|
13
|
-
That rule
|
|
13
|
+
That rule kills the most common lie in this space: calling a system "global scale" because the read path is global, while the write path is one box in one region. Averaging lets the strong axis hide the weak one. The minimum refuses to.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
RSG is not an external certification, and no auditor issues it. It is a mirror the team holds up to find the weakest link and fix it first. The full rubric lives at the repository root in [`RSG.md`](../../RSG.md).
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## What the bar forces
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Aiming for AAA as the default is not a slogan. It forces a specific set of decisions, and they are why toil looks the way it does.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
### The strong version is the default
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
- **Tamper-proofing is on, not opt-in.** Every shipped asset carries SHA-384 Subresource Integrity plus importmap integrity, so a modified script is rejected by the browser instead of run. Nothing to remember to turn on.
|
|
23
|
+
On most stacks the resilient path is a paid tier and the secure path is a checklist for later. toil flips that. The strong version is what you get before you configure anything.
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
- **Post-quantum login in about one line.** Enable auth and a password is stretched through an OPRF (ristretto255) and Argon2id into a deterministic ML-DSA-44 keypair. Only the public key ever leaves the device. Login runs ML-KEM-768 mutual auth, so the client verifies the server too. The password never reaches the server in a usable form. ML-DSA and ML-KEM are the NIST-standardized algorithms built to survive a quantum computer. You do not assemble any of it. You switch it on.
|
|
26
|
+
- **Asset tamper-proofing is on by default.** Every shipped asset carries SHA-384 Subresource Integrity plus importmap integrity. A modified script is rejected by the browser instead of run. There is nothing to remember to turn on.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
### Everything is built in and first-party
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Auth, [ToilDB](../database/README.md), email, rate limiting, analytics, realtime streaming, background jobs, materialized views, scheduled jobs, sessions and cookies, an environment and secrets store. toil builds and owns all of it. This is the Dependencies axis, where zero third-party code on the critical path is what earns AAA. Nothing you cannot inspect or fix sits between a request and its response.
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
### One framework instead of ten vendors
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
You write a React frontend and a TypeScript backend in one project. toil runs both, plus the database, near your users worldwide. The browser calls the backend through a generated, fully typed `Server` client. There is no hand-written fetch and no drift between the two halves. One project, one deploy, one mental model. The alternative is a frontend host plus an API host plus a database vendor plus an auth vendor plus a queue, each glued to the next.
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
### Modern foundations
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- **
|
|
38
|
+
toil bets on current technology instead of propping up old tooling.
|
|
39
|
+
|
|
40
|
+
- **WebAssembly backends.** Your TypeScript compiles to a small, sandboxed WASM module. Because the sandbox is real, one edge box safely runs many tenants at once. That multi-tenancy is what makes running near everyone affordable.
|
|
41
|
+
- **Post-quantum crypto** as the login default, not classical crypto you swap out later.
|
|
42
|
+
- **End-to-end types** from the generated `Server` client. A backend change that breaks the frontend is a compile error, not a production 500.
|
|
41
43
|
- **QUIC and WebTransport** carry realtime ([@stream](../concepts/tiers.md)).
|
|
42
44
|
|
|
43
|
-
###
|
|
45
|
+
### Honest limits
|
|
44
46
|
|
|
45
|
-
toil grades itself on honesty, so the docs
|
|
47
|
+
toil grades itself on honesty, so the docs state the limits plainly.
|
|
46
48
|
|
|
47
|
-
- **Distributed writes** are real in design and tested in the core
|
|
48
|
-
- **Compute tiers
|
|
49
|
-
- **Analytics** is real on the edge
|
|
50
|
-
- **Auth secrets** ship as clearly
|
|
51
|
-
- toil is younger than the incumbents and its integration catalog is smaller. [vs other frameworks](./vs-other-frameworks.md) covers when not to reach for it.
|
|
49
|
+
- **Distributed writes** are real in design and tested in the core. Every key has one home region that orders its writes, while data replicates outward for fast local reads (see [How toil is distributed](./distributed.md)). But live multi-region deployment is configuration-gated, not on by default, and the local dev database is a single in-process store. toil is built to distribute writes worldwide, and the mechanism is real. The claim is not that every app is already running a live global write cluster today.
|
|
50
|
+
- **Compute tiers.** The per-request edge (L1) is live and real. Regional, continental, and global-daemon tiers are opt-in and deployment-gated, not always-on for every app.
|
|
51
|
+
- **Analytics** is real on the edge. The dev server returns sample data.
|
|
52
|
+
- **Auth secrets** ship as clearly insecure dev placeholders so `toiljs dev` just works. A real deployment must set its own.
|
|
53
|
+
- toil is younger than the incumbents, and its integration catalog is smaller. [vs other frameworks](./vs-other-frameworks.md) covers when not to reach for it.
|
|
52
54
|
|
|
53
|
-
## The
|
|
55
|
+
## The nine axes
|
|
54
56
|
|
|
55
|
-
Each axis names a way a system can be weak. Each row is the single design
|
|
57
|
+
Each axis names a way a system can be weak. Each row is the single design choice toil makes so that axis cannot be the thing that caps it.
|
|
56
58
|
|
|
57
59
|
| RSG axis | What it grades | The toil design choice that hits it |
|
|
58
60
|
| --- | --- | --- |
|
|
@@ -66,9 +68,9 @@ Each axis names a way a system can be weak. Each row is the single design decisi
|
|
|
66
68
|
| **Client performance + reach** | How well the shipped app runs on old and low-end devices as data grows | A lean React client: a small bundle and linear-or-better hot paths, so it stays smooth on weak hardware, not just new flagships. |
|
|
67
69
|
| **Modern stack + compatibility** | Current foundations, *with* graceful fallback for older clients | WebAssembly backends, post-quantum auth, and QUIC/WebTransport realtime, negotiating down cleanly so a client one version behind still works. |
|
|
68
70
|
|
|
69
|
-
##
|
|
71
|
+
## Why every axis has to hold
|
|
70
72
|
|
|
71
|
-
toil is opinionated because being AAA on
|
|
73
|
+
toil is opinionated because being AAA on every axis at once forces these choices. It delivers them as the default, so you do not have to earn each one by hand. You cannot reach the top grade with a fast frontend on a centralized database. You cannot reach it with a global system running slow code, or a modern stack that only works on the newest browser. The weakest-link rule closes every one of those escape hatches. Any framework that lets a single axis slip is, by its own honest scoring, not AAA. Holding all nine at once, out of the box, is why toil looks the way it does.
|
|
72
74
|
|
|
73
75
|
## Related
|
|
74
76
|
|
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
# How toil is distributed
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Spreading a website's reads across the world is easy. Spreading its writes is hard, and almost nobody solves it. That is why most "global" apps are only global for reading. This page covers the problem and how ToilDB, the database built into toil, is built to distribute the writes.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Why writes are the hard part
|
|
6
6
|
|
|
7
|
-
A read
|
|
7
|
+
A read changes nothing, so you copy your data to servers worldwide and let each user read the nearest copy. Every copy agrees. A reader in Tokyo and one in Paris both get a fast, local answer.
|
|
8
8
|
|
|
9
|
-
A write is a change, and two writes to the
|
|
9
|
+
A write is a change, and two writes to the same key can collide. A counter says `10`. Tokyo and Paris both read `10`, both add one, both write `11`. The real answer was `12`. One add vanished, with no error. That is a **write conflict**.
|
|
10
10
|
|
|
11
|
-
You could make every copy agree before accepting a write
|
|
11
|
+
You could make every copy agree before accepting a write. The network, though, will eventually split into a **partition**. During a split the **CAP tradeoff** says you keep only two of three: consistency, availability, and partition tolerance. You either refuse the write, which is correct but unavailable, or accept it on one side and reconcile later, which is available but briefly inconsistent. Distributing writes is a tradeoff to design around. There is no patch that removes it.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## The usual fix: one write database
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
So nearly every stack keeps **one** primary write database in **one** region and spreads only read replicas worldwide. Every write funnels to that one box, one at a time, so conflicts cannot happen.
|
|
16
16
|
|
|
17
|
-
It is a reasonable choice
|
|
17
|
+
It is a reasonable choice. It also hides two costs the [RSG rubric](./design-principles.md) flags on its data-path axis:
|
|
18
18
|
|
|
19
|
-
- **Far writes are slow.** Post from Tokyo to a primary in Virginia and
|
|
20
|
-
- **The primary is a single point of failure.** One region holds every write
|
|
19
|
+
- **Far writes are slow.** Post from Tokyo to a primary in Virginia, and the write crosses the planet and back before anything saves. The page loaded locally. The write did not.
|
|
20
|
+
- **The primary is a single point of failure.** One region holds every write. If it has a bad day, nothing anywhere can change.
|
|
21
21
|
|
|
22
|
-
The read path is global
|
|
22
|
+
The read path is global. The write path is one machine in one city. Under RSG's weakest-link rule, that single data path caps the whole system.
|
|
23
23
|
|
|
24
|
-
## ToilDB's
|
|
24
|
+
## ToilDB's model: one home per key
|
|
25
25
|
|
|
26
|
-
ToilDB gives **every key its own home**. A key is the label you store data under
|
|
26
|
+
ToilDB gives **every key its own home**. A key is the label you store data under: a user id, a username, a room name. See the [database overview](../database/README.md). Each key gets one home: the single source of truth that orders its writes.
|
|
27
27
|
|
|
28
|
-
Two things follow
|
|
28
|
+
Two things follow from that:
|
|
29
29
|
|
|
30
|
-
- **Writes to one key are safe.** Every write to a key travels to that key's home, which **serializes** them
|
|
31
|
-
- **Writes spread worldwide.** Different keys get different homes, so total write load spreads out. Tokyo users' data can home near Tokyo, Paris users' near Paris. No single box every write
|
|
30
|
+
- **Writes to one key are safe.** Every write to a key travels to that key's home, which **serializes** them: it applies them one at a time, in order. Both counter adds are ordered at the counter's home, so the result is `12`. No global lock over the whole database.
|
|
31
|
+
- **Writes spread worldwide.** Different keys get different homes, so total write load spreads out. Tokyo users' data can home near Tokyo, Paris users' near Paris. No single box carries every write, so no single bottleneck.
|
|
32
32
|
|
|
33
33
|
```mermaid
|
|
34
34
|
flowchart TB
|
|
@@ -39,13 +39,13 @@ flowchart TB
|
|
|
39
39
|
KA -.-> KC
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Reads stay local
|
|
42
|
+
Reads stay local. Each key still replicates outward, so a reader anywhere gets a nearby copy. Those copies are **eventually consistent**. For a brief moment after a write lands at the home, a far read can lag before it catches up. That moment is usually a few milliseconds, and for almost all app data it is invisible. The [database overview](../database/README.md) has the full picture.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
A shared formula decides which location owns a key. It is called rendezvous hashing, and every node computes it the same way, so any node routes a write to the right home with no central coordinator. A key's home can move to follow demand, without rehashing the database.
|
|
45
45
|
|
|
46
|
-
## The seven families
|
|
46
|
+
## The seven families
|
|
47
47
|
|
|
48
|
-
One "home orders the writes" rule fixes the counter
|
|
48
|
+
One "home orders the writes" rule fixes the counter. Different jobs, though, want different guarantees. So ToilDB ships **seven families**. Each is a collection type tuned for one shape of data, and each exposes only the operations that are safe and fast for it.
|
|
49
49
|
|
|
50
50
|
| Family | What it gives |
|
|
51
51
|
| --- | --- |
|
|
@@ -57,23 +57,31 @@ One "home orders the writes" rule fixes the counter, but different jobs want dif
|
|
|
57
57
|
| [Membership](../database/membership.md) | Sets of who belongs to what |
|
|
58
58
|
| [View](../database/views.md) | A read-optimized result a background job builds |
|
|
59
59
|
|
|
60
|
-
Distributing writes
|
|
60
|
+
Distributing writes has no single answer. Each family is the right tool for one shape of data.
|
|
61
61
|
|
|
62
|
-
## The
|
|
62
|
+
## The machinery underneath
|
|
63
63
|
|
|
64
|
-
The per-key-home model only works if a lot of unglamorous machinery runs reliably across a flaky network
|
|
64
|
+
The per-key-home model only works if a lot of unglamorous machinery runs reliably across a flaky network. ToilDB owns all of it:
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
- Per-key **placement** and safe **rehoming**: a rising epoch plus a fencing token, so the old owner stops the instant the new one takes over.
|
|
67
|
+
- Ordered **cross-region replication** with per-stream cursors that detect and backfill gaps.
|
|
68
|
+
- **Idempotent apply**, so a redelivered write cannot double-count.
|
|
69
|
+
- **Capacity escrow** and **tenant quotas**.
|
|
70
|
+
- **Failover** with snapshot re-seeding for a cell that has fallen too far behind.
|
|
67
71
|
|
|
68
|
-
|
|
72
|
+
Getting all of these right at once is why truly distributed websites are rare.
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
## What is real today
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
This is the hard part that almost nobody ships, so here is a straight account of where it stands.
|
|
73
77
|
|
|
74
|
-
|
|
78
|
+
The per-key-home model and all its core logic are **built and tested**: placement, rehoming, replication, idempotent apply, escrow, quotas, failover. This is running code, and it passes its tests.
|
|
75
79
|
|
|
76
|
-
The
|
|
80
|
+
The **live multi-region deployment** is not on by default. Wiring many real regions into one running mesh (the WAN routing) and the [ScyllaDB](https://www.scylladb.com/) storage that backs a production cluster are **configuration-gated**. You switch them on for a real deployment. You do not get a live global write cluster automatically. The full database-level **leader fencing** on the write path is also still landing, and a host-side leader gate is the current version. The design is settled. What remains is the last-mile host wiring.
|
|
81
|
+
|
|
82
|
+
On your laptop, `toiljs dev` runs a single **in-process** database. Everything homes in one place, so your code behaves exactly as it will worldwide. The distribution only spreads out once you deploy with the multi-cell backing configured. You write your app the same way either way.
|
|
83
|
+
|
|
84
|
+
toil is **built** to distribute writes worldwide. The mechanism runs today. Turning it on across live regions is a deployment step, not a rewrite of your app.
|
|
77
85
|
|
|
78
86
|
## Related
|
|
79
87
|
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
# How toil works
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This page walks one app from source to response. What you write, what the build produces, and what happens when a request arrives. Every term is defined where it first appears. You configure none of the machinery below. This page just explains it.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## The client and the server
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A toil app is one project. Your code lives in two folders.
|
|
8
8
|
|
|
9
|
-
- **`client/`** is a **React** frontend
|
|
10
|
-
- **`server/`** is your backend, written in **TypeScript with decorators**.
|
|
9
|
+
- **`client/`** is a **React** frontend built with Vite. File-based routing, data loaders, the usual components. It runs in the user's browser.
|
|
10
|
+
- **`server/`** is your backend, written in **TypeScript with decorators**. Mark a class `@rest` for HTTP endpoints, `@service` for typed RPC, `@stream` for realtime, `@daemon` for background jobs, `@database` or `@collection` for stored data, `@auth` or `@user` for login. There are no servers to provision and no routes to wire by hand. The decorator is the wiring.
|
|
11
11
|
|
|
12
12
|
That is the whole surface. One repo, one language family, one `toiljs dev`.
|
|
13
13
|
|
|
14
14
|
## What the build produces
|
|
15
15
|
|
|
16
|
-
`toiljs build` turns that one project into three kinds of output
|
|
16
|
+
`toiljs build` turns that one project into three kinds of output. The browser and the edge need different things.
|
|
17
17
|
|
|
18
|
-
**
|
|
18
|
+
**The client bundle.** Your React app, plus any pages toil renders ahead of time. It ships as ordinary web files that run in the browser: HTML, JS, CSS, images. Every asset is fingerprinted with SHA-384 Subresource Integrity, so the browser refuses a file that was tampered with in transit.
|
|
19
19
|
|
|
20
|
-
**
|
|
20
|
+
**The server WASM.** Your `server/` backend, compiled by the **toilscript** compiler into a small sandboxed **WebAssembly** module. It runs on the edge, not in the browser and not in Node. The build emits three artifacts, one per compute tier.
|
|
21
21
|
|
|
22
22
|
| Artifact | Holds | Runs |
|
|
23
23
|
| --- | --- | --- |
|
|
@@ -25,22 +25,22 @@ That is the whole surface. One repo, one language family, one `toiljs dev`.
|
|
|
25
25
|
| `release-stream.wasm` | realtime handlers (`@stream`) | for the life of a connection |
|
|
26
26
|
| `release-cold.wasm` | background and scheduled jobs (`@daemon`) | on a timer, worldwide-singleton |
|
|
27
27
|
|
|
28
|
-
**
|
|
28
|
+
**The generated typed client, `shared/server.ts`.** toil reads the shape of your backend and generates a small browser-side client from it. Your React code calls a normal async function, `Server.REST.*` or `Server.Stream.*` or `Server.<service>`, and the types line up end to end. Rename a field on the server and the frontend stops compiling until you fix it. No hand-written `fetch`, no drift.
|
|
29
29
|
|
|
30
|
-
**WebAssembly
|
|
30
|
+
A word on **WebAssembly**. It is a compact binary format that runs at close to native speed with no interpreter warm-up. It runs inside a **sandbox**: a locked box that cannot open files, reach the operating system, or make network calls on its own. The guest touches the outside world only through a small fixed set of functions the host hands it. Read the request, build a response, query the database. That narrow surface is what makes it safe to pack many apps onto one shared machine. ([Why that matters for scale](./hyperscale.md).)
|
|
31
31
|
|
|
32
|
-
toilscript is a strict TypeScript dialect
|
|
32
|
+
toilscript is a strict TypeScript dialect: explicit value types, no `any`, no runtime `RegExp`. The [decorators reference](../concepts/decorators.md) covers the full set.
|
|
33
33
|
|
|
34
|
-
## The edge
|
|
34
|
+
## The edge nodes
|
|
35
35
|
|
|
36
|
-
Your WASM does not run on a laptop or in one central data center. It runs on the **edge**: a fleet of Rust runtime nodes spread across many cities
|
|
36
|
+
Your WASM does not run on a laptop or in one central data center. It runs on the **edge**: a fleet of Rust runtime nodes spread across many cities. Each user is served by whichever node is physically closest. There is no **origin server**, no single far machine every request funnels back to. Your backend and its data are already at the edge.
|
|
37
37
|
|
|
38
38
|
Two things make this work at the density it needs.
|
|
39
39
|
|
|
40
|
-
- **Multi-tenant on shared boxes.**
|
|
41
|
-
- **A custom userspace datapath.** The node handles network packets itself in userspace
|
|
40
|
+
- **Multi-tenant on shared boxes.** Each app is a tiny sandboxed module, so one edge box safely runs many apps at once. That shared density is what makes putting compute near everyone affordable instead of a luxury.
|
|
41
|
+
- **A custom userspace datapath.** The node handles network packets itself in userspace, multi-queue, bypassing the operating system's network stack. This keeps the per-request cost low as traffic grows. **QUIC** and **WebTransport** ride on top to power realtime `@stream` connections.
|
|
42
42
|
|
|
43
|
-
##
|
|
43
|
+
## What happens on a request
|
|
44
44
|
|
|
45
45
|
A request never leaves the neighborhood. It lands on the nearest edge node, and everything happens right there.
|
|
46
46
|
|
|
@@ -67,28 +67,28 @@ sequenceDiagram
|
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
1. **The request lands on the closest edge node.** The network routes the user there automatically.
|
|
70
|
-
2. **Page or
|
|
71
|
-
3. **Otherwise the host runs your guest.** The **WASM host
|
|
72
|
-
4. **Your handler reads and writes locally.** When it needs stored data it talks to [ToilDB](../database/README.md), which
|
|
73
|
-
5. **Your handler returns a `Response
|
|
70
|
+
2. **Page or dynamic call.** A prerendered page, a server-rendered page, or a static asset is served directly by the edge, which never wakes your backend. This is the fast path for most page loads.
|
|
71
|
+
3. **Otherwise the host runs your guest.** The **WASM host**, trusted Rust, decodes the raw bytes into a `Request`. It calls into your **guest module**, your sandboxed WASM, at its single entry point, which routes to the handler you wrote.
|
|
72
|
+
4. **Your handler reads and writes locally.** When it needs stored data it talks to [ToilDB](../database/README.md), which keeps a copy right there at the edge. No ocean crossing.
|
|
73
|
+
5. **Your handler returns a `Response`.** The host encodes it back to bytes and the edge sends it to the browser.
|
|
74
74
|
|
|
75
|
-
The mental model for your backend
|
|
75
|
+
The mental model for your backend is a function of the request. Bytes in, bytes out, one request at a time.
|
|
76
76
|
|
|
77
|
-
###
|
|
77
|
+
### Why the backend is stateless
|
|
78
78
|
|
|
79
|
-
A fresh copy of your handler serves each request
|
|
79
|
+
A fresh copy of your handler serves each request. The next request might land on a node on the other side of the planet, so anything you set on an instance field does not survive. This is deliberate. Interchangeable copies with nothing to coordinate are what let the backend scale worldwide by adding more nodes. When you need something to persist, write it to ToilDB. See the [backend overview](../backend/README.md#stateless-by-default).
|
|
80
80
|
|
|
81
|
-
##
|
|
81
|
+
## The database, ToilDB
|
|
82
82
|
|
|
83
|
-
State lives in [ToilDB](../database/README.md), toil's own database, replicated next to your code at the edge. It offers seven purpose-built families
|
|
83
|
+
State lives in [ToilDB](../database/README.md), toil's own database, replicated next to your code at the edge. It offers seven purpose-built families: Documents, Unique, Counter, Events, Membership, Capacity, View. You pick the shape that fits instead of forcing everything into one table.
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
The hard part is **distributed writes**. Every key has one **home region** that orders its writes, while the data replicates outward so reads stay fast and local everywhere. Most stacks distribute reads but pin writes to one region. toil is built to distribute both.
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
Where this stands today. The home-region model and its core logic are real and tested. A live multi-region deployment, meaning WAN routing over the ScyllaDB backing, is configuration-gated. It is not switched on for every app by default. A far read can also lag its home by a few milliseconds, the eventual-consistency trade-off. Your local `toiljs dev` database is a single in-process store. The [distributed writes page](./distributed.md) covers the write model and its trade-off in full.
|
|
88
88
|
|
|
89
|
-
## The compute tiers
|
|
89
|
+
## The four compute tiers
|
|
90
90
|
|
|
91
|
-
Not all backend code has the same lifespan. toil sorts it into four tiers, and the build assigns each piece
|
|
91
|
+
Not all backend code has the same lifespan. toil sorts it into four tiers, and the build assigns each piece from its decorator automatically.
|
|
92
92
|
|
|
93
93
|
| Tier | What runs here | Status |
|
|
94
94
|
| --- | --- | --- |
|
|
@@ -96,9 +96,9 @@ Not all backend code has the same lifespan. toil sorts it into four tiers, and t
|
|
|
96
96
|
| **L2 / L3** | longer-lived regional and continental work, `@stream` realtime | opt-in, deployment-gated |
|
|
97
97
|
| **L4** | a single worldwide `@daemon` for background and scheduled jobs | opt-in, deployment-gated |
|
|
98
98
|
|
|
99
|
-
Most apps live entirely on **L1**,
|
|
99
|
+
Most apps live entirely on **L1**, which is real and running today. L2 through L4 are built and opt-in, not always-on for every app. Full detail is on the [tiers page](../concepts/tiers.md).
|
|
100
100
|
|
|
101
|
-
## The
|
|
101
|
+
## The parts of a running app
|
|
102
102
|
|
|
103
103
|
Five parts make up a running toil app. You have now met all of them.
|
|
104
104
|
|
|
@@ -110,7 +110,7 @@ Five parts make up a running toil app. You have now met all of them.
|
|
|
110
110
|
| **ToilDB** | the database with distributed writes, replicated next to your code | the edge |
|
|
111
111
|
| **The four tiers** | where and for how long a piece of backend code lives | L1 nearest, up to L4 worldwide |
|
|
112
112
|
|
|
113
|
-
None of these need configuring to get the good version. You write React and decorated TypeScript
|
|
113
|
+
None of these need configuring to get the good version. You write React and decorated TypeScript. The build, the edge, and the database come as the default, not a stack you assemble and babysit.
|
|
114
114
|
|
|
115
115
|
## Related
|
|
116
116
|
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
# What makes toil hyper-scalable
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Hyper-scale means serving very large worldwide traffic at low latency without rebuilding your app as it grows. Picture traffic climbing from a thousand users to a hundred million across every continent. Do you rewrite the system, or just run more of it?
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Any stack can reach that scale if you spend enough: dedicated infrastructure per app, a rented vendor for each moving part, and an ops team to keep the seams from tearing. toil reaches the same scale from the other direction, cheaply. The whole design aims to make worldwide reach a default, not a budget line. This page is about cost.
|
|
6
6
|
|
|
7
|
-
## Why
|
|
7
|
+
## Why serving the whole world is normally expensive
|
|
8
8
|
|
|
9
|
-
Running near your users usually means paying
|
|
9
|
+
Running near your users usually means paying in three places at once.
|
|
10
10
|
|
|
11
|
-
- **Dedicated infrastructure per app.** Each app gets its own boxes, its own containers, its own always-warm capacity. Spread that across many cities and you
|
|
12
|
-
- **A far-away origin.** Most stacks serve pages from everywhere but send anything real back to one origin
|
|
13
|
-
- **A centralized write database.**
|
|
11
|
+
- **Dedicated infrastructure per app.** Each app gets its own boxes, its own containers, its own always-warm capacity. Spread that across many cities and you rent a lot of mostly-idle hardware.
|
|
12
|
+
- **A far-away origin.** Most stacks serve pages from everywhere but send anything real back to one origin in one region. Every write and every dynamic call pays for a round trip across the planet.
|
|
13
|
+
- **A centralized write database.** Reads scale out. Writes funnel into one primary in one city. That box is both the bottleneck and the thing you overprovision to stay ahead of.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
These are cost multipliers, and they stack. toil removes all three.
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## The three things that make toil cheap
|
|
18
18
|
|
|
19
19
|
Three mechanisms do the work, and they reinforce each other.
|
|
20
20
|
|
|
21
|
-
**1. Density: one box safely runs many apps.** Your backend compiles to its own tiny
|
|
21
|
+
**1. Density: one box safely runs many apps.** Your backend compiles to its own tiny [sandboxed](./how-it-works.md#what-build-produces) WebAssembly module. It starts fast, runs at near-native speed, and cannot touch another tenant's files, memory, or network. Because the sandbox is that tight, one shared edge box holds many apps at once instead of one app per machine. That density is what makes running near everyone affordable. You are not renting dedicated hardware in fifty cities. You are one tenant on boxes that are already there and already busy.
|
|
22
22
|
|
|
23
|
-
**2. Edge locality: no trip to a central origin.** toil has no origin server. Every request runs on the [edge](../concepts/tiers.md) node nearest the user, on the per-request
|
|
23
|
+
**2. Edge locality: no trip to a central origin.** toil has no origin server. Every request runs on the [edge](../concepts/tiers.md) node nearest the user, on the per-request L1 hot path. The network hands the bytes to the WASM host, your handler runs, and the response goes back, all in one place. There is no slow hop to a faraway box to make the request real. Removing the origin removes both the latency and the standing cost of running one.
|
|
24
24
|
|
|
25
|
-
**3. Local reads, homed writes.** The data your handlers share lives in [ToilDB](../database/README.md), replicated outward so every edge node reads from a copy right next to it. Writes are not centralized
|
|
25
|
+
**3. Local reads, homed writes.** The data your handlers share lives in [ToilDB](../database/README.md), replicated outward so every edge node reads from a copy right next to it. Writes are not centralized. Every key has one home region that orders its writes, and the data replicates out for fast local reads. You get nearby reads everywhere without a single primary that every write funnels through, and without paying to overprovision one. The [distributed writes](./distributed.md) page covers the mechanism and its honest trade-off, eventual consistency.
|
|
26
26
|
|
|
27
27
|
```mermaid
|
|
28
28
|
flowchart LR
|
|
@@ -42,24 +42,24 @@ flowchart LR
|
|
|
42
42
|
end
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
The left side has one hot center every user drags a request to and back from, plus its dedicated fleet
|
|
45
|
+
The left side has one hot center every user drags a request to and back from, plus its dedicated fleet. No amount of caching removes that cost. The right side has no center and no per-app fleet. Add a city, add a shared box, and every tenant on it gets that city for near nothing.
|
|
46
46
|
|
|
47
|
-
## Why
|
|
47
|
+
## Why each mechanism lowers the cost
|
|
48
48
|
|
|
49
|
-
- **Density**
|
|
50
|
-
- **Edge locality** means no origin fleet to run and no cross-planet round trip
|
|
51
|
-
- **No dedicated
|
|
49
|
+
- **Density** splits the cost of an edge presence across many tenants instead of loading it on one app.
|
|
50
|
+
- **Edge locality** means no origin fleet to run and no cross-planet round trip on every real request.
|
|
51
|
+
- **No dedicated infrastructure** means you scale by adding interchangeable shared nodes, not by standing up a new stack per app.
|
|
52
52
|
|
|
53
|
-
Take any one away and a cost
|
|
53
|
+
Take any one away and a cost comes back. Without density, running near everyone is a luxury again. Without edge locality, you pay for an origin. With a centralized write path, the database caps you and gets overprovisioned to compensate.
|
|
54
54
|
|
|
55
|
-
##
|
|
55
|
+
## What is real today
|
|
56
56
|
|
|
57
|
-
This is the design, not a benchmark. Real throughput and latency depend on your hardware, where your users are, how your data is shaped, and how your handler is written. toil removes the central bottlenecks and keeps per-request cost low
|
|
57
|
+
This is the design, not a benchmark. Real throughput and latency depend on your hardware, where your users are, how your data is shaped, and how your handler is written. toil removes the central bottlenecks and keeps per-request cost low. It does not make a slow handler fast, and it does not repeal the speed of light between continents.
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
Some parts are honestly staged, not already everywhere.
|
|
60
60
|
|
|
61
|
-
- The per-request
|
|
62
|
-
- ToilDB's home-region write model and its core logic are real and tested
|
|
61
|
+
- The per-request L1 edge path is live and real. The L2 regional, L3 continental, and L4 global-daemon tiers are opt-in and deployment-gated, not always-on for every app. See the [tiers page](../concepts/tiers.md).
|
|
62
|
+
- ToilDB's home-region write model and its core logic are real and tested. Live multi-cell deployment, meaning WAN routing and the ScyllaDB backing, is configuration-gated, not on by default. The local dev database is a single in-process store. [How toil is distributed](./distributed.md) is honest about what is finished.
|
|
63
63
|
|
|
64
64
|
## Related
|
|
65
65
|
|