Summary#
An entity with no security { } block is denied to every user request — access is granted only where you
write it. This is the secure-by-default posture: you cannot forget to lock down a table, because an unmentioned
table is already locked. It is not a setting you turn on and there is no way to turn it off, so there is nothing to
write and nothing to remember: every app is deny-all, every entity starts closed, and the only question left is
which access you grant back.
// Deny-all is the default — no security block → denied to every user request.
entity Ledger { string Entry; }
// Grant access back explicitly — anyone may read the catalog.
entity Product {
string Name;
security { allow read when IsAuthenticated || IsAnonymous; }
}Signature#
entity Ledger { string Entry; } // no block → denied to every user request
security { allow read when IsAuthenticated; } // a block → a LIST OF GRANTS, and the only way inThere is nothing app-level to write. The posture is a property of the language, not a configuration value, so it
cannot be relaxed for an app, a file or an entity — the only lever is which grants you write in a security { }
block. The one thing that genuinely has to run before anyone is signed in — login, signup, a password reset — is
declared as an auth-bootstrap and runs under an identity you defined, on a leash of the
grants you gave that role.
Description#
The posture changes only what a user request may touch. It does not change how the platform runs itself:
- User requests (a logged-in or anonymous caller reading/writing app data) are subject to the posture. An entity
with no
security { }block is denied; an entity with a block is governed by exactly what that block grants. - System, designer, and bootstrap work (schema evolution, seeding, the metadata designer) is never affected — it does not run through a user's security context, so the posture never denies the platform's own operations.
- Platform (
osy.*) entities are exempt — their access is gated by other layers, so the posture never denies a built-in read.
Grant access back with ordinary security { } rules — allow read where Owner == user;, a role check,
or one of the built-in principal predicates (IsAuthenticated / IsAnonymous) and open reads (allow read when IsAuthenticated, when IsAnonymous, or
when IsAuthenticated || IsAnonymous for a genuinely public read).
Every
allowmust say WHO — a rule with nowhenand nowhereis a compile error. Deny-all decides what happens when you say nothing; this decides what happens when you open a block and then forget to qualify a line inside it. An unqualifiedallowgrants that verb to every caller, anonymous included, and the runtime consults nothing — so it is not a weaker rule, it is the absence of one wearing the syntax of one, and it is worse than no block at all because deny-all would have caught the omission. It applies to all four verbs and to every app, whether or not it declares a[Principal].The remedy is never to remove the grant — it is to name who holds it:
you mean you write the owner of the row allow update where Owner == user;anyone signed in allow read when IsAuthenticated;signed-out visitors (signup, a public form) allow create when IsAnonymous;genuinely everyone allow read when IsAuthenticated \|\| IsAnonymous;That last row is supported and is the right answer for a store front — "everyone" is a legitimate decision, and spelling it out is the whole point. What is refused is leaving it unsaid.
when trueis a compile error for the same reason: it says yes without saying who, so a reader cannot tell a public surface from an unfinished one. The diagnostic names the form that does mean everyone.An entity-level
denymust say WHEN as well. "Denies more, never less" is the reasoning of an allow-by-default system; under deny-all there is nothing broader to narrow. A baredeny update;on an entity is either dead text — the verb was already denied — or, when a siblingallownames the same verb, a silent kill switch: an unconditional deny is checked FIRST, so it cancels the grant written above it. Both readings are worse than deleting the line, which is the same argument that refusesdefault deny;.A FIELD-scoped
denyneeds no condition — except on a CREDENTIAL.deny read PasswordHash when !IsAuthenticator;subtracts one column from whatever the entity grants, because property rules REPLACE the entity's rather than layering on them — it is the only way to say "nobody, ever", and nowhenstates it as cleanly.A
partial entityis exempt too, since narrowing a declaration made elsewhere is the whole reason it exists.There is no way out, and that is the point. Deny-all holds for every app, always — so "we will turn security on before we ship" is not a state this platform can be in, and an app that appears to work is an app whose grants you actually wrote. The only sanctioned path past a locked table is the one that must exist: login and signup have to reach a user row before a principal exists, so you declare them in
app.AuthBootstrapand the engine runs them as an ephemeral principal bearing a role you chose — which may touch exactly what yoursecurity { }blocks grant that role, and nothing else. See auth bootstrap (login, before anyone is signed in), and page authorization (policies) for page-level authorization, the request-time complement to entity-level deny-all.
Examples#
A public catalog an anonymous visitor may browse, alongside a private table only an owner sees — under the posture,
the untouched Ledger is denied to all:
[Principal] entity User { string Name; }
entity Product { // anonymous visitors may browse the catalog
string Name;
security { allow read when IsAnonymous || IsAuthenticated; }
}
entity Order { // a buyer sees only their own orders
User Buyer;
string Item;
security { allow read where Buyer == user; }
}
entity Ledger { string Entry; } // no security block → denied to everyone (the default)See also#
- security { } — the
security { }block that grants access back - rows that are part of another row — the exception: a row that is PART OF another takes that row's rule, with no block of its own
- principal predicates (IsAuthenticated / IsAnonymous) and open reads —
IsAuthenticated/IsAnonymous, and why everyallowmust say who - page authorization (policies) — page-level authorization, the request-time complement to deny-all