Protoss docs
A total, deterministic, content-addressed language for full-stack apps. Live demo: protoss.app — and this documentation is itself a Protoss program.
Install
macOS arm64 (other platforms: build from source, OCaml >= 5.1):
curl -fsSL https://store.protoss.app/releases/install.sh | shThe installer verifies the tarball's sha256, unpacks binary + standard library to ~/.protoss/dist, and prints the PATH line for your shell. Update later by re-running the same line.
Five minutes
protoss init myapp
cd myapp
protoss live . # http://localhost:8080/You get a working full-stack counter: the local half lives in the page, the shared half lives in one backend model folded from an event log. Open two tabs, bump in one, watch the other move.
- Edit src/Frontend.protoss, save - the page reloads in ~0.6s. A failing save shows the type error in the page and keeps the last good build running.
- Press the hourglass button: the time-travel debugger. Click any past message to refold the app at that instant; present resumes.
The shape of an app
Three files. Types.protoss is the contract shared by both halves:
type alias FrontendModel =
{ count : Nat, shared : String }
type FrontendMsg
= Bump
| BumpShared
| GotShared Nat
type ToBackend = Bump
type ToFrontend = Synced NatFrontend.protoss - update is pure and synchronous (a click can never be lost or clobbered by a slow request); effects runs against the same (msg, model) pair and re-enters through normal dispatch:
update : FrontendMsg -> FrontendModel -> FrontendModel
update msg model =
case msg of
Bump _ -> { model | count = succ model.count }
BumpShared _ -> model
GotShared n -> { model | shared = Nat.toString n }
effects : FrontendMsg -> FrontendModel -> Process (Maybe FrontendMsg)
effects msg model =
case msg of
BumpShared _ ->
bind (sendToBackend (Bump unit))
(\m -> done (Some (GotShared m.count)))
_ -> done None
view : FrontendModel -> View FrontendMsg
view model =
column
[ text (String.concat "local: " (Nat.toString model.count))
, button "Bump local" (Bump unit)
]Backend.protoss - the backend is not mutable state: it is the deterministic fold of updateBackend over the to-backend event log. Broadcasts push to every connected client; onConnect welcomes each new one:
updateBackend : ToBackend -> BackendModel -> Tuple BackendModel (Cmd {} ToFrontend)
updateBackend msg model =
tuple { count = succ model.count } (broadcast (Synced (succ model.count)))
fromBackend : ToFrontend -> FrontendMsg
fromBackend tf =
case tf of
Synced n -> GotShared n
onConnect : BackendModel -> ToFrontend
onConnect model =
Synced model.countLanguage tour
The surface is deliberately Elm-like. Under it, every program canonicalizes to a typed graph and hashes to a stable p2: ref - two spellings of the same program get the same hash.
Types, records, unions
type alias User = { name : String, score : Nat }
type Shape
= Circle Nat
| Square Nat
area : Shape -> Nat
area s =
case s of
Circle r -> r * r * 3
Square w -> w * wStructural recursion
Protoss is total: every program terminates. Recursion is written the Elm way and checked structural - the recursive call must take the smaller part:
sum : List Nat -> Nat
sum xs =
case xs of
[] -> 0
x :: rest -> x + sum rest
count : Nat -> Nat
count n =
case n of
0 -> 0
succ m -> 1 + count mShapes the checker cannot prove terminating are rejected with the equivalent fold named in the error. Folds are also directly callable:
total : List Nat -> Nat
total xs =
foldList xs 0 (\x acc -> x + acc)Let, lambdas, prelude
describe : List Nat -> String
describe xs =
let doubled = List.map xs (\x -> x * 2) in
String.concat "n=" (Nat.toString (List.length doubled))The standard library (List, Maybe, Result, Map, Set, String, Nat) is itself written in Protoss. Polymorphic functions apply directly: List.map, List.filter, List.reverse, ...
Effects and subscriptions
All effects are typed Process values built with done / bind; capabilities are declared per file (capabilities Server.request) and enforced by the checker.
Typed subscriptions (all optional defs; paused while time-traveling, replayed from the ledger):
onFrame : Nat -> Model -> Maybe Msg -- requestAnimationFrame, dt in ms
onKeyDown : String -> Model -> Maybe Msg
onKeyUp : String -> Model -> Maybe MsgViews are data (column, text, button, node/SVG...) - which is what makes a whole game run scrubbable: see the Game tab on protoss.app.
Deploy
With an invite token, your app goes live at <name>.protoss.app - no account, no infrastructure:
protoss deploy . --name <name> --token <your-invite-token>
# Deployed: https://<name>.protoss.app/The backend runs at the edge as the same evaluator the browser runs, folding your updateBackend over a durable event log. Redeploying keeps the log: your data survives.
No invite yet? Ask the person who sent you here. Bring-your-own-Cloudflare is also supported: protoss deploy . --edge --domain yourdomain.com
CLI cheatsheet
| Command | What it does |
|---|---|
protoss init <dir> | scaffold a full-stack app (its URL is fixed at creation) |
protoss live . | dev server: hot reload, in-page errors, time-travel |
protoss app check . | typecheck the full-stack contract end to end |
protoss deploy . --token <t> | publish to <name>.protoss.app |
protoss hash <file> | the program's content hash - the program IS this ref |
protoss fmt <file> | canonical formatting |
protoss explain <code> | explain an error code |
protoss version | check your binary is current |
Why Protoss
- Content-addressed: equivalent programs - any spelling, any machine - hash to the identical p2: ref. The store, packages and deploys are all derived from it; missing objects resolve from the world store automatically.
- Total and deterministic: no crashes, no non-termination, no wall-clock or randomness leaking into results. That is what makes replay exact.
- Time-travel is not a devtool bolt-on: the app is a fold over a message log, so any past state is recomputable - in dev AND in prod.
- One language, both halves: the browser and the edge run the same evaluator over the same typed contract. sendToBackend is typed end to end.
- Built for AI agents: programs are checked canonical graphs, and the MCP server mutates them only through validated patches - an agent can never leave the project in a broken state. See the tab.