# Backend Tables with a REST API for your own app or site. Make a table, make a key, call it from a browser or a server, and watch every request land. Keys are shown once and revoked at once. URL: https://docs.qaldy.com/guides/backend/ Qaldy can be the backend of a product of your own: tables with a REST API, a key for a browser and a key for a server, request logs and the load on your island. A backend project is an **app** like any other: it has an owner, sharing and an audit trail, and a building in the City. :::preview Preview The data API runs in the pilot cell. Limits and behaviour can change before there is a version promise. Rows live in a database of their own for each project. A table is **private until a rule says otherwise**: see [Who can see which rows](https://docs.qaldy.com/guides/backend-access/). Backups of project data are not a feature yet: export a table (CSV or JSON) to keep a copy of data you cannot lose. ::: ## Make a table Open **Backend** in the console, create a project, then a table. Every table has an `id` (an integer, the primary key) and `created_at` (set by the database). You add columns: | Type | Stored as | |---|---| | `text` | text | | `integer` | a whole number | | `real` | a decimal number | | `boolean` | true or false | | `timestamp` | an ISO 8601 date and time | | `json` | text, returned parsed | Changes from the API are additive: create a table, add a column. Dropping a table needs a person in the console, typing the table's name back. You can import a CSV (up to 5,000 rows, typed by the columns) and export a table as CSV or JSON. ## Make a key | Key | Starts with | What it can do | Where it goes | |---|---|---|---| | **Publishable** | `qk_pub_` | Read the tables a rule opens to visitors, and their schema | A browser or a mobile app | | **Secret** | `qk_sec_` | Read, insert, update and delete rows in this one project. Changing the schema needs the `schema:write` scope, which is opt-in. A key can be limited to some tables | A server or a worker. Never a browser | A key is shown once, then only its prefix and last four characters. It is stored as a keyed hash, so Qaldy cannot show it again; make a new one and revoke the old. A revoked key stops working at once. Each key has a rate limit (600 requests a minute by default): a refusal is `429` with `Retry-After`. A publishable key is also limited per address (120 a minute by default), a request body may be at most 256 KB by default (1 MB at the most), and wrong keys and wrong tokens are slowed to 30 attempts a minute per address. A publishable key answers a browser only from the project's **allowed origins**, and a new project has none: add your site's address in Settings, or the browser is refused with `origin_not_allowed` and no CORS headers. A secret key never gets cross-origin headers for another site. ## Call it The base address is `https://cloud-api.qaldy.com/v1/projects/{projectId}`. Send the key as `Authorization: Bearer ` (or an `apikey` header). ```sh # The schema (a caller sees only the tables and columns the rules let them use) curl -s https://cloud-api.qaldy.com/v1/projects/$PROJECT/schema -H "Authorization: Bearer $KEY" # Open orders, newest first, 20 at a time curl -s "https://cloud-api.qaldy.com/v1/projects/$PROJECT/rest/orders?status=eq.open&order=created_at.desc&limit=20" \ -H "Authorization: Bearer $KEY" # Insert one row (a secret key); an array inserts up to 500, all or nothing curl -s -X POST https://cloud-api.qaldy.com/v1/projects/$PROJECT/rest/orders \ -H "Authorization: Bearer $SECRET" -H "Content-Type: application/json" \ -d '{ "status": "open", "total": 12.5 }' ``` | Request | What it does | |---|---| | `GET /schema` | Tables and typed columns | | `GET /rest/{table}` | List rows with filters, `select`, `order`, `limit`, `offset` | | `GET /rest/{table}/{id}` | One row | | `POST /rest/{table}` | Insert one object or an array (a secret key) | | `PATCH /rest/{table}/{id}`, or `PATCH /rest/{table}?filters` | Update one row, or every row that matches at least one filter | | `DELETE /rest/{table}/{id}`, or `DELETE /rest/{table}?filters` | Delete one row, or every row that matches at least one filter | **Filters** follow a subset of PostgREST: `?status=eq.open`, `neq`, `gt`, `gte`, `lt`, `lte`, `like` and `ilike` (with `*` as the wildcard), `in.(a,b)`, `is.null`, `is.true`, `is.false`. `limit` is at most 1,000 (100 by default). `Prefer: count=exact` adds a `Content-Range: 0-19/57` header. A PATCH or DELETE with no filter and no id is refused. The API never builds SQL from text. A query string becomes a typed operation, identifiers must match the declared schema and every value is a bound parameter. Errors use the [standard error shape](https://docs.qaldy.com/reference/api/#errors). Every route is in the [Backend data API reference](https://docs.qaldy.com/reference/api/backend-data-api/). ## Watch it Every data call is logged, refused ones included: method, path, table, status, latency, which key and how many rows. The console shows the last 2,000 per project, requests per minute and p50 and p95 latency over the last hour, and the datacenter building in the City spins faster with load. Creating a project, making or revoking a key, creating a table, adding a column, dropping a table, changing settings and importing a CSV are [audit rows](https://docs.qaldy.com/concepts/audit/). ![The Backend screen with the Connect a client dialog open: the address and a snippet for the client you pick.](https://docs.qaldy.com/screens/backend-mcp.webp "Backend, then Connect a client, puts the address and a ready snippet in front of you. Sample data.") ## From an agent An agent with a token can list backends, read and insert rows and create tables through [MCP](https://docs.qaldy.com/mcp/tools/) (`list_backend_projects`, `query_table`, `insert_rows`) or the [command line](https://docs.qaldy.com/cli/commands/) (`qaldy tables`). Rows come back as data, never as instructions to the model. Keys and dropping tables stay with a person in the console.