Audience and bindings
How audience tiers and data/logic bindings combine into a default-deny permission model for Apps.
An App's access to data is governed by two independent controls that both have to allow a request: the audience tier a visitor falls into, and the bindings that expose specific collections, fields, and workflows. Both default to denying access.
Audience tiers structurally limit what's possible
Audience isn't just a login gate — it also caps what operations are ever allowed, regardless of how a binding is configured:
- Just me — only you, the author, can open the App at all.
- Team — any workspace member can open it, with whatever operations their binding allows.
- Invited — people you've specifically invited sign in with their Cheqq account through a branded auth screen; visitors get an
owner,member, orguestrole depending on how they were invited. - Public — anyone can open it without signing in. Update and delete operations are never allowed on a Public App, no matter what a data binding's
accesslist says — public visitors are structurally capped at read (and insert, if explicitly bound).
Data bindings
A data binding connects one collection to the App and defines exactly what's reachable:
- Operations — an allow-list of
read,insert,update,delete. Only the operations listed are permitted (subject to the audience cap above). - Fields — an explicit allow-list of field names. This is not a hide-list — it's the complete set of fields the App can ever see or write. A binding has to name at least one: an empty list is refused when you configure it, since an App bound that way could reach nothing and would fail the first time a visitor used it.
- Fields a visitor may change (optional) — which of those fields the App may write. The rest stay read-only: the App can show them but never set them. Without this list, every field the App can see can also be written.
- A server-enforced filter (optional) — restricts which rows the binding can touch (e.g. only records belonging to the visitor). This filter can't be widened by anything the App's frontend sends; it's applied on the server for every request.
Example
Binding a Contacts collection with fields: ["name", "company"] and access: ["read"] lets the App display a contact's name and company — the contact's email, phone, and every other field stay invisible, even though the collection itself has them.
If a request doesn't match any of these, it's denied with a specific reason (useful if you're debugging an integration against the App's public API): no binding exists for that collection, the operation isn't in the binding's allow-list, the audience forbids the operation, no fields are exposed at all, or the specific field requested isn't in the allow-list.
Reading through a binding
A read binding supports three shapes, all of them inside the binding:
- List the bound collection.
- Fetch one record by id, so rendering a single page doesn't mean paging the whole collection to find it.
- Narrow a list with
?filter.<field>=<value>.
Every record carries an updatedAt, so a site built on the API has something to
hang a lastmod or dateModified on.
A caller's filter can only narrow
Filtering never widens what a binding exposes. Filtering on a field the binding doesn't expose is rejected outright; the binding's own filter always merges last, so no caller value can displace it; and a filter that fights the binding (asking for drafts against a published-only binding) returns an empty page rather than the binding's rows.
Fetching by id re-checks the binding's filter against the row it loaded, and a row that fails it is reported as missing, not as forbidden — otherwise the id space becomes a way to probe for records the App is meant to hide.
Records that belong to the visitor
A binding can hold a collection to the signed-in visitor: only the records whose email field matches the email of the visitor's Cheqq account. This is how a portal shows each client only their own projects, or each resident only their own lease.
- Reads return only that visitor's records.
- A record they add is stamped with their email, whatever the request says, and they can't change it afterwards.
- Updates and deletes reach only their own records.
- A request with no signed-in visitor is refused, so this only works on an App people sign in to, never a Public one.
A binding can also let through only records in certain states, such as invoices that are Sent, Paid or Overdue. Combined with the visitor match, a client sees their invoices once they go out, and a draft stays yours even with their email already on it. A state added later stays hidden until you add it to the list.
A visitor's changes stay inside the binding too. A record they add gets the fixed values filled in for them, a field limited to a list of values must be one of the list, and an edit can't move a record out of what the binding shows: change it to a value the binding doesn't allow and it's refused, with the record unchanged.
Limits on what visitors send
A Public or Invited App accepts up to 500 new records a day. Past that, it's told "This app has taken all the new submissions it can for today. Please try again tomorrow." Every field has a length limit, from 320 characters for an email to 20,000 for long text, and one submission can't be larger than 1 MB.
Wider access needs your yes
A change that would let an App reach more than it does now, such as dropping the visitor match above or opening the App to more people, waits for a person to approve it, even in Auto mode. See Cheqq asks before an App reaches more.
Bindings in the App's code
The App's code reads and writes your data through a module generated from its bindings on every build. A field or operation that isn't bound doesn't exist there, so code that reaches for one fails the build — the App Builder hears about it before a visitor ever could.
Logic bindings
A logic binding connects a workflow's webhook trigger to the App, so the App's frontend can invoke that workflow directly — this is the only way an App can trigger a Wapp workflow. There's no equivalent for other trigger types (schedule, email, collection change); those keep firing independently of the App.
Continue with Branded auth and domains for how the sign-in screen works.