Menu QaldyDocs

Who can see which rows

Preview

A Backend table is private until a rule says otherwise. Write rules as plain sentences, open a table to visitors only by typing its name, and check them with the policy tester.

"Your database is public if you forget to turn the rules on" is one failure behind several well-known leaks: a table reachable by people who should not reach it, and nobody said so on purpose. In a Qaldy Backend a table is private until a rule says otherwise, opening one to the public takes a person typing its name, and a rule is a sentence anyone on the team can read.

The Access tab of a backend: rules written as sentences for the orders table, and a card showing what visitors, members and admins get.
The Access tab of a backend: rules as sentences, and who gets what. Sample data.

Safe by default

DefaultWhat happens
No browser originA new backend allows no website. A page from an unlisted origin is refused with origin_not_allowed. Add your site in Settings. A project made before this release keeps what it had, and Settings flags a *.
Tables start privateA new table has no rules: only servers (secret keys) and the workspace can use it.
Typed confirm to open a tableAny change that gives visitors (people who are not signed in) something they did not have, such as a new action, one more column, or more rows, needs the table's name typed back. A rule that is the same, shows fewer columns, or narrows the rows asks nothing.
A person decides, never an agentA secret key or a personal token over MCP cannot make a table public (public_needs_person), and rules, identity and settings are console-only.
qaldy_ is reservedTable names, new column names, app manifest tables and the runtime host's SQL access refuse qaldy_, sqlite_ and pragma_.
Least-privilege keysA publishable key reads. A secret key reads and writes rows but cannot change the schema unless it was given schema:write. A key can be limited to some tables.
Limits600 requests a minute per key; 120 a minute per address for a publishable key; bodies up to 256 KB; inserts up to 500 rows; failed keys and tokens 30 a minute per address.

Who is asking

CallerHow they arriveDo row rules apply?
A serverA secret key (qk_sec_)No. Limited by the key's scopes and its table list
A workspace memberA console session, or a personal token or MCPNo. Limited by the member's role and the token's scope. This is what exports use
A visitorA publishable key and no tokenYes
A signed-in personA publishable key plus Authorization: Bearer <token>Yes, as that person

A token that fails verification is 401 token_invalid, never a quiet downgrade to "visitor".

Rules are sentences

Open the backend, then Access. A rule reads like this:

text
members can see rows where owner = me
viewers can read only status and total
visitors can add only email, name and source
admins can see, add, change and delete all rows
members can change only status on rows where owner = me and archived is false
signed-in people can see rows where team = my team

The shape is <who> [can] <verbs> [only <columns>] [on|of|from] [all rows | rows [where <condition>]], in English for now.

  • Who: anyone, visitors, signed-in people, a role in the plural (members, viewers) or people with role <name>.
  • Verbs: see or read, add, change, delete, and manage for all four. only <columns> limits what is shown (read) or written (add, change).
  • Condition: comparisons joined by and, or groups joined by or (no brackets): =, !=, >, >=, <, <=, is one of a, b, is empty, is not empty, is true. A value is a word, a number, "quoted text", me (the person's id) or my <claim>. At most 8 comparisons per rule and 16 rules per table.

Nobody writes SQL. The sentence is parsed into a closed, typed rule and stored as that rule, and the sentence you read is rebuilt from it, so what you read is what is enforced.

What the rules mean

  • A person can do something only if a rule says so. No matching rule is 403 policy_denied, with the reason and an example sentence. Rules for the same people add up: the rows are the union.
  • Reads are masked per row. "Members see rows where owner = me" plus "members read only status and total" gives everyone's status and total and only their own other columns; the rest read null.
  • Changing and deleting need seeing. A person can change or delete only rows they can see, and the changed row must still match the rule, so owner = me cannot be turned into someone else's row (403 policy_violation, nothing is written).
  • Adding checks the new row against the rule; owner = me fills owner in when it is left out.
  • Servers and workspace members are not an audience: rules do not apply to them.

No oracle through the query

A column the caller cannot read at all is "no such column", in select, filters and order, the same as a column that does not exist. A column visible only on some rows cannot be filtered or sorted by (column_not_filterable), so ?email=ilike.*bob* cannot answer questions about a value the person may not see. Prefer: count=exact counts only rows in the caller's scope, and a row outside it is 404, the same as a row that does not exist. The API has no joins, embeds, or=, aggregates or full-text search; if they are added they inherit the rules.

People who sign in with your own system

Qaldy does not issue sign-ins and keeps no signing key of yours. You paste the public keys that your own server or identity provider signs with (Access, People who sign in), and the data API verifies each token with them:

  • The algorithm must be RS256, ES256 or EdDSA and the one the key's type allows. none, HS256 and anything else is refused, so a public key cannot be used as a shared secret. A pasted private key or shared secret is refused by name and never stored.
  • A kid naming one of your keys (at most 8) is required; RSA keys under 2048 bits are refused.
  • The issuer must equal the one you pinned and the audience must include yours. exp is required, not past and at most 24 hours ahead. sub is who "me" means.
  • The role comes from the claim you choose; no claim means member. Other simple claims (at most 24) feed "my team".
  • Nothing from a token is logged or echoed in an error.

To cut a person off, stop signing tokens for them (they last at most 24 hours) or rotate the key.

Try the rules before you trust them

Try the rules on the Access tab: pick a visitor, a signed-in person (id, role, details) or one of the backend's keys, an action and an optional query. A read runs the real data path and returns the rows that caller gets, with the hidden cells marked; a bypass attempt returns the data API's own refusal. A write is a rehearsal: nothing is written.

Moving an existing backend

A table that was public-read becomes the rule "anyone can see all rows" (marked "From the old public switch"); every other table has no rules. Existing secret keys keep full access, publishable keys stay read-only, and existing projects keep their origins. Nothing changes behaviour until you change it.

Known gaps

  • Rules and warnings are written in English; the console's Russian and Kazakh translate the screens, not the sentences.
  • Hiding by row is exact. Hiding by column uses null, so a masked cell and a real null look alike.
  • A rule change reaches other API processes within 10 seconds (at once on the one that saved it).
  • Limits and caches are per API process. The pilot runs one.
  • The sample backend in the demo ships with rules so the tab is not empty; a real backend starts with none.

Type to search. Up and down to choose, Enter to open, Esc to close.