Documentation

Build well

The handful of ordering and shape decisions that determine whether an app survives its second change request.

This is not a feature tour. It is the short list of decisions that decide whether your app absorbs its second change request in an afternoon or gets rebuilt.

Everything here costs nothing on day one and a great deal on day thirty.


1. Data shape first. Then screens. Then permissions.

Build in that order, every time.

Screens bind to your entities, so the shape is the thing everything else is pinned to. Reshape an entity later and you are not editing one thing — you are revisiting every screen that read it, every rule scoped to it, and every permission written against it.

The platform will tell you how bad a change is before you make it: adding a field is additive, renaming or retyping one is breaking, and removing one is destructive. Those are not warnings to click past. A destructive change to a shape your client already has data in is a conversation with them, not a save.

⚙️ The practical test. Before you draw a single screen, write down the things this business talks about and what it knows about each. If you cannot do that without saying “and then on the second tab…”, you are describing a screen, not a shape.


2. Name fields for the business, not for the form

A field key outlives the screen it was created for. status on a form called Intake becomes status everywhere forever — in filters, in rules, in exports your client opens in a spreadsheet, in the columns of a chart.

  • Name it what the business calls it. If they say “site”, do not call it location_2.
  • Do not encode the screen in the name. intake_notes becomes wrong the moment notes are taken somewhere else.
  • Do not encode the type. date_start reads worse than starts_on and tells you nothing extra.

★ Renaming later is a breaking change. Naming carefully is the cheapest thing on this page.


3. Enums over free text, wherever a value will be counted

If anyone will ever filter by it, group a chart by it, or report on it — make it a defined set of options, not a text box.

Free text guarantees you will eventually have Urgent, urgent, URGENT and urgnet in the same column, and every count of them is wrong. No amount of care by the person typing prevents this; they are typing quickly, into a box, on a phone.

Defined options also pay you back immediately: the platform derives the picker from the options you set, so the edit form and the filter offer the same list without you maintaining either. A value that cannot be typed wrongly does not need cleaning later.

⚠️ The reverse is also true — do not make something an enum when the set genuinely is not known. A required dropdown that lacks the right answer teaches people to pick the nearest wrong one, and that is worse than free text, because it looks clean.


4. One entity per real-world thing

Not one per screen, and not one per step in a process.

A “Site Visit” and a “Site Visit Follow-Up” are usually one entity with a type field, not two entities. Two entities means two sets of permissions, two sets of rules, and a report that has to union them forever.

The test: would the business ever hold one of these in their hand and call it a different thing? If not, it is one entity.

Conversely, do not fold two real things into one because they share fields today. If they have separate lifecycles — different people, different timing, different endings — they are separate, and merging them means every rule needs a condition to tell them apart.


5. Let the template map the roles

When you build from a template, it asks you to map its field roles onto your entity. Map every one deliberately.

★ If a role has nothing sensible to map to, that is information: your entity is probably missing a field. The template is describing a shape that has worked before, and the gap it found is real more often than not. Adding the field is usually the right move; forcing an unrelated field into the role to get past the screen is how an app ends up with a date column called “owner”.

The platform refuses to instantiate a template with unmapped roles, and names each one. That refusal is the feature.

⚙️ Declare a money field as money. A template carries a format for each role — the Executive Dashboard says currency for its amount — but that is a guess made before anyone knew which of your fields it would land on. Bound to a duration in minutes it once rendered $5,295 on a live app, so the platform now discards the template’s guess and takes the answer from your field instead.

⚠️ Which means the guess is discarded even when it was right: currency and number are the same field type, so a real money field that says nothing about itself is presented as a plain number. Set the field’s format to Currency and it is honoured everywhere — in the stored page, on the canvas, and in the delivered app. It costs one dropdown at build time and it is the difference between a headline that reads $1,839,772 and one that reads 1,839,772.


There are two ways to say “an order belongs to a client”, and only one of them is enforced.

  • A reference field is a real link. Delete a client that orders still point at, and the platform refuses, naming the orders. Nothing is ever left pointing at nothing.
  • A relationship entry is documentation of intent. Nothing checks it on delete, so the client goes and the orders keep a value that no longer resolves.

⚙️ You can see which is which without asking: on the schema canvas, an accented, solid connector is protected and a faint, dashed one is not — the legend in the corner says so — and each row in the Relationships panel is badged Delete protected or No delete protection.

