Data Permissions
The supported data boundary is AuthorizedCollection. It combines the caller's Mongo-style filter, exact scope fields, persisted policy where, and field permissions before touching MongoDB.
It is not a transparent MonSQLize collection proxy. It is permission-core's protected data facade: callers provide a safe, auditable, cost-bounded query subset. If a workflow needs the full MonSQLize expression surface, keep that work explicit in the application repository layer instead of passing arbitrary query objects through an authorized collection.
Use this mental model first:
filter and where Have Different Jobs
The caller filter is a safe Mongo-style business query for one operation, such as { status: 'paid' }; it is not the complete MonSQLize query syntax. Persisted where belongs to allow or deny policy rules, and scopeFields maps trusted scope dimensions to exact scalar fields in each business document.
pc.forSubject(...).data.collection(...) synchronously creates an AuthorizedCollection. It binds the current subject, physical collection orders, logical resource db:orders, and scope field mapping. It does not touch the database when created; real MonSQLize reads and writes happen later through methods such as find, findPage, updateOne, and deleteMany.
scopeFields: { tenantId: 'tenantId' } does not hard-code the tenant value to tenantId, and it does not write a tenant value. The left tenantId means subject.scope.tenantId; the right 'tenantId' is the field path inside each business document. When the current subject scope is { tenantId: 'acme' }, every real Mongo operation also requires the document tenantId field to equal acme.
If you write scopeFields: { tenantId: 'acme' }, permission-core maps subject.scope.tenantId to the document field named acme. That is only meaningful when your documents really contain an acme field; it is usually not the intended tenant mapping.
In this example, orders.find({ status: 'paid' }) is logically close to:
The real implementation also adds scalar scope guards, field permission checks, deny inversion, transactions, and query budgets; this snippet is only the mental model for permission composition.
The public API does not return a filter and ask the caller to remember to use it. The collection executes the combined conditions directly.
It also does not accept persisted functions such as rows: (subject) => .... Functions cannot be serialized, audited, replayed across processes, or version-compared reliably. Put application-specific computed values into trusted claims or request context, then reference them with valueFrom.
Multiple Policy Conditions
Use serializable all, any, and not nodes to compose policy conditions:
Leaf operators include eq, ne, in, nin, gt, gte, lt, lte, contains, and exists. valueFrom can read trusted subject, claims, or explicit policy context. Missing dynamic context evaluates to unknown and tightens authorization instead of widening it.
Mongo-Style Caller Queries
Caller filters use SafeMongoFilter. They support bounded pure-data Mongo operators including $and, $or, $nor, comparison and set operators, $exists, literal $regex with optional i, $not, $elemMatch, $all, and $size. JavaScript predicates, Proxy values, accessors, $where, and arbitrary operators are rejected.
Safe filters are capped at 12 levels, 256 nodes, 32 children per logical node, and 128 KiB of normalized bytes. Treat them as "Mongo-like authorized query input", not as query objects passed through unchanged to the underlying MonSQLize collection.
Field Permissions
Once field rules exist, every projected, filtered, sorted, or modified field must be authorized for the corresponding operation. That prevents callers from inferring hidden values through filters or sort order.
This is the raw array returned by orders.find(). The field roles.allow() calls each return their own mutation envelope; the example omits those responses because this section focuses on the read result. Production initialization should still check write failures.
projection: ['publicValue'] is the caller's requested field set. The final result is still tightened by field allow/deny rules. The status field used by the filter also needs read permission even when it is not returned in the projection.
Protected Read and Write Operations
The facade supports find, findOne, count, findAndCount, signed-cursor findPage, insertOne, updateOne, updateMany, deleteOne, and deleteMany. Inserts validate the authorized post-image and inject trusted scope fields. Updates check both pre-image and post-image, including field rules and scope stability.
Signed cursor pagination is available directly through findPage(). The first request provides the business filter, stable sort, and page size; the next request submits the cursor returned by the previous page:
The cursor binds the query contract, scope, subject, claims/context fingerprints, and policy revision. Changing user, scope, filter/sort, or tampering with the cursor prevents reuse. total is returned only when totals: true is requested.
This is the complete business result object from updateOne(). matchedCount=0 means the authorized combination found no candidate. Trying to modify unauthorized fields or scope fields throws explicitly instead of silently returning 0.
Transaction and Ownership Boundary
Every operation uses a real MonSQLize transaction. A borrowed MonSQLize Transaction must belong to the same runtime; ownership remains with the caller, and permission-core does not commit or roll back borrowed transactions. The physical collection name belongs to application configuration, while logical resource is the authorization contract.
Continue with Manage Menus.