toiljs 0.0.102 → 0.0.103
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 +5 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/toil-docs.generated.js +8 -8
- package/docs/introduction/README.md +15 -13
- package/docs/introduction/design-principles.md +58 -34
- package/docs/introduction/distributed.md +13 -4
- package/docs/introduction/how-it-works.md +81 -34
- package/docs/introduction/hyperscale.md +36 -17
- package/docs/introduction/modern-stack.md +63 -18
- package/docs/introduction/vs-other-frameworks.md +44 -28
- package/docs/introduction/why-toil.md +20 -12
- package/package.json +1 -1
- package/src/compiler/toil-docs.generated.ts +8 -8
|
@@ -1,55 +1,79 @@
|
|
|
1
1
|
# Why toil is built this way (the RSG bar)
|
|
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, and the purpose is a single one: reach top-tier ("AAA") infrastructure, and ship it as the **default**. AAA-grade tech is normally something you assemble from a dozen vendors and babysit with a team that understands distributed systems. toil makes the good version the version you get out of the box, in one framework, with 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 one goal. This page is the rubric toil holds itself against, and the principles 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
|
+
## The bar: one grade, set by the weakest link
|
|
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** (Resilience and Scale Grade) is toil's internal rubric for how resilient, distributed, fast, lean, and secure an app actually is, scored as a single letter from **AAA** down to **D**. It grades nine axes, and one rule runs the whole thing: **your grade is your weakest axis**, never the average and never the best. To earn AAA, all nine must be AAA at the same time.
|
|
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 exists to kill 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. 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).
|
|
14
|
+
|
|
15
|
+
## The principles the bar forces
|
|
16
|
+
|
|
17
|
+
Aiming for AAA-as-default is not a slogan. It forces a specific handful of decisions, and they are the reason toil looks the way it does.
|
|
18
|
+
|
|
19
|
+
### 1. The good, safe, fast version is the default, not an upgrade
|
|
20
|
+
|
|
21
|
+
On most stacks the resilient path is a paid tier and the secure path is a checklist you get to later. toil inverts that: the strong version is what you get **before** you configure anything.
|
|
22
|
+
|
|
23
|
+
- **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, and 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 meant to survive a quantum computer. You do not assemble any of that; you switch it on.
|
|
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.
|
|
25
|
+
|
|
26
|
+
### 2. Batteries are included, and owned
|
|
27
|
+
|
|
28
|
+
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: all built and owned by toil, all first-party. That is not just convenience. It 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.
|
|
29
|
+
|
|
30
|
+
### 3. One framework, not ten vendors
|
|
31
|
+
|
|
32
|
+
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, so there is no hand-written fetch and no drift between the two halves. One project, one deploy, one mental model, instead of a frontend host plus an API host plus a database vendor plus an auth vendor plus a queue, each glued to the next.
|
|
33
|
+
|
|
34
|
+
### 4. Modern foundations, not legacy tooling
|
|
35
|
+
|
|
36
|
+
toil bets on current technology rather than propping up old tooling.
|
|
37
|
+
|
|
38
|
+
- **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, and that multi-tenancy is exactly what makes running near everyone affordable.
|
|
39
|
+
- **Post-quantum crypto** as the login default (see principle 1), not classical crypto you swap out later.
|
|
40
|
+
- **End-to-end types** from the generated `Server` client, so a backend change that breaks the frontend is a compile error, not a production 500.
|
|
41
|
+
- **QUIC and WebTransport** carry realtime ([@stream](../concepts/tiers.md)).
|
|
42
|
+
|
|
43
|
+
### 5. Honesty about the limits
|
|
44
|
+
|
|
45
|
+
toil grades itself on honesty, so the docs and the product both state limits plainly rather than overclaim.
|
|
46
|
+
|
|
47
|
+
- **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; it is not the claim that every app is already running a live global write cluster today.
|
|
48
|
+
- **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.
|
|
49
|
+
- **Analytics** is real on the edge; the dev server returns sample data.
|
|
50
|
+
- **Auth secrets** ship as clearly-insecure dev placeholders so `toiljs dev` just works. A real deployment must set its own.
|
|
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.
|
|
52
|
+
|
|
53
|
+
## The bar in nine axes
|
|
54
|
+
|
|
55
|
+
Each axis names a way a system can be weak. Each row is the single design decision toil makes so that axis cannot be the thing that caps it.
|
|
26
56
|
|
|
27
57
|
| RSG axis | What it grades | The toil design choice that hits it |
|
|
28
58
|
| --- | --- | --- |
|
|
29
|
-
| **Topology + distribution** | How close your code runs to users, and in how many places | Edge compute:
|
|
59
|
+
| **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
60
|
| **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
61
|
| **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 +
|
|
62
|
+
| **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. |
|
|
63
|
+
| **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
64
|
| **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
|
|
65
|
+
| **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
66
|
| **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
|
|
67
|
+
| **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
68
|
|
|
39
69
|
## The punchline
|
|
40
70
|
|
|
41
|
-
toil is opinionated because being AAA on **every** axis at once forces these choices. You cannot
|
|
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.
|
|
71
|
+
toil is opinionated because being AAA on **every** axis at once forces these choices, and 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, or 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 the whole reason toil looks the way it does.
|
|
46
72
|
|
|
47
73
|
## Related
|
|
48
74
|
|
|
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.
|
|
75
|
+
- [How toil is distributed](./distributed.md): the data-path axis in depth, and why distributing writes is the hard one.
|
|
76
|
+
- [What makes toil hyper-scalable](./hyperscale.md): the topology, latency, and program axes in practice.
|
|
53
77
|
- [Security](../concepts/security.md): the security axis and its hard caps.
|
|
54
|
-
- [
|
|
55
|
-
|
|
78
|
+
- [vs other frameworks](./vs-other-frameworks.md): the honest "when not to use toil".
|
|
79
|
+
- [`RSG.md`](../../RSG.md) at the repository root: the full rubric, the internal mirror this page summarizes.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# How toil is distributed
|
|
2
2
|
|
|
3
|
-
Distributing a website's reads is easy. Distributing its writes is the hard part almost nobody solves, and it is why "global" apps are usually only half global. Here is the problem, and how ToilDB (the database built into toil)
|
|
3
|
+
Distributing a website's reads is easy. Distributing its writes is the hard part almost nobody solves, and it is why "global" apps are usually only half global. Here is the problem, and how ToilDB (the database built into toil) is built to distribute the writes.
|
|
4
4
|
|
|
5
5
|
## Reads are easy, writes are hard
|
|
6
6
|
|
|
@@ -8,8 +8,7 @@ A read never changes anything, so you copy your data to servers worldwide and le
|
|
|
8
8
|
|
|
9
9
|
A write is a change, and two writes to the *same thing* can collide. A counter says `10`. Tokyo and Paris both read `10`, both add one, both write `11`. The real answer was `12`, so one add vanished with no error. That is a **write conflict**.
|
|
10
10
|
|
|
11
|
-
You could make every copy agree before accepting a write, but the network will eventually split (a **partition**), and the **CAP tradeoff** says that during a split you keep only two of consistency, availability, and partition tolerance. You either refuse the write (correct but unavailable) or accept it on one side and reconcile later (available but briefly inconsistent). Distributing writes is a real tradeoff to design around.
|
|
12
|
-
|
|
11
|
+
You could make every copy agree before accepting a write, but the network will eventually split (a **partition**), and the **CAP tradeoff** says that during a split you keep only two of consistency, availability, and partition tolerance. You either refuse the write (correct but unavailable) or accept it on one side and reconcile later (available but briefly inconsistent). Distributing writes is a real tradeoff to design around, not a bug to patch away.
|
|
13
12
|
|
|
14
13
|
## So almost everyone centralizes the write database
|
|
15
14
|
|
|
@@ -64,7 +63,17 @@ Distributing writes is not one problem with one answer; each family is the right
|
|
|
64
63
|
|
|
65
64
|
The per-key-home model only works if a lot of unglamorous machinery runs reliably across a flaky network, and ToilDB owns it: 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), ordered **cross-region replication** with per-stream cursors that detect and backfill gaps, **idempotent apply** so redelivered writes cannot double-count, **capacity escrow** and **tenant quotas**, and **failover** with snapshot re-seeding for a cell that has fallen too far behind. Getting all of these right at once is exactly why truly distributed websites are rare.
|
|
66
65
|
|
|
67
|
-
|
|
66
|
+
## What is real today, honestly
|
|
67
|
+
|
|
68
|
+
This is the hard part almost nobody does, so here is a straight account of where it stands.
|
|
69
|
+
|
|
70
|
+
The per-key-home model and all of its core logic (placement, rehoming, replication, idempotent apply, escrow, quotas, failover) are **built and tested**. The mechanism is real, not a diagram.
|
|
71
|
+
|
|
72
|
+
What is not on by default is the **live multi-region deployment**: wiring many real regions into one running mesh (WAN routing) and the [ScyllaDB](https://www.scylladb.com/) storage that backs a production cluster are **configuration-gated**, so you switch them on for a real deployment rather than getting a live global write cluster automatically. The full database-level **leader fencing** on the write path is also still landing (a host-side leader gate is the current version). The design is settled; the last-mile host wiring is what remains.
|
|
73
|
+
|
|
74
|
+
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, but the distribution only spreads out once you deploy with the multi-cell backing configured. You write your app the same way either way; that is the point.
|
|
75
|
+
|
|
76
|
+
The honest one-liner: toil is **built** to distribute writes worldwide, the mechanism is real, and turning it on across live regions is a deployment step, not a rewrite of your app.
|
|
68
77
|
|
|
69
78
|
## Related
|
|
70
79
|
|
|
@@ -1,74 +1,121 @@
|
|
|
1
1
|
# How toil works
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The whole machine, end to end: what you write, what the build produces, and what happens when a user makes a request. Every term is defined as it appears. You configure none of the moving parts below; this page just explains them.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## One project, two homes
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
You build a toil app as a single project, and your code lives in two places.
|
|
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 (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**. You mark a class `@rest` to expose HTTP endpoints, `@service` for typed RPC, `@stream` for realtime, `@daemon` for background jobs, `@database`/`@collection` for stored data, and `@auth`/`@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
|
|
15
|
+
|
|
16
|
+
`toiljs build` turns that one project into three kinds of output, because the browser and the edge need different things.
|
|
17
|
+
|
|
18
|
+
**1. The client bundle.** Your React app plus any pages toil renders ahead of time, packaged as ordinary web files (HTML, JS, CSS, images) that run in the browser. Every shipped asset is fingerprinted with SHA-384 Subresource Integrity, so a browser refuses a file that was tampered with in transit.
|
|
19
|
+
|
|
20
|
+
**2. 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
|
+
**3. 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.*`, `Server.Stream.*`, `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
|
+
**WebAssembly, in two sentences:** a compact binary format that runs at close to native speed with no interpreter warm-up, inside a **sandbox**, a locked box that cannot open files, reach the operating system, or make network calls on its own. It touches the outside world only through the small, fixed set of functions the host hands it (read the request, build a response, query the database), which is exactly 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 node
|
|
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, where each user is served by whichever node is physically closest. There is no **origin server**, no single far machine every request funnels back to, because your backend and its data are already at the edge.
|
|
16
37
|
|
|
17
|
-
|
|
38
|
+
Two things make this work at the density it needs.
|
|
18
39
|
|
|
19
|
-
|
|
40
|
+
- **Multi-tenant on shared boxes.** Because each app is a tiny sandboxed module, 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) to keep the per-request cost low as traffic grows. **QUIC** and **WebTransport** ride on top to power realtime `@stream` connections.
|
|
20
42
|
|
|
21
|
-
|
|
22
|
-
|
|
43
|
+
## The request lifecycle
|
|
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 code?** If the path is a prerendered page, a server-rendered page, or a static asset, the edge serves it and never wakes your backend. This is the fast path for most page loads.
|
|
45
|
-
3. **Otherwise
|
|
70
|
+
2. **Page or code?** If the path is a prerendered page, a server-rendered page, or a static asset, the edge serves it directly and 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`, then calls into your **guest module** (your sandboxed WASM) at its single entry point, which routes to the handler you wrote.
|
|
46
72
|
4. **Your handler reads and writes locally.** When it needs stored data it talks to [ToilDB](../database/README.md), which has a copy right there at the edge. No ocean crossing.
|
|
47
|
-
5. **Your handler returns a `Response`,**
|
|
73
|
+
5. **Your handler returns a `Response`,** the host encodes it back to bytes, and the edge sends it to the browser.
|
|
48
74
|
|
|
49
75
|
The mental model for your backend: a function of the request. Bytes in, bytes out, one request at a time.
|
|
50
76
|
|
|
51
77
|
### Stateless by default
|
|
52
78
|
|
|
53
|
-
A fresh copy of your handler serves each request, and the next request might be served by a node on the other side of the planet
|
|
79
|
+
A fresh copy of your handler serves each request, and the next request might be served by a node on the other side of the planet, so anything you set on an instance field does not survive. This is a feature: interchangeable copies with nothing to coordinate are what let the backend scale worldwide by simply 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
|
+
## Where the data lives: ToilDB
|
|
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 (Documents, Unique, Counter, Events, Membership, Capacity, View), so you pick the shape that fits instead of forcing everything into one table.
|
|
84
|
+
|
|
85
|
+
Its hard trick 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 keep writes pinned to one region; toil is built to distribute both.
|
|
86
|
+
|
|
87
|
+
Be honest about where this stands. The home-region model and its core logic are real and tested, but a *live* multi-region deployment (WAN routing, the ScyllaDB backing) is configuration-gated, not switched on for every app by default, and a far read can lag its home by a few milliseconds (eventual consistency). Your local `toiljs dev` database is a single in-process store. The write model and its trade-off are covered in [how toil is distributed](./distributed.md).
|
|
88
|
+
|
|
89
|
+
## The compute tiers
|
|
90
|
+
|
|
91
|
+
Not all backend code has the same lifespan. toil sorts it into four tiers, and the build assigns each piece automatically from its decorator.
|
|
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 |
|
|
98
|
+
|
|
99
|
+
Most apps live entirely on **L1**, and that tier 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
101
|
## The pieces
|
|
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 are 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.
|
|
@@ -2,50 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
**Hyper-scale** is serving very large, worldwide traffic at low latency without rebuilding your app as it grows. The test: when traffic goes 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
|
+
Plenty of stacks can reach that scale if you throw money at it: dedicated infrastructure per app, a rented vendor for each moving part, and an ops team to keep the seams from tearing. toil was built for the same scale from the other direction: **cheaply and efficiently**. The whole design is aimed at making top-tier reach the default, not a budget line. Cost is the point of this page.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Why hyper-scale is normally expensive
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Running near your users usually means paying for it in three places at once:
|
|
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 are renting 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 server in one region. Every write and every dynamic call pays for a round trip across the planet.
|
|
13
|
+
- **A centralized write database.** The reads scale out; the writes funnel into one primary in one city. That box is both the bottleneck and the thing you overprovision to keep ahead of.
|
|
14
|
+
|
|
15
|
+
Each of those is a cost multiplier, and they stack. toil removes all three.
|
|
16
|
+
|
|
17
|
+
## How toil makes it cheap
|
|
18
|
+
|
|
19
|
+
Three mechanisms do the work, and they reinforce each other.
|
|
20
|
+
|
|
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 that 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 multi-tenant 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
|
+
|
|
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, a 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
|
+
|
|
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 either: every key has one **home region** that orders its writes, while the data replicates out for fast local reads. So you get nearby reads everywhere without a single primary that every write funnels through, and without paying to overprovision one. The mechanism and its honest trade-off (eventual consistency) are in [how toil is distributed](./distributed.md).
|
|
10
26
|
|
|
11
27
|
```mermaid
|
|
12
28
|
flowchart LR
|
|
13
|
-
subgraph Origin["
|
|
29
|
+
subgraph Origin["The expensive shape: everything funnels to one origin"]
|
|
14
30
|
direction TB
|
|
15
31
|
A1["User (Tokyo)"] -->|slow| O[("Origin + DB<br/>(Virginia)")]
|
|
16
32
|
A2["User (Paris)"] -->|slow| O
|
|
17
33
|
A3["User (Sydney)"] -->|slow| O
|
|
18
34
|
end
|
|
19
|
-
subgraph Toil["
|
|
35
|
+
subgraph Toil["The cheap shape: shared edge + homed writes"]
|
|
20
36
|
direction TB
|
|
21
|
-
B1["User (Tokyo)"] --> E1["Edge + data (Tokyo)"]
|
|
22
|
-
B2["User (Paris)"] --> E2["Edge + data (Paris)"]
|
|
23
|
-
B3["User (Sydney)"] --> E3["Edge + data (Sydney)"]
|
|
37
|
+
B1["User (Tokyo)"] --> E1["Edge box + local data (Tokyo)"]
|
|
38
|
+
B2["User (Paris)"] --> E2["Edge box + local data (Paris)"]
|
|
39
|
+
B3["User (Sydney)"] --> E3["Edge box + local data (Sydney)"]
|
|
24
40
|
E1 <-.->|"replicate"| E2
|
|
25
41
|
E2 <-.->|"replicate"| E3
|
|
26
42
|
end
|
|
27
43
|
```
|
|
28
44
|
|
|
29
|
-
The left side has one hot center every user drags a request to and back from, a
|
|
45
|
+
The left side has one hot center every user drags a request to and back from, plus its dedicated fleet, a cost no amount of caching removes. 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.
|
|
30
46
|
|
|
31
|
-
|
|
47
|
+
## Why this is cheap, in one line each
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
- **Density** means the cost of an edge presence is split across many tenants, not carried by one app.
|
|
50
|
+
- **Edge locality** means no origin fleet to run and no cross-planet round trip to pay for on every real request.
|
|
51
|
+
- **No dedicated infra** means you scale by adding interchangeable shared nodes, not by standing up a new stack per app.
|
|
34
52
|
|
|
35
|
-
|
|
53
|
+
Take any one away and a cost reappears: no density and running near everyone is a luxury again; no edge locality and you are back to paying for an origin; a centralized write path and the database caps you and gets overprovisioned to compensate.
|
|
36
54
|
|
|
37
|
-
|
|
55
|
+
## An honest note
|
|
38
56
|
|
|
39
|
-
|
|
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 or repeal the speed of light between continents.
|
|
40
58
|
|
|
41
|
-
|
|
59
|
+
A few things are honestly staged, not "already everywhere":
|
|
42
60
|
|
|
43
|
-
|
|
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, but **live multi-cell deployment** (WAN routing, 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.
|
|
44
63
|
|
|
45
64
|
## Related
|
|
46
65
|
|
|
47
66
|
- [How toil is distributed](./distributed.md): distributing the writes, the hard problem this rests on.
|
|
48
|
-
- [Why toil is built this way (the RSG bar)](./design-principles.md): the efficiency check behind the hot path.
|
|
49
67
|
- [Compute tiers](../concepts/tiers.md): L1 through L4, and the stateless request model.
|
|
50
68
|
- [How toil works](./how-it-works.md): the build outputs and the request lifecycle.
|
|
51
69
|
- [The database (ToilDB)](../database/README.md): families, homes, and eventual consistency.
|
|
70
|
+
- [Why toil is built this way (the RSG bar)](./design-principles.md): the efficiency check behind the hot path.
|
|
@@ -1,36 +1,81 @@
|
|
|
1
1
|
# The modern stack: what toil gives you that others do not
|
|
2
2
|
|
|
3
|
-
Most frameworks give you a way to write code and then send you shopping: a database, an auth provider, email, a rate limiter, analytics, a realtime service, a job runner, all wired together and kept in sync by you. toil owns those parts instead
|
|
3
|
+
Most frameworks give you a way to write code and then send you shopping: a database, an auth provider, email, a rate limiter, analytics, a realtime service, a job runner, each its own account, bill, and SDK, all wired together and kept in sync by you. toil owns those parts instead. They ship in one framework, they are toil's own (nothing third-party sits on your critical path), and they are on from the first line with zero configuration. This page is the full catalog.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The point is not that toil bundles a lot. It is that the good version is the default version: a solo builder gets the same baseline as a funded team, without assembling or babysitting ten vendors. For how the edge and worldwide distribution actually work, see [How toil works](./how-it-works.md) and [How toil is distributed](./distributed.md).
|
|
6
|
+
|
|
7
|
+
## The backend, built in
|
|
8
|
+
|
|
9
|
+
Your TypeScript backend declares what it needs with a decorator or a one-line config flag. toil provides the machinery.
|
|
6
10
|
|
|
7
11
|
| Feature | What it is | Why it matters |
|
|
8
12
|
| --- | --- | --- |
|
|
9
|
-
|
|
|
10
|
-
| [ToilDB](../database/README.md) | A global database with no connection string and seven families
|
|
11
|
-
| [
|
|
12
|
-
| Automatic SRI | A SHA-384 fingerprint on every local script, preload, and stylesheet, plus an import map covering the whole module graph. | Tampered assets simply do not run, even if a CDN or cache hop is compromised. |
|
|
13
|
-
| [Email](../services/email.md) | `EmailService.send(...)` or a reusable `EmailTemplate` with `{{placeholder}}` bodies, through a provider you configure once. | Verification codes, resets, and receipts are one call: validated, per-tenant capped, and off the worker while the provider replies. |
|
|
13
|
+
| [Post-quantum auth](../auth/README.md) | Password login via `server: { auth: true }`. The password is stretched (OPRF + Argon2id) into an ML-DSA-44 keypair in the browser; only the public key is sent; login runs ML-KEM-768 mutual auth so the client also verifies the server. | Real post-quantum cryptography (the NIST-standardized algorithms), in about one line. The password never reaches the server in a usable form, so a breached server yields nothing replayable, and there is no identity vendor to rent. |
|
|
14
|
+
| [ToilDB](../database/README.md) | A global database with no connection string and seven purpose-built families. Its differentiator is distributed writes: every key has one home region that orders its writes, while data replicates outward for fast local reads. | Distributes writes, not just reads, so a thousand servers can write at once with no single-region bottleneck. This is the hard part almost nobody does (trade: eventual consistency, a far read can lag a few ms). |
|
|
15
|
+
| [Email](../services/email.md) | `EmailService.send(...)`, or a reusable `EmailTemplate` with `{{placeholder}}` bodies, through a provider you configure once. | Verification codes, resets, and receipts are one validated, per-tenant-capped call, off the worker while the provider replies. |
|
|
14
16
|
| [Rate limiting](../services/ratelimit.md) | `@ratelimit` on a route, counted at the edge in an exact cross-worker limiter keyed on the caller's network address. | Over-limit clients are rejected before your code runs, blunting brute-force and spam. |
|
|
15
|
-
| [Analytics
|
|
16
|
-
| [Realtime streaming](../realtime/README.md) | A `@stream` class with `@connect
|
|
17
|
-
| [
|
|
18
|
-
| [
|
|
19
|
-
| [
|
|
20
|
-
|
|
|
17
|
+
| [Analytics](../services/analytics.md) | The `Analytics` global reads your site's own counters: `self()` for a snapshot, `series(metric, range)` for a graph. | A real usage dashboard with no extra code and no analytics vendor. |
|
|
18
|
+
| [Realtime streaming](../realtime/README.md) | A `@stream` class with `@connect` / `@message` / `@close` / `@disconnect` hooks, opened from React with `useChannel`. WebTransport (over QUIC) in production, WebSocket in dev. | A resident instance stays alive per connection and remembers state next to the user: chat, presence, live progress. |
|
|
19
|
+
| [Background jobs (`@daemon`)](../background/daemons.md) | One global background worker on a schedule (interval or cron), with lease-based failover so exactly one instance runs worldwide. | Nightly jobs run once globally, with no cron server, queue, or leader election to run yourself. |
|
|
20
|
+
| [Scheduled jobs (`@scheduled`)](../background/daemons.md) | A method that fires on an interval or cron expression, driven by the same lease so it runs once across the fleet. | Recurring work (cleanup, digests, syncs) without standing up a scheduler. |
|
|
21
|
+
| [Materialized views (`@derive`)](../background/derive.md) | A function that re-runs when its source data changes and writes the result into a View family. | Rollups and denormalized read models stay fresh automatically, so reads are cheap without a hand-rolled pipeline. |
|
|
22
|
+
| [Sessions and cookies](../services/cookies.md) | No-import cookie read/set, signed or encrypted, plus the session layer that backs `@auth`. | Login state and small per-user data without a session store to provision. |
|
|
23
|
+
| [Environment and secrets](../services/environment.md) | A per-tenant environment store for config and secrets, read at the edge, never baked into the shipped WASM. | Your compiled program carries no credential; secrets live in one place, disjoint from public config. |
|
|
24
|
+
|
|
25
|
+
### The seven ToilDB families
|
|
26
|
+
|
|
27
|
+
One database, seven shapes, each tuned to its access pattern rather than bent out of a single table model.
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
| Family | For |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| [Documents](../database/documents.md) | General structured records. |
|
|
32
|
+
| [Unique](../database/unique.md) | Uniqueness constraints (one email, one handle). |
|
|
33
|
+
| [Counter](../database/counters.md) | High-throughput increments (views, likes, quotas). |
|
|
34
|
+
| [Events](../database/events.md) | Append-only logs and feeds. |
|
|
35
|
+
| [Membership](../database/membership.md) | Set membership and relationships (who is in what). |
|
|
36
|
+
| [Capacity](../database/capacity.md) | Bounded resources and seat/slot allocation. |
|
|
37
|
+
| [View](../database/views.md) | Read models materialized by `@derive`. |
|
|
38
|
+
|
|
39
|
+
## The frontend, built in
|
|
40
|
+
|
|
41
|
+
The React client is Vite-fast and typed end to end. The pieces that usually take a build config, a fetch layer, and an SEO plugin are already wired.
|
|
42
|
+
|
|
43
|
+
| Feature | What it is | Why it matters |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| [File routing](../frontend/routing.md) | Routes are files under `client/routes/`. The tree is your URL map. | No router config to hand-maintain; add a file, get a route. |
|
|
46
|
+
| [Loaders with revalidation](../frontend/data-fetching.md) | A route `loader` fetches its data before render and revalidates on demand. | Data arrives with the page, not after a client-side waterfall. |
|
|
47
|
+
| [Two rendering paths](../frontend/rendering.md) | Build-time prerender for SEO by default, plus opt-in edge SSR with `export const ssr = true`, both hydrated with `hydrateRoot`. | Static-fast pages where you can, live server rendering where you need it, one codebase. |
|
|
48
|
+
| [Typed `Server` client](../frontend/data-fetching.md) | A generated client (`Server.REST.*`, `Server.Stream.*`, `Server.<service>`) so the browser calls the backend with end-to-end types. | No hand-written fetch and no drift: rename a field on the server and the frontend stops compiling until you fix it. |
|
|
49
|
+
| [Image with LQIP](../frontend/images.md) | A built-in `Image` component with blur / low-quality placeholders and reserved aspect ratio. | Fast, layout-stable images with no CLS and no separate image service. |
|
|
50
|
+
| [Metadata and SEO](../frontend/metadata.md) | Per-route title, description, canonical, and Open Graph (including `og:image`), baked into the served HTML head. | Correct SEO and share cards in view-source, not assembled by JavaScript after load. |
|
|
51
|
+
| [Page search](../frontend/search.md) | A static index of each route's title, description, keywords, and Open Graph, generated at build. | In-app search with no search vendor (see the caveat below). |
|
|
52
|
+
| [SHA-384 SRI](../concepts/security.md) | Subresource Integrity plus importmap integrity on every shipped script, preload, and stylesheet, across the whole module graph. | A tampered asset simply does not run, even if a CDN or cache hop is compromised. |
|
|
53
|
+
|
|
54
|
+
## Owned, not rented
|
|
55
|
+
|
|
56
|
+
Everything above is toil's own code on your critical path, not a black box you cannot inspect, patch, or secure. That is the difference the table below draws.
|
|
23
57
|
|
|
24
58
|
| Capability | toil (built in, zero setup) | Typical stack (you assemble it) |
|
|
25
59
|
| --- | --- | --- |
|
|
26
|
-
| Edge and transport | Frontend and backend worldwide over HTTP/3 | A separate edge product, often reads-only |
|
|
27
60
|
| Database | ToilDB: global, distributed writes, seven families | A managed database, usually single-region for writes |
|
|
28
61
|
| Auth | Post-quantum login; the server holds no password | A rented identity provider or hand-rolled hashing |
|
|
29
62
|
| Email, rate limiting, analytics | Built-in primitives, one call each | A separate SDK or vendor per capability |
|
|
30
|
-
| Realtime and background | `@stream`, `@daemon`, `@derive` | A realtime service plus a cron server, queue, and leader election |
|
|
31
|
-
|
|
|
63
|
+
| Realtime and background | `@stream`, `@daemon`, `@scheduled`, `@derive` | A realtime service plus a cron server, queue, and leader election |
|
|
64
|
+
| Sessions, secrets, cookies | Owned globals, no store to provision | A session store and a secrets manager to run |
|
|
65
|
+
| Frontend integrity and SEO | Automatic SHA-384 SRI, Image/LQIP, metadata, page search | Plugins and services bolted on per concern |
|
|
32
66
|
| Critical-path ownership | The core is toil's own | A mix of vendors you cannot inspect or fix |
|
|
33
67
|
|
|
68
|
+
Honest boundary: "owned" means the core of a working app is toil's, not that outside services are banned. Call a payment provider or another API and you still can.
|
|
69
|
+
|
|
70
|
+
## The honest caveats
|
|
71
|
+
|
|
72
|
+
toil grades itself on honesty, so read these before you count on anything.
|
|
73
|
+
|
|
74
|
+
- **Distributed writes are built, live multi-cell is config-gated.** The home-region model and its core logic are real and tested, but live multi-region deployment (WAN routing, the ScyllaDB backing) is opt-in, not on by default. The local dev database is a single in-process store. toil is built to distribute writes worldwide; not every app is running a live global write cluster today.
|
|
75
|
+
- **Analytics is a dev stub locally.** It is real on the edge; the local dev server returns sample data.
|
|
76
|
+
- **Auth secrets ship as dev placeholders.** The session HMAC, OPRF seed, and ML-KEM key are clearly-insecure placeholders so `toiljs dev` just works. A real deployment must set its own (per-tenant auto-generation at domain registration is the plan).
|
|
77
|
+
- **Page search indexes static metadata only.** Routes whose metadata comes from a dynamic `generateMetadata` are not in the index.
|
|
78
|
+
|
|
34
79
|
## Why it is all a default
|
|
35
80
|
|
|
36
|
-
None of this is an upgrade you unlock later. toil grades itself against [RSG](./design-principles.md) (Resilience and Scale Grade), whose one rule is that your grade is your weakest axis, never the average, so these batteries exist to keep any single axis from quietly capping the whole. Where the honest trade fits your project (ToilDB is not general SQL, the server language is a strict TypeScript subset, the catalog is younger than long-established platforms), the built-in stack is the whole point
|
|
81
|
+
None of this is an upgrade you unlock later. toil grades itself against [RSG](./design-principles.md) (Resilience and Scale Grade), whose one rule is that your grade is your weakest axis, never the average, so these batteries exist to keep any single axis from quietly capping the whole. Where the honest trade fits your project (ToilDB is not general SQL, the server language is a strict TypeScript subset, the catalog is younger than long-established platforms), the built-in stack is the whole point. [Why toil](./why-toil.md) says where it does not.
|