get by ID, an index range, a
search, a count. You declare it on the table with .access and write it as an
ordinary query that answers the reads Bijection asks it about.
bijection/schema.ts
taskRead rule. The rest
of this page describes what a rule receives, what it returns, and what callers
see when it refuses.
Declaring a read rule
.access takes an object with two required fields:
read: a reference to the rule, a query exported from a module of the same component. It can be public or internal; an internal query keeps clients from calling it directly.reads: the tables the rule may read, its policy inputs. List every table the rule queries, including the protected table itself if the rule looks up the row it’s deciding.
.access can be declared on a table or on a view. A table
has at most one read rule; several tables can share the same rule.
When you push your code, Bijection checks each rule. A deployment is refused
when the rule isn’t an exported query, when its args aren’t
readAccessArgs, or when its returns isn’t readAccessResults.
What the rule receives
A rule receives a batch of requests: every read of the protected table that the calling function made and that has to be decided together.tableis the table being read.targetis what is being read:objectis one document, named by its ID, asctx.db.getreads it.queryis a range or scan.equalitieslists the fields the query’s index range pins with.eq(...). They are guaranteed index bounds, not a guess from.filter(). An equality without avaluemeans the field is missing;nullis an explicit value. A query with no equalities is a read of the whole table.
propertiesis the list of properties the read uses, including those that decide membership, filtering and order, and_idand_creationTime.nullmeans the whole document.
object request, so a rule
can decide a range and still look at each row.
What the rule returns
A rule returns one result per request, in the same order:
To refuse, throw. A throw refuses the whole batch.
Bijection refuses the batch when a result is missing, when there are too many,
or when one isn’t a permission. A rule that returns early or falls off its end
therefore refuses instead of permitting reads it never looked at.
decideEach(requests, decide) calls decide for each request in order and
collects the results, which is the easiest way to get this right. A rule that
decides the whole table at once, for example “administrators read everything”,
still answers each request:
bijection/access.ts
Time-limited permissions
A rule that reads the clock must say how long its answer holds. Return{ validUntil } for each permitted request; a rule that observes the time and
returns null is refused. validUntil must be in the future and at most 366
days ahead.
Reasons
A reason is a string of at most 256 bytes. It never reaches the caller: it is written to the audit log when the table is audited. To give the reason for a refusal, throw aBijectionError:
Policy inputs
While the rule runs, it can read the tables listed inreads and nothing
else. Reading any other table fails with
Policy input is outside its admitted scope. The list holds at most 32 tables,
and each must be an ordinary table of your schema, not a view.
Policy inputs are private to the rule. The caller doesn’t need permission to
read them, and nothing the rule computes from them reaches the caller: a
refused read reports Read access was refused, whatever the rule threw, and
the rule’s console output is not shown to the caller.
What callers see
A read rule never changes the data it permits. Refused reads behave as follows:- An index range in a query leaves out the rows the rule refuses and
returns the rest, as if the refused rows didn’t exist. The rule still has to
permit the range itself: if it refuses the range’s
queryrequest, the whole query is refused. - Every other refused read refuses the whole function with
Read access was refused. That includesctx.db.get, reads of a whole table, searches and counts, and every read inside a mutation. - A refusal never redacts. If a rule refuses a property, the read of the document that includes it is refused; Bijection doesn’t return the document with the property removed.
taskRead does, answers a hidden,
a deleted and a never-existing document the same way, so a refusal doesn’t
reveal whether an ID exists.
Reading several documents by ID
getMany from bijection/server reads up to 100 documents of one table and
answers each ID on its own. In a query, a refused document answers
restricted while the others are returned:
bijection/tasks.ts
absent is only reported to a caller the rule permits to read the whole
table; for anyone else a missing ID is restricted. In a mutation, getMany
behaves like a series of ctx.db.get calls, and one refusal refuses the whole
mutation.
Reading fewer properties
When a caller may read only some properties, ask for only those with.select. The rule then receives just the selected properties plus the ones
the query uses to find and order rows:
bijection/customers.ts
Reactivity
The reads a rule makes are dependencies of the query it decided. When a membership row changes, Bijection reruns the subscriptions whose decision read it: a live query that was refused becomes permitted after a grant, and a permitted one is refused after a revocation. A refusal carries no expiry of its own. If access should begin at a later time, make that a committed change, for example with a scheduled function that inserts the membership.Scheduled functions and cron jobs run without a user identity, so inside a
rule
ctx.auth.getUserIdentity() returns null for their reads.Large ranges
Each function can carry at most 128 decided reads of protected tables, and every returned row counts as one. A function that reads more rows than that from protected tables fails withReadAccessBounds.
When your rule decides a whole range the same way for every row in it, declare
that with rowsFollowRange. When deciding the range’s rows one by one would
exceed the bound, Bijection keeps one decision for the range instead:
bijection/schema.ts
field. The range
must pin field with an equality. The rule then receives a query request
with covers: true, and its answer decides every row the range covers. A text
or vector search whose filter pins the same field searches that range’s rows
only.
If rows in the range carry their own labels that the rule checks, name that
field as markings. Bijection then keeps one decision per distinct value of
that field within the range.
rowsFollowRange is a promise you make about your rule. In tests and
development builds Bijection checks it: when the rule permits a covering
request, every row it covers is decided too, and a row the rule would refuse
fails the function with RowsFollowRangeViolated. A release build trusts the
declaration unless BIJECTION_CHECK_DECLARED_RANGES is set.
Auditing decisions
Addaudit: true to record every decision of the table’s rules in the
deployment’s audit log: each read
its read rule decides, each release its disclosure rule
decides and each change its write rule decides.
bijection/schema.ts