⚠️ A third case: the entity at the other end is gone. If you archive an entity that something still links to, the link does not quietly disappear — the canvas draws it in red, dotted, ending in an ✕ (the legend gains a BROKEN row), and the Relationships panel badges that row Target missing. Repoint it at a live entity, or remove it.

⚙️ Deleting an entity follows the same rule as deleting a record, so “Delete protected” means one thing everywhere:

  • Another entity points at it with a reference field ⇒ the delete is refused, naming the referrers: “Can’t delete “Customer”: 1 reference field in 1 other entity still point at it — order — customer_ref.” Repoint or remove those fields, then delete.
  • Only relationship entries point at it ⇒ the delete is allowed, after a confirmation that names them: “1 relationship link … will BREAK … Nothing enforces these, so the delete is allowed — they will simply stop resolving.”

⚠️ That asymmetry is deliberate. Refusing on a relationship would make “No delete protection” false in the other direction.

★ If losing the parent would corrupt the child, use a reference field. Reach for a plain relationship only when the link is genuinely advisory.

⚠️ The cascade checkbox on a relationship does nothing today. Ticking it does not delete children and does not add protection.


5b. The studio saves as you go — so “did I save?” is not a question you carry

Everything you author in Build saves itself. There is no Save button to hunt for and no work to lose by clicking away: leaving a tab writes what is outstanding rather than dropping it, and the tab says where your work is — Saving shortly… · All changes saved · Could not save.

⚠️ The exceptions are the ones that reach outside the studio, and they stay explicit on purpose: a deploy, a publish, a send, and a run that writes records. Those happen when you say so, never on a timer.

⚙️ If a save fails, the tab says so and keeps your work. That is the half that makes the rest safe — you can stop thinking about saving only because the screen shouts when it could not. A tab whose data failed to load never writes at all, so a blank state cannot overwrite a real one.

⛔ One behaviour across every tab, and Appearance — the tab most often open in front of a client — discarded an entire look, with no question, when you left it.

6. Deploy early, and often

A draft nobody has deployed has not been tested. It has been looked at.

Deploy the moment there is anything to see, then keep deploying. Two reasons, and the second is the one people learn the hard way:

  1. The gap between what you built and what your client sees is where surprises live. Closing it daily makes each surprise small.
  2. Your first deploy will surface configuration you did not know you owed — readiness checks run before an app can go out, and finding those on day two costs an hour. Finding them the morning of a demo costs the demo.

⚙️ Deploying is not the same as showing. Deploy freely; choose separately when to point your client at it.


7. Permissions last — but never “later”

Do permissions after the shape and the screens settle, because writing them against a moving shape means writing them twice.

But do them before anyone real signs in, because nothing is granted implicitly. A role with no explicit grant on an entity sees nothing at all — not a filtered list, not an empty state you can explain. That is deliberate: the alternative is a role quietly seeing more than intended, which is the failure you cannot detect by looking.

A user in that position now sees “You don’t have access to this data — ask your administrator for read access to entity” on each widget, rather than a failure message. That is a deliberate distinction: a denial is a normal state for a restricted seat, and dressing it as breakage sends the client’s staff to you with a bug report instead of an access request.

⚙️ A deploy REFUSES while a role can read nothing at all, naming the role. It still never grants anything for you. An app whose members only submit forms is a legitimate design, so there is a way through — “Deploy anyway — nobody will hold “‹role›””, naming the roles it acknowledges — and taking it is recorded against the deploy.

⚠️ It refuses rather than warning. A warning would be justified as “shipping a blind role is a choice an operator may make”, and that turned out to be false in practice: an entity is created with no grants at all, so every app was blind by default. Nobody chose it. Refusing until you either grant access or say that nobody will hold the role is what makes it a choice.

⚠️ Sign in as the role and look. Do not read the permission matrix and conclude it is right. The matrix is what you meant; the session is what you did.


The shape of a build that ages well

Do thisBecause
Write down the business’s nouns before drawing screensscreens bind to shapes, not the other way round
Name fields the way the client speaksthe key outlives every screen it appears on
Define the options for anything countablefree text becomes four spellings of one value
One entity per real thingtwo entities is two of everything, forever
Map every template role, or add the missing fieldan unmappable role is a gap in your shape
Deploy on day one and every day aftera draft has been looked at, not tested
Sign in as each role before handoverdeny-on-unset means a missing grant is invisible from the matrix