How generation works
Your files compile to one model that generates every output on each request. Every output has a default, and the constraints make generation possible.
- Reads
- every Noun ·
theme.json - Generates
- 2 outputs
One model
Your four files compile to one model, and every output reads it. Nothing is copied into your repo, so no generated code drifts from the definition.
- Parse. The engine reads each file's frontmatter, its
$typeand its$id. - Check. It refuses any key, name or input outside the shape, and the refusal names the fix.
- Compile. It builds your Startup's model on the platform's own Nouns and stamps it as a dated Version.
- Generate. The API routes, the typed
$, the MCP tools, the CLI, the schemas, the App screens and the Docs each read that model. - Serve. Every request reads the current Version, so an improvement to a default reaches you on the next request.
Change one line
Add rent to Lease:
rent: { type: money, currency: USD }Send the field in try, which stores nothing:
curl -X PATCH "https://api.sb/<your-domain>/nouns/Lease" -H 'Prefer: mode=try' \
-H "authorization: Bearer $SB_TOKEN" -H 'content-type: application/json' \
-d '{"fields":{"rent":{"type":"money","currency":"USD"}}}'
# "mode": "try", "major": false, "stored": false,
# with the Version it would become and every Interface it touchesThe answer arrives before you commit, and it covers every Interface at once:
| Before | After | |
|---|---|---|
$.Lease | no rent | rent?: number |
| REST operations | 18 | 18 |
| Admin list | 4 of 7 columns | Rent joins before Unit, 5 of 7, with a money input |
Money has one form everywhere: an amount and a currency, as the Offer writes its price. Removing renter answers "major": true, because records hold it. A rename of renter is refused once records exist, and the refusal says to change its label instead.
The defaults
Every SaaS answers the same questions. The platform answers each one once, for every Startup on it.
| Question | Answered for you |
|---|---|
| Who sees a new record? | Only those you grant: private by default |
| What does delete do? | Records a Version, so nothing is lost |
| How many come back in a list? | At most 200 |
| Can a GET change anything? | No. A GET on a Verb's URL previews and runs nothing |
| Does a retried Verb run twice? | No. A Verb POST outside try needs an Idempotency-Key |
| Who may do what? | Four Access levels: owner, admin, member, viewer |
| What are the methods called? | get, list, find, create, update, revert, plus each declared Verb |
| What does your agent's script do by default? | A $ call without a mode runs in try and stores nothing |
| How many things does a script commit? | All its writes, or none |
| Where does the price live? | In the Offer, as one amount and currency every output reads |
| What does a refusal look like? | Typed, with its message, and it costs nothing |
| What does a list screen show? | At most 7 columns, and one button per Verb |
The two script rows apply to $ and MCP. On REST, a record write without Prefer: mode=try commits.
Overrides
The defaults are the product. An override replaces one output, never the definition, and everything else stays generated.
| Override | What you then maintain | What stays generated |
|---|---|---|
| Theme | The token values | Everything |
| CSS | Your rules | Everything; a rule can mask a later token change |
| A field’s input | That one input | The field’s API, schema and filter |
| A Noun’s view | That view’s layout | Routes, data, Verb buttons and the other views |
| A page or screen | That page’s or screen’s layout | Theme, Access levels, try and revert |
| A Function body | What the Verb does | Its URL, preview, undo, button and agent call |
| An API route of your own | Refused: declare a Verb | — |
A Startup with no theme file renders in the default theme, on its Site and in every App screen. Three lines restyle Leaseclock's Site, Admin and SaaS App:
{
"fonts": ["Libre Caslon Text:wght@700"],
"light": { "accent": "#8A4B0F" }
}A printed-form face and a brass clock-hand accent, Leaseclock's own. Every primary button takes the accent. A value that could end a CSS declaration is refused, never emitted.
A page of your own returns catalog blocks with computed props, and writes no markup and no CSS:
const block = (name: string, props: object) => <sb-block data-block={name} data-props={JSON.stringify(props)} />
export default function NoticeBy({ endsOn, days = 60 }: { endsOn: string; days?: number }) {
const by = new Date(Date.parse(endsOn) - days * 86_400_000).toISOString().slice(0, 10)
return block('facts', { title: 'Renewal notice', items: [
{ label: 'Lease ends', value: endsOn },
{ label: `Give notice by (${days} days)`, value: by },
] })
}The renderer applies the theme and checks the props, and you maintain that one component. A screen you add to the App reads and writes through the generated API, so Access levels, try and revert still apply.
An API route of your own is refused. New behaviour is a declared Verb, which gets its URL, preview, undo, button and agent call from one block.
The constraints
The engine refuses what it cannot generate from. Each refusal buys something every output relies on:
| Rule | What it buys | The refusal |
|---|---|---|
| Every key is declared | Every Interface reads the same fields | "properties" is not part of a definition |
| Names follow one grammar | The name is the URL, the method and the screen title | PascalCase, letters and digits |
| Prices live in Offers | The Site, pricing and Checkout read one number | Price has no field "interval" |
The same refusals let an agent write the definition and fix its own mistakes before anything is stored:
| Step | Your agent |
|---|---|
| Learn | Read /llms.txt and the types. |
| Draft | Write each file in Leaseclock's shape. |
| Check | Send it in try mode, which stores nothing. |
| Fix | Fix each refusal. |
| Commit | Commit a dated Version. |
| Undo | Undo with one call. |
What you write as does: is what your agent reads on the method. The platform's own Startup Noun takes the same shape, and its types need no key:
curl -s 'https://api.sb/$.d.ts?nouns=Startup' | grep -A1 'Sell it'
# /** Sell it, with its Customers, to another party · Live|Dormant → Sold · modes try|send · a legal Decision */
# sell(target: string, input: StartupSellInput, o?: { mode?: 'try' | 'send' }): Promise<Action>Why the constraints
Made by hand, screens multiply: 2 audiences, 4 views plus 1 per Verb with input, 3 states, 2 themes and 2 widths.
| Noun | Screens to draw by hand |
|---|---|
Unit | 96 |
Lease | 120 |
The platform carries that count while you stay inside the constraints.
What fits
Made for products that are records and the actions people take on them. A Lease and its renewal fit; so does a Unit and its building. Inside that shape, an agent writes the definition and one person checks it.
Blog and changelog
Every Startup gets a blog at /blog, under its own theme, and a changelog. Each changelog entry is a dated Version of the definition, so the day Lease gained rent is an entry with no post to write.
Your Customers' API
Each Customer gets your API, MCP and Docs on your own domain, with their own key, Limit and Tenant, and a sign-in that sets what people and agents may do.
Get paid with Stripe
Write the price once in your Offer, and your pricing page, Stripe Checkout and every renewal read it, with the money landing in your own Stripe account.