A full-stack system
This guide follows todo, a to-do list in the platform's own repository. Every
declaration quoted here is copied from it, so it is one that validates and
deploys.
Four words carry the rest of this guide:
- A component is one separately deployable part. Its
kinddecides what else it may declare. - An interface is one facet a component serves, and the unit others name. A component may serve several.
- A variant is one way a component is made or obtained. An environment picks between them.
- A part holds business logic. Parts belong to the system rather than to a component, and a component binds the ones it needs.
What the parts are
| component | kind | what it is |
|---|---|---|
web |
web-assets |
the page a browser loads |
kong |
online-service |
the gateway that fronts everything the browser reaches |
curity |
online-service |
the identity server |
postgres |
online-service |
the identity server's own database |
api |
online-service |
our service, holding the to-do logic |
couchbase |
online-service |
the store, holding the data |
cli |
installed |
a command a person installs and runs |
A component declares what it is and what it needs. It never declares an address: hosts, ports and credentials are resolved at deploy time and handed to the code as configuration, so the same declaration works on a laptop and in a cluster.
How a part names another
There is one rule with two spellings, and the difference is who does the reaching.
A link is a call. Our api reaches Couchbase, so the api's model part says so:
links.store = "couchbase/data"
That names a component and one of its interfaces. What host, what port, what credential, all of it arrives at deploy time and none of it is written here.
The two halves are named by different people. The target couchbase/data is
<component>/<interface>, and a component is named for the contract it
provides. The key store is this consumer's own word for what it reaches; it
becomes the module path the code uses (links::store) and nothing outside this
part depends on it.
A route is a forward. The gateway does not call the api on its own behalf; it passes a browser's request through to it:
routes.api = { is = "api/todos", headers = { "token-handler-version" = "1" } }
routes.apps = "curity/apps"
routes.userinfo = "curity/userinfo"
links.introspect = "curity/introspect"
The gateway both forwards to Curity and calls it: routes for the pages a
browser is sent to, links.introspect for the token exchange the gateway
performs itself. routes only appear on a router; a component that merely calls
another has links and nothing else.
A page's links are all fronted. The browser reaches everything under one origin, so the page names the gateway's routes rather than the services behind them:
links.todos = "kong/api"
links.apps = "kong/apps"
links.userinfo = "kong/userinfo"
These name the gateway's routes, and resolution follows the chain to the
surface at the end of it, so todos answers at the api's own base-path of
/api and the page's generated client calls that path. Because every call goes
to the page's own origin, the session cookie is first-party.
Where each piece is written
This is the todo system as it stands. Two rules place most of it: a
component's declarations live under components/<id>/, and its code under
src/components/<id>/. Both paths are fixed, so nothing points at anything.
todo/
├─ system.toml the environments, and a deploy config per place
├─ targets.toml every place a deploy of this project can land
├─ bluetext.toml the project manifest — its presence is the claim
│
├─ components/ WHAT EACH PART IS — one directory per component
│ ├─ api/
│ │ ├─ component.toml kind, its interface, who may call it
│ │ ├─ variants/main.toml what it is made from: toolchain, image, port
│ │ └─ k8s/ deployment.yaml, service.yaml
│ ├─ couchbase/
│ │ ├─ component.toml the couchbase driver, and its bucket state
│ │ ├─ api-config/schema/bundle.toml the bucket, made before consumers
│ │ └─ variants/, k8s/
│ ├─ curity/
│ │ ├─ component.toml its interfaces; `use` picks hosted or ours
│ │ ├─ variants/main.toml, variants/hosted.toml
│ │ └─ file-config/main/ config.xml, curity-users.xml
│ ├─ kong/ routes and links: what it forwards, what it calls
│ ├─ postgres/ curity's own database
│ ├─ web/ the page: links only, no interfaces
│ └─ cli/ the installed command
│
└─ src/ THE CODE — yours, and the platform's beside it
├─ models/
│ ├─ parts/todos-core.toml the part: what it links, what it holds
│ └─ src/lib.rs the business logic — yours
├─ links/src/lib.rs generated — how a part reaches what it links
└─ components/
├─ api/
│ ├─ endpoints/todos.toml one table per endpoint
│ ├─ shapes/todos.toml the wire shapes they name
│ ├─ main/src/main.rs the service — yours
│ ├─ main/src/bluetext/ generated — this environment's compiled answers
│ ├─ wire/ generated — the wire types
│ └─ driver/todos/rust/ generated — the Rust client, for the cli
└─ web/
├─ src/app.ts the page — yours
├─ src/links/todos.ts generated — the TypeScript client
└─ bluetext.proxy.js generated — the dev server's forwards
Bold is what you write; the rest the platform writes on every b generate
and every deploy. Generated files are committed so the repository shows what the
declarations produced, and a release refuses to publish if regenerating would
change them, which is what keeps the two in step.
Two absences are worth naming. There is no client library for the page or the
command-line client, and no Dockerfile for the api: both come from the
declarations. The k8s/ directories are yours, and hold the Deployment and
Service for components the platform does not build images for.
The rest of this guide takes one part of this system per page, and the last two pages leave the declarations for the code: what a service's crate and handlers look like, and what a part does with the links it declares.