Summary#
canPress: <policy> ties a control's pressable state to a declared policy. The button is enabled when the
signed-in caller satisfies the policy and disabled when they don't — computed on the client, reactively, so it
reflects the current caller with no extra code.
// Creating an organization is admins-only — declared once on the entity:
// entity Organization { ... security { allow create when IsPlatformAdmin; } }
// The button that starts that create REFLECTS the same rule, so a non-admin sees it disabled:
Pressable(onClick: NewOrg, canPress: IsPlatformAdmin) {
BtnPrimary("New organization") { Icon(Icons.Plus, size: 18); }
}This is a reflection, not a gate. The server enforces the policy on the action itself; canPress only spares the
caller a click that would be refused. Because the rule lives in one place — the policy (see page authorization (policies)) — the
button can never drift out of sync with what the server actually allows.
A value control uses canEdit the same way — the field stays visible but becomes read-only unless the caller
satisfies the policy:
[Principal] entity User { [Required] string Email; }
[Role] enum AppRole { PlatformAdmin, Member }
entity RoleGrant {
[Required] User User;
[Required] AppRole Role;
security { allow read when IsAuthenticated; }
}
policy IsPlatformAdmin => RoleGrant.Any(r => r.User == user && r.Role == AppRole.PlatformAdmin);
entity Application {
[Required] string Slug;
[Required] string Name;
security { allow create, read, update when IsAuthenticated; }
}
[Page("/settings/{id}")] [Render(CSR)]
component OrgSettings(Guid id) {
var org = Application.Single(a => a.Id == id);
action Seed() { }
action NewOrg() { }
render {
// The field shows the value to everyone, editable to admins:
Input(value: org.Slug, canEdit: IsPlatformAdmin);
// An admin-only affordance — a member never sees it (nothing is rendered, not just hidden):
Stack(canSee: IsPlatformAdmin) {
Button("Seed sample data", onPress: Seed);
}
// Disabled rather than hidden, and it says WHY.
Pressable(onClick: NewOrg,
canPress: IsPlatformAdmin,
whenDenied: "Only platform admins can create organizations.") {
Text("New organization");
}
}
}To hide an element (and its whole subtree) unless the policy holds, use canSee on any element — a container is the
usual choice, so a whole section appears only for callers who satisfy the policy:
// An admin-only affordance — a member never sees it (nothing is rendered, not just hidden):
Stack(canSee: IsPlatformAdmin) {
Button("Seed sample data", onPress: Seed);
}Signature#
<PressableControl>(canPress: <PolicyName>) // → disabled unless the policy holds
<ValueControl>(canEdit: <PolicyName>) // → read-only unless the policy holds
<AnyElement>(canSee: <PolicyName>) // → not rendered at all unless the policy holds<PolicyName>— a declaredpolicy, the same vocabulary page authorization (policies) uses. A name that isn't a declared policy is a compile error (with a did-you-mean).canPressapplies to pressable controls (aPressable/ button);canEditapplies to value controls (anInput) — used on a control that doesn't take the matching state, it is a compile error.canSeeapplies to any element; when the policy is false the element and its children render nothing at all.
Description#
A policy names a condition about the caller — for example "is a platform admin", or "is an owner or admin of an
organization". You already write policies to gate pages (page authorization (policies)) and entity access. canPress lets the same
policy drive a control's enabled state, so the UI and the enforced rule are one declaration.
The control is disabled until the policy is known to be true. This is deliberate and fail-safe: while the data a policy depends on is still loading, the control stays locked, then unlocks the moment the policy resolves true — it never flashes enabled first. If the policy is false, the control stays disabled.
A policy usable on a control must be self-scoped — a check about the caller's own membership or roles, such as:
policy IsPlatformAdmin => PlatformRoleGrant.Any(g => g.User == user && g.Role == PlatformRole.Admin);
policy IsOrgAdmin => OrganizationMember.Any(m => m.User == user && m.Role != OrgRole.Member);A policy that compares to data the control doesn't hold (a specific page's row, or an unrelated entity) can't be reflected on a control today and is a compile error — keeping the clean syntax honest about what it can and can't show.
Only the caller's own rows are ever loaded to evaluate the reflection; nothing about other users is read to decide whether your button is enabled.
Saying WHY a control is locked — whenDenied#
Pair a policy prop with whenDenied to explain why a control is unavailable — the sentence shows as a tooltip
while the control is locked (disabled or read-only), and is silent once the caller satisfies the policy:
Pressable(onClick: NewOrg,
canPress: IsPlatformAdmin,
whenDenied: "Only platform admins can create organizations.") {
BtnPrimary("New organization") { Icon(Icons.Plus, size: 18); }
}The message lives at the control, not the policy — the same policy can deny different actions for different
reasons. It renders to the native title attribute only while the control is locked, so a normal reader hovers to see
why an action is off, and assistive technology reads it either way.
Policy as a value#
Sometimes you don't want to reflect a policy onto one control — you want to branch the whole layout on it, or reuse
the answer in several places. A policy can be used directly as a bool value: in an if, or held in a live var.
policy CanManageApp(Application a) => RoleGrant.Any(r => r.User == user && r.Role == AppRole.PlatformAdmin);
[Composable] component AppRosterEditor(Application app) { render { Text(app.Name); } }
[Composable] component AppRosterReadonly(Application app) { render { Text(app.Name); } }
// A page that lays out differently for someone who can manage the app vs. a plain viewer:
component AppDetail(string slug) {
var app = Application.Single(a => a.Slug == slug);
render {
if (CanManageApp(app)) {
// the manager's view — an editable roster, an invite button …
AppRosterEditor(app: app);
} else {
// the read-only view
AppRosterReadonly(app: app);
}
}
}Hold the answer once and reuse it with a live var — it tracks reactively, exactly like a control prop:
[Composable] component NavLinks() { render { Text("nav"); } }
[Page("/toolbar")] [Render(CSR)]
component Toolbar() {
live var canManage = IsPlatformAdmin; // a bool, reactive to the caller's own rows
action OpenSettings() { }
render {
if (canManage) { Button("Settings", onPress: OpenSettings); }
NavLinks();
}
}Both forms lower to the same self-list reflection a canPress: prop uses — client-computed, reactive, and reading
only the caller's own rows. And they carry the same guarantee: this is render-only. Branching the layout hides an
affordance a caller can't use; it does not protect anything. The server still enforces the policy on every read and
every action, so a page that showed the manager's view to the wrong caller would still refuse every mutation behind it.
The rules match a control prop: the value must be a declared, self-scoped policy (a typo is an unknown-identifier
error, not a silently-false branch), and a parameterized policy is applied to a value the page already holds
(CanManageApp(app)). A policy is not callable in a server position — you can't put one in an entity query's
where; it is the server's own enforcement, never a query term.
See also#
- page authorization (policies) — gate a whole page or route on a policy (the enforced side).
- component — where controls and their handlers live.
- style props — the visual vocabulary a control's variants assign.