NAS: one document that IS the app
Every Nibor app is a single JSON document, the Nibor App Spec. Models, pages, permissions, navigation, automations and theme live in one place, and everything else (database, API, UI) is compiled from it.
Why one document
- The AI always sees the whole app. Chat edits are made against the full spec, never a partial view, which is why targeted changes do not break unrelated pages.
- Security is compiled, not hand-written. Row-level security policies, tenant isolation and per-role access rules are generated from the spec for every model, uniformly.
- It is portable. The spec ships with every export; the same document can be re-compiled on your infrastructure.
What is inside (excerpt)
{
"app": { "name": "Warehouse Stock Manager", "description": "..." },
"models": [
{ "name": "Product",
"fields": [ { "name": "sku", "type": "text", "required": true }, ... ],
"access": { "read": "authenticated", "write": "staff" } }
],
"views": [
{ "route": "/", "title": "Dashboard", "layout": "dashboard", ... }
],
"appAuth": { "audience": "org", "roles": [ ... ], "profilesVisibility": "own" },
"storage": { "rules": [ ... ] },
"scheduledJobs": [ { "name": "digest", "kind": "digest", "schedule": "*/10 * * * *", "digest": { ... } } ],
"emailTemplates": [ ... ],
"dbViews": [ ... ],
"automations": [ ... ]
}
Who the app is for: appAuth
audience:org(your organisation's members sign in with their Nibor account, provisioned on first login),invite(admin-issued invites only),public(self-serve signup, optionally limited byallowedDomains) oranonymous(no accounts). This decides which pages exist and who becomes a member.profilesVisibility: whether members may read each other's profile rows (members) or only their own plus staff (own, the default for public apps so customers never see each other).identityLogin: sign-in with an eID through an OpenID Connect provider (BankID through Idura Verify). Names the customer model and the fields that hold the personal number and the email; the proven number is matched to that row and the session carries its email. Staff roles are never admitted this way.
Security you declare
- Every model has an access level; the compiler turns it into row-level security. When the rule is membership-shaped ("members of the channel", "assignees of the case"), declare
rls.helpers(small SQL functions) andrls.policiesthat call them with{{fn:name}}. storage.rules: who may read, upload and delete files under a path such as{{orgId}}/cases/{{caseId}}, expressed with the same helpers;saas-file-uploaddeclares which rule it uses.dbViewswithpublic: true: a read-only projection anonymous visitors may see (a catalogue, availability), granted explicitly instead of opening the table.
Things that run on their own
scheduledJobs: cron-scheduled server functions. A job withkind: "digest"needs no code: the platform writes it from the config (which model holds the unread items, which fields are title, body and link, which per-user setting turns mail off) and mails each person their unread items, marking them as sent.automations: on-create, on-update and schedule triggers with actions;emailTemplates: the mails your functions and jobs send, with a built-in fallback for digests.externalRefon a field: this column carries another system's key for a company entity; the organisation map (ONAS) reads it back.
Live data
Any dataSource can declare "realtime": true. Components bound to it (tables, lists, feeds, charts) refresh the moment a row is inserted, updated or deleted, without a page reload. The stream is filtered by the same row-level security as every query, so a member only ever receives rows they could have read anyway. Ask the Studio chat to make a page live, or set the flag in the document and push it.
"dataSources": [{ "name": "messages", "model": "Message", "order": "createdAt.desc", "realtime": true }]
Forms and functions
- A form inserts its row itself (
action="create") or posts its fields to a server function:<saas-form model="Project" action="fn:createProject">. A wizard does the same at its last step withcomplete-action="fn:name". Use the function when creating the record needs logic (a membership check, a derived field, a number series). - What a person types is content. A contract template reading "between {{counterparty}} and us" saves as typed; only a token the document itself writes into a value attribute (
value="{{user.id}}") is resolved. Date tokens resolve wherever user tokens do:{{today}},{{tomorrow}},{{weekStart}},{{weekEnd}},{{monthStart}},{{monthEnd}},{{yearStart}},{{now}}. - A select takes its options as a comma list, as
value:labelpairs or as a JSON array of{ value, label }objects; the stored value is the value part. - A model with
"create": "none"gets no create page; its rows arrive through a function or a job. A model a plain form creates whose own policies allow no INSERT gets aninsert_ownpolicy (the row's user column must be the caller, else a member). - A function's
okmeans its own write succeeded. An optional step after the save that fails (geocoding, an email, an AI classification, a provider that is not connected) comes back as awarningon a successful result, never as a failure, and never undoes the save.
What changed
- 12 October 2026. Forms and wizards post to functions (#353). Typed braces are content (#353). Select options in three forms (#352). A model a form creates gets an INSERT policy when its policies allow none (#353). No create page for create-locked models (#356). A function's ok is its own write (#356). Date tokens (#346). A declared model the build dropped is added before pages are synthesised (#356).
- 11 October 2026. Build status comes from compile results, the migration and the plan (#315). Database views are created in dependency order, after the auth helpers (#318). Open APIs become working integrations and nothing is simulated (#327). ctx.ai (#329). The browser check runs on every build and every page-changing chat turn (#330). Versions carry the data (#332).
Working with it directly
The Studio's NAS Editor shows the live document; Code View shows what it compiles to. Most people never need either, but nothing is hidden.