Why Pleris
Pleris is a language for web apps, and a compiler and a server for it. You write the data, the server's reads and writes, and the pages in one language. The compiler then refuses programs with whole classes of bugs that TypeScript, React, Next.js and Svelte let through to production.
This page shows what it refuses, with the decision records (ADRs) behind each refusal, and then what it does not do yet. Every example on it is compiled by a test, so what it says the compiler does is what it does.
What the compiler refuses
A user's data in a cache everyone shares
In Next.js, one wrong revalidate or a missing cookies() call can cache
one user's cart and serve it to the next visitor. In Pleris a query says
whose data it returns (public, session or user), and a session's data
cannot sit in a shared cache:
module cart
import capability.{ Session, SessionId }
type Cart = Cart { items: Int }
fn cart_of(s: Session<SessionId>) -> Cart !{ database.read<Cart> }
host "shop:data/carts#get"
session query MyCart(session: Session<SessionId>) -> Cart
freshness 0.seconds
consistency read_your_writes
cache shared
key session
concurrency one_per_key
on_key_change cancel
timeout 2.seconds
{
cart_of(session)
}The label follows the value through calls, branches and returns (ADR-0128, ADR-0129, ADR-0252, ADR-0282).
Rendering that reaches the database
A React component can fetch in its body, in an effect, or in a helper three
calls down. In Pleris a view declares an empty effect row, !{}, and the
compiler checks the claim through every call it makes:
module menu
type Item = Item { name: String }
fn items() -> List<Item> !{ database.read<Item> }
host "shop:data/items#all"
view Menu() !{} {
let all = items()
<ul>
{#each all as item (item.name)}
<li>{item.name}</li>
{/each}
</ul>
}A page reads data only through a declared query, which has a key, a cache
policy and a timeout (charter §7.4, §7.5). The tour shows how.
One gap: the check does not look in an {#each} head (where pw check
stops).
A function that says it is pure and is not
An effect row is a claim the compiler holds a function to. A helper that reads the clock makes every caller that claims purity wrong:
module stamp
import clock
fn stamp() -> Int {
clock.now()
}
fn label() -> Int !{} {
stamp()
}A case forgotten
A sum type's cases are checked in every match, as in Rust or Elm, and a
misspelt case is a case the type lacks, never a catch-all binding:
module shapes
type Shape =
| Circle(Int)
| Square(Int)
| Point
fn area(s: Shape) -> Int !{} {
match s {
Circle(r) => 3 * r * r,
Square(side) => side * side,
}
}A link to nowhere
Every internal link is matched against the program's routes (ADR-0160):
module site
page Home() {
route "/"
view {
<title>Home</title>
<main>
<a href="/abuot">About us</a>
</main>
}
}
page About() {
route "/about"
view {
<title>About</title>
<main>
<h1>About us</h1>
</main>
}
}A form control nobody can name
A control a screen reader cannot announce is refused (ADR-0141), as are
invalid nesting, a duplicate id and a page without a <title>:
module search
page Search() {
route "/search"
view {
<title>Search</title>
<main>
<input type="text">
</main>
}
}And more, each with a code
A button that sends a command its reader may not run (ADR-0307), a command a page can send twice without saying how a repeat is answered (ADR-0154), a query that keeps running for a key the page no longer shows, an unchecked cast of outside data, a secret serialized to the browser. The diagnostic codes list all of them, from the compiler's own registry.
What it does not do yet
Pleris is a research project, and these are true today (2026-10-15):
- Your app's storage is Rust. A program reads and writes through host
bindings, functions whose body is an operation the server supplies
(
host "feed:data/posts#timeline", ADR-0218, ADR-0262). The development server has three data layers, written in Rust for the reference apps: a social feed's, a food-delivery store's and a dictionary's. A new app uses one of them, as the tutorial's guestbook uses the feed's, or someone writes a new one in Rust. A program cannot yet declare its own storage. - What a predicate means is the server's. A command
requires SignedInorOwnsPost(post), and the development server knows three predicates by name (ADR-0302). - Sign-in is a development provider on this machine (ADR-0258). There is no adapter for a real OpenID Connect provider yet.
- One server. A second server on the same database hears nothing of the first's writes (ADR-0246).
- No project tooling. There is no
pw new, no project file and no editor support yet: you name the standard packages on the command line. - Gaps in the checks.
pw checkis not the whole build:pw buildrefuses programs it passes (a<title>that reads a signal, a computed list in an{#each}), so build before you trust a green check. Effects in an{#each}head are now checked, including its list and key (ADR-0345). An<img>withoutaltis not refused. The tour's last section shows the remaining gaps and the repaired effect refusal in programs the guide's test compiles. The message for&&now names Pleris's&spelling (errors). - Missing features: offline and real-time collaboration, i18n,
background jobs beyond a scheduled command (ADR-0334), and charts.
docs/vision/capabilities.mdkeeps the full matrix.
Where to go next
- The language tour: the language, one topic at a time.
- The tutorial: a guestbook with sign-in, built end to end and run on the development server.
- The diagnostic codes.
This page is docs/guide/why.md, and a test compiles every example on it.