toiljs 0.0.102 → 0.0.104
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 +37 -21
- package/docs/introduction/design-principles.md +62 -36
- package/docs/introduction/distributed.md +42 -25
- package/docs/introduction/how-it-works.md +85 -38
- package/docs/introduction/hyperscale.md +37 -18
- package/docs/introduction/modern-stack.md +68 -19
- package/docs/introduction/vs-other-frameworks.md +46 -30
- package/docs/introduction/why-toil.md +34 -26
- package/package.json +1 -1
- package/src/compiler/toil-docs.generated.ts +8 -8
|
@@ -1,41 +1,57 @@
|
|
|
1
1
|
# Understanding toil
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
|
|
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
|
+
Most apps read fast and write slow. Pages load from caches everywhere, but a write, say a comment or an order, has to reach one database in one region. A user in Tokyo writing to Virginia waits for the trip there and back. That single region is also a single point of failure.
|
|
22
|
+
|
|
23
|
+
toil closes the gap in two moves. Your code runs at the edge instead of one origin. And ToilDB is built to distribute the writes, not just the reads: every key has a home region that orders its writes, while the data copies outward so reads stay local and quick.
|
|
24
|
+
|
|
25
|
+
Spreading reads is easy, and everyone does it. Spreading writes is the hard part, and it is why most "global" apps are only global for reading.
|
|
10
26
|
|
|
11
27
|
```mermaid
|
|
12
28
|
flowchart TB
|
|
13
29
|
subgraph Ordinary["The ordinary web"]
|
|
14
|
-
U1["User in Tokyo"] -->|fast| E1["Edge (
|
|
30
|
+
U1["User in Tokyo"] -->|fast| E1["Edge (pages)"]
|
|
15
31
|
E1 -->|slow, one region| DB1[("Database in Virginia")]
|
|
16
32
|
end
|
|
17
33
|
subgraph Toil["toil"]
|
|
18
|
-
U2["User in Tokyo"] -->|fast| E2["Edge near Tokyo<br/>
|
|
34
|
+
U2["User in Tokyo"] -->|fast| E2["Edge near Tokyo<br/>frontend + backend + data"]
|
|
19
35
|
end
|
|
20
36
|
```
|
|
21
37
|
|
|
22
|
-
|
|
38
|
+
## Where to go from here
|
|
23
39
|
|
|
24
|
-
|
|
40
|
+
Read these in order. Each one builds on the last.
|
|
25
41
|
|
|
26
|
-
1. **[Why toil
|
|
27
|
-
2. **[
|
|
28
|
-
3. **[How toil works](./how-it-works.md)** The whole
|
|
29
|
-
4. **[
|
|
30
|
-
5. **[How toil
|
|
31
|
-
6. **[toil
|
|
32
|
-
7. **[
|
|
42
|
+
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.
|
|
43
|
+
2. **[What comes built in](./modern-stack.md)** The full list of what toil owns and runs for you, and what it does not.
|
|
44
|
+
3. **[How toil works](./how-it-works.md)** The whole path, from a React click through WebAssembly to ToilDB and back.
|
|
45
|
+
4. **[Why it scales cheaply](./hyperscale.md)** How one small program can serve the whole planet without a per-app server bill.
|
|
46
|
+
5. **[How toil distributes writes](./distributed.md)** The hardest problem in web infrastructure, and how ToilDB is built to solve it.
|
|
47
|
+
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.
|
|
48
|
+
7. **[The bar toil holds itself to](./design-principles.md)** The RSG rubric, and its one rule: your grade is your weakest part.
|
|
33
49
|
|
|
34
|
-
## The short
|
|
50
|
+
## The short answer
|
|
35
51
|
|
|
36
|
-
- **Who it is for:**
|
|
37
|
-
- **Why it is fast:** the code runs next to the user, with no
|
|
38
|
-
- **Why it is different:** it distributes
|
|
39
|
-
- **Why it is safe:** the backend is
|
|
52
|
+
- **Who it is for:** anyone shipping a real product who wants global speed without a platform team or ten stitched-together services.
|
|
53
|
+
- **Why it is fast:** the code runs next to the user, with no trip to a distant origin.
|
|
54
|
+
- **Why it is different:** it distributes writes, not only reads.
|
|
55
|
+
- **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.
|
|
40
56
|
|
|
41
|
-
|
|
57
|
+
Ready to build? Jump to [Getting started](../getting-started/README.md).
|
|
@@ -1,55 +1,81 @@
|
|
|
1
|
-
# Why toil is built this way
|
|
1
|
+
# Why toil is built this way
|
|
2
2
|
|
|
3
|
-
toil is opinionated on purpose.
|
|
4
|
-
holds itself against, and this page shows the rubric and the choice each axis forces.
|
|
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.
|
|
5
4
|
|
|
6
|
-
|
|
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.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
fast, lean, and secure an app really is, scored as a single letter from AAA down to D. It grades
|
|
10
|
-
nine axes, and the one rule that matters is this: **your grade is your weakest axis**, never the
|
|
11
|
-
average and never the best. To earn AAA, all nine must be AAA at the same time. A globally edged
|
|
12
|
-
frontend on a single-region database is capped by the database. A worldwide system serving a
|
|
13
|
-
one-second app is capped by latency. The lowest column sets the grade, every time. RSG is not an
|
|
14
|
-
external certification and no auditor issues it; it is a design mirror the team holds up to find
|
|
15
|
-
the weakest link and fix it first. The full rubric lives at the repository root in
|
|
16
|
-
[`RSG.md`](../../RSG.md).
|
|
7
|
+
## How RSG grades an app
|
|
17
8
|
|
|
18
|
-
|
|
19
|
-
"global scale" because the read path is global, while the write path is one box in one region.
|
|
20
|
-
Averaging would let that one strong axis hide the weak one. The minimum refuses to.
|
|
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.
|
|
21
10
|
|
|
22
|
-
|
|
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.
|
|
23
12
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
|
|
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
|
+
|
|
17
|
+
## What the bar forces
|
|
18
|
+
|
|
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
|
+
|
|
21
|
+
### The strong version is the default
|
|
22
|
+
|
|
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.
|
|
24
|
+
|
|
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
|
+
|
|
28
|
+
### Everything is built in and first-party
|
|
29
|
+
|
|
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
|
+
|
|
32
|
+
### One framework instead of ten vendors
|
|
33
|
+
|
|
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
|
+
|
|
36
|
+
### Modern foundations
|
|
37
|
+
|
|
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.
|
|
43
|
+
- **QUIC and WebTransport** carry realtime ([@stream](../concepts/tiers.md)).
|
|
44
|
+
|
|
45
|
+
### Honest limits
|
|
46
|
+
|
|
47
|
+
toil grades itself on honesty, so the docs state the limits plainly.
|
|
48
|
+
|
|
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.
|
|
54
|
+
|
|
55
|
+
## The nine axes
|
|
56
|
+
|
|
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.
|
|
26
58
|
|
|
27
59
|
| RSG axis | What it grades | The toil design choice that hits it |
|
|
28
60
|
| --- | --- | --- |
|
|
29
|
-
| **Topology + distribution** | How close your code runs to users, and in how many places | Edge compute:
|
|
61
|
+
| **Topology + distribution** | How close your code runs to users, and in how many places | Edge compute: frontend and backend both run on nodes next to users, worldwide, not in one origin region. |
|
|
30
62
|
| **Availability** | What survives a failure | Cross-region failover with no single point of failure, so losing a node or a region does not take the app down. |
|
|
31
63
|
| **Data path** | Where data is read and *written* (the hard one) | ToilDB's per-key-home model distributes the **writes**, not just the reads. See [How toil is distributed](./distributed.md). |
|
|
32
|
-
| **Delivered p99 latency** | The end-to-end time the user actually feels
|
|
33
|
-
| **Program performance +
|
|
64
|
+
| **Delivered p99 latency** | The end-to-end time the user actually feels | An allocation-free hot path, measured rather than assumed, so the response is fast for real, not just on paper. |
|
|
65
|
+
| **Program performance + architecture** | Hot-path code quality and cost per request (no brute-forcing latency with a big server bill) | No blocking work on the request path; speed comes from the code, not from overprovisioning. |
|
|
34
66
|
| **Dependencies** | How much of your critical path you own (zero third-party on it = AAA) | An owned, batteries-included stack: nothing third-party sits on the critical request path for you to be unable to inspect or fix. |
|
|
35
|
-
| **Security** | How hard the system is to break, and how bad a breach would be | Post-quantum password login
|
|
67
|
+
| **Security** | How hard the system is to break, and how bad a breach would be | Post-quantum password login, sandboxed WebAssembly backends, and Subresource Integrity on every asset. See [Security](../concepts/security.md). |
|
|
36
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. |
|
|
37
|
-
| **Modern stack + compatibility** | Current
|
|
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. |
|
|
38
70
|
|
|
39
|
-
##
|
|
71
|
+
## Why every axis has to hold
|
|
40
72
|
|
|
41
|
-
toil is opinionated because being AAA on
|
|
42
|
-
reach the top grade with a fast frontend on a centralized database, or a global system running
|
|
43
|
-
slow code, or a modern stack that only works on the newest browser. The weakest-link rule closes
|
|
44
|
-
every one of those escape hatches. Any framework that lets a single axis slip is, by its own
|
|
45
|
-
honest scoring, not AAA. Holding all nine at once is the whole reason toil looks the way it does.
|
|
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.
|
|
46
74
|
|
|
47
75
|
## Related
|
|
48
76
|
|
|
49
|
-
- [How toil is distributed](./distributed.md): the data-path axis in depth, and why distributing
|
|
50
|
-
|
|
51
|
-
- [What makes toil hyper-scalable](./hyperscale.md): the topology, latency, and program axes in
|
|
52
|
-
practice.
|
|
77
|
+
- [How toil is distributed](./distributed.md): the data-path axis in depth, and why distributing writes is the hard one.
|
|
78
|
+
- [What makes toil hyper-scalable](./hyperscale.md): the topology, latency, and program axes in practice.
|
|
53
79
|
- [Security](../concepts/security.md): the security axis and its hard caps.
|
|
54
|
-
- [
|
|
55
|
-
|
|
80
|
+
- [vs other frameworks](./vs-other-frameworks.md): the honest "when not to use toil".
|
|
81
|
+
- [`RSG.md`](../../RSG.md) at the repository root: the full rubric, the internal mirror this page summarizes.
|
|
@@ -1,35 +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
|
+
## The usual fix: one write database
|
|
13
14
|
|
|
14
|
-
|
|
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.
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
It is a reasonable choice. It also hides two costs the [RSG rubric](./design-principles.md) flags on its data-path axis:
|
|
17
18
|
|
|
18
|
-
|
|
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.
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
- **The primary is a single point of failure.** One region holds every write, so if it has a bad day, nothing anywhere can be changed.
|
|
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.
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
## ToilDB's model: one home per key
|
|
24
25
|
|
|
25
|
-
|
|
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.
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
Two things follow from that:
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- **Writes to one key are safe.** Every write to a key travels to that key's home, which **serializes** them (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.
|
|
32
|
-
- **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 funnels through, so no single bottleneck.
|
|
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.
|
|
33
32
|
|
|
34
33
|
```mermaid
|
|
35
34
|
flowchart TB
|
|
@@ -40,13 +39,13 @@ flowchart TB
|
|
|
40
39
|
KA -.-> KC
|
|
41
40
|
```
|
|
42
41
|
|
|
43
|
-
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.
|
|
44
43
|
|
|
45
|
-
|
|
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.
|
|
46
45
|
|
|
47
|
-
## The seven families
|
|
46
|
+
## The seven families
|
|
48
47
|
|
|
49
|
-
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.
|
|
50
49
|
|
|
51
50
|
| Family | What it gives |
|
|
52
51
|
| --- | --- |
|
|
@@ -58,13 +57,31 @@ One "home orders the writes" rule fixes the counter, but different jobs want dif
|
|
|
58
57
|
| [Membership](../database/membership.md) | Sets of who belongs to what |
|
|
59
58
|
| [View](../database/views.md) | A read-optimized result a background job builds |
|
|
60
59
|
|
|
61
|
-
Distributing writes
|
|
60
|
+
Distributing writes has no single answer. Each family is the right tool for one shape of data.
|
|
61
|
+
|
|
62
|
+
## The machinery underneath
|
|
63
|
+
|
|
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
|
+
|
|
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.
|
|
71
|
+
|
|
72
|
+
Getting all of these right at once is why truly distributed websites are rare.
|
|
73
|
+
|
|
74
|
+
## What is real today
|
|
75
|
+
|
|
76
|
+
This is the hard part that almost nobody ships, so here is a straight account of where it stands.
|
|
77
|
+
|
|
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.
|
|
62
79
|
|
|
63
|
-
|
|
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.
|
|
64
81
|
|
|
65
|
-
|
|
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.
|
|
66
83
|
|
|
67
|
-
|
|
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.
|
|
68
85
|
|
|
69
86
|
## Related
|
|
70
87
|
|
|
@@ -1,74 +1,121 @@
|
|
|
1
1
|
# How toil works
|
|
2
2
|
|
|
3
|
-
What
|
|
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
|
-
-
|
|
10
|
-
- **`server
|
|
11
|
-
- **Generated typed client (`shared/`).** A small browser-side client toil generates from the shape of your backend. Your React code calls a normal-looking async function, and the types line up end to end: rename a field on the server and the frontend stops compiling until you fix it.
|
|
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.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
That is the whole surface. One repo, one language family, one `toiljs dev`.
|
|
14
13
|
|
|
15
|
-
##
|
|
14
|
+
## What the build produces
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
`toiljs build` turns that one project into three kinds of output. The browser and the edge need different things.
|
|
18
17
|
|
|
19
|
-
|
|
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.
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
|
|
22
|
+
| Artifact | Holds | Runs |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| `release.wasm` | the hot request path (`@rest`, `@service`) | on every request |
|
|
25
|
+
| `release-stream.wasm` | realtime handlers (`@stream`) | for the life of a connection |
|
|
26
|
+
| `release-cold.wasm` | background and scheduled jobs (`@daemon`) | on a timer, worldwide-singleton |
|
|
27
|
+
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
|
|
34
|
+
## The edge nodes
|
|
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. 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
|
+
|
|
38
|
+
Two things make this work at the density it needs.
|
|
39
|
+
|
|
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
|
+
|
|
43
|
+
## What happens on a request
|
|
44
|
+
|
|
45
|
+
A request never leaves the neighborhood. It lands on the nearest edge node, and everything happens right there.
|
|
23
46
|
|
|
24
47
|
```mermaid
|
|
25
48
|
sequenceDiagram
|
|
26
49
|
participant U as User's browser
|
|
27
|
-
participant E as Nearest edge node
|
|
28
|
-
participant W as
|
|
50
|
+
participant E as Nearest edge node (Rust)
|
|
51
|
+
participant W as WASM host
|
|
52
|
+
participant G as Your guest module
|
|
29
53
|
participant DB as ToilDB (local copy)
|
|
30
54
|
|
|
31
|
-
U->>E: Request
|
|
32
|
-
alt
|
|
33
|
-
E-->>U: Prerendered / SSR page or
|
|
34
|
-
else
|
|
35
|
-
E->>W: Decode bytes
|
|
36
|
-
W->>
|
|
37
|
-
DB
|
|
38
|
-
|
|
55
|
+
U->>E: Request over the network
|
|
56
|
+
alt Page or static asset
|
|
57
|
+
E-->>U: Prerendered / SSR page or file
|
|
58
|
+
else Dynamic API call
|
|
59
|
+
E->>W: Decode raw bytes into a Request
|
|
60
|
+
W->>G: Call your handler
|
|
61
|
+
G->>DB: Read / write nearby data
|
|
62
|
+
DB-->>G: Result
|
|
63
|
+
G-->>W: Response object
|
|
64
|
+
W-->>E: Encode to bytes
|
|
39
65
|
E-->>U: Response
|
|
40
66
|
end
|
|
41
67
|
```
|
|
42
68
|
|
|
43
69
|
1. **The request lands on the closest edge node.** The network routes the user there automatically.
|
|
44
|
-
2. **Page or
|
|
45
|
-
3. **Otherwise
|
|
46
|
-
4. **Your handler reads and writes locally.** When it needs stored data it talks to [ToilDB](../database/README.md), which
|
|
47
|
-
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
|
+
|
|
75
|
+
The mental model for your backend is a function of the request. Bytes in, bytes out, one request at a time.
|
|
76
|
+
|
|
77
|
+
### Why the backend is stateless
|
|
78
|
+
|
|
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
|
+
|
|
81
|
+
## The database, ToilDB
|
|
48
82
|
|
|
49
|
-
|
|
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.
|
|
50
84
|
|
|
51
|
-
|
|
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
|
+
|
|
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
|
+
|
|
89
|
+
## The four compute tiers
|
|
90
|
+
|
|
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
|
+
|
|
93
|
+
| Tier | What runs here | Status |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| **L1** | the stateless per-request hot path (`@rest`, `@service`) | live, the default |
|
|
96
|
+
| **L2 / L3** | longer-lived regional and continental work, `@stream` realtime | opt-in, deployment-gated |
|
|
97
|
+
| **L4** | a single worldwide `@daemon` for background and scheduled jobs | opt-in, deployment-gated |
|
|
52
98
|
|
|
53
|
-
|
|
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).
|
|
54
100
|
|
|
55
|
-
## The
|
|
101
|
+
## The parts of a running app
|
|
56
102
|
|
|
57
|
-
Five parts make up a running toil app
|
|
103
|
+
Five parts make up a running toil app. You have now met all of them.
|
|
58
104
|
|
|
59
105
|
| Piece | What it is | Where it runs |
|
|
60
106
|
| --- | --- | --- |
|
|
61
|
-
| **React client** |
|
|
62
|
-
| **toilscript backend** |
|
|
63
|
-
| **The
|
|
64
|
-
| **ToilDB** |
|
|
65
|
-
| **The four tiers** |
|
|
107
|
+
| **React client** | your frontend UI, the client bundle from the build | the user's browser |
|
|
108
|
+
| **toilscript backend** | your `server/` code compiled to sandboxed WASM | the edge |
|
|
109
|
+
| **The edge node** | the Rust runtime that terminates the connection, serves pages, and runs your WASM | servers in many cities |
|
|
110
|
+
| **ToilDB** | the database with distributed writes, replicated next to your code | the edge |
|
|
111
|
+
| **The four tiers** | where and for how long a piece of backend code lives | L1 nearest, up to L4 worldwide |
|
|
66
112
|
|
|
67
|
-
|
|
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.
|
|
68
114
|
|
|
69
115
|
## Related
|
|
70
116
|
|
|
71
117
|
- [Backend overview](../backend/README.md): the request/response model and the sandbox in depth.
|
|
72
|
-
- [The database (ToilDB)](../database/README.md):
|
|
118
|
+
- [The database (ToilDB)](../database/README.md): families, home regions, and eventual consistency.
|
|
73
119
|
- [Compute tiers](../concepts/tiers.md): L1 request, L2/L3 stream, L4 daemon.
|
|
120
|
+
- [Realtime streams](../realtime/streams.md) and [background daemons](../background/daemons.md): the other two artifacts.
|
|
74
121
|
- [What makes toil hyper-scalable](./hyperscale.md): why this design serves the planet cheaply.
|