# Concepts (/en/docs/developers/database/concepts)



## What Database is for [#what-database-is-for]

Database is a headless content store: people, agents and scripts read and write structured tables and rows directly through the public API, as with a headless CMS. It holds content such as itineraries and catalogs, independently of the applications that use it. It is neither an application's private runtime database nor a data warehouse.

| Role                      | Who uses it                                                              | Lifecycle                                                                 | Where to store it                                                                   |
| ------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Content store             | People, agents and scripts; applications can read it through public APIs | Content is edited and reused independently of application releases        | Database for structured content; File for files; Knowledge for searchable knowledge |
| Application runtime store | Application code managing orders, sessions and business rules            | State changes as the application runs, under its own business constraints | The application's own `d1`, `kv` or `r2` components                                 |
| Data warehouse            | Analysts and reporting pipelines                                         | Historical data accumulated for analysis                                  | A dedicated analytics warehouse, outside Database                                   |

A typical pattern is **content store → build → publish to Application**: read itinerary content from Database, build site files, then use [`praxis.application.publish`](/en/docs/developers/application) to upload, version and deploy them. Editing the source content does not update those published files; build and publish again to include the edits.

A database is an isolation unit. Tables define record structure, fields define value types, and rows hold the records.

## Databases, tables, fields and rows [#databases-tables-fields-and-rows]

Your API Key determines which databases you can access. Tables and fields accept IDs or names. Links use table IDs, so renaming a table does not change the link target. Database labels support categorization and filtering.

Default rows contain only the primary key `id` and user fields, including `select=*`, `return=representation` and RPC rows. Only `id` is a reserved field name; new fields cannot start with `_`. `created_at`, `updated_at` and `version` are ordinary field names. Field names start with a lowercase letter, followed by lowercase letters, digits or underscores, up to 63 characters; table names also allow a leading underscore. Existing underscore user fields remain readable and writable and take precedence over same-named metadata, making that metadata inaccessible in the table.

System metadata is read-only and opt-in: use `select=*,_version,_created_at,_updated_at`. Star never expands it. Use `_version=eq.3` in filters or `order=_created_at.desc` for sorting. Without `order`, rows still sort by internal creation time, then `id`. Old metadata names in filters/order without a same-named user field return 400: `details` says `use _created_at` (or the corresponding name), and `hint` says `system metadata moved to underscore columns`.

A datetime field can use `{"name":"created_at","type":"datetime","default":"now()"}`. Omitting it on insertion stores current server UTC time; the field definition continues to show `"now()"`. Explicit null and omitted PATCH fields do not use defaults. String/json defaults of `"now()"` remain literal strings. Adding this field to an existing table evaluates the default for the backfill too.

## Seven field types [#seven-field-types]

| Type       | Value                                                                                                                                                                                                                                                                                            |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `string`   | A string                                                                                                                                                                                                                                                                                         |
| `integer`  | An integer                                                                                                                                                                                                                                                                                       |
| `number`   | A number                                                                                                                                                                                                                                                                                         |
| `boolean`  | `true` or `false`                                                                                                                                                                                                                                                                                |
| `datetime` | An RFC 3339 date-time such as `2026-10-11T08:30:00+08:00`; impossible dates (such as February 30) return 400. Values are stored and read back in UTC with three fractional digits, such as `2026-10-11T00:30:00.000Z`; precision is milliseconds and extra fractional digits are rounded half up |
| `json`     | A JSON value satisfying the supplied JSON Schema                                                                                                                                                                                                                                                 |
| `link`     | A row ID, or multiple row IDs, in another table in the same database                                                                                                                                                                                                                             |

`required` requires a non-null value. A new field with a `default` fills every existing row with it; a new field without one is null on existing rows. Adding a `required` field to a table that already has rows needs a `default`; making an existing field `required` returns 400 `VALIDATION_FAILED` with the number of empty rows while any row is null in that field, so fill those rows first. `unique` requires unique values. The first five types are indexed by default; set `indexed: false` where unnecessary. `json` and `link` cannot be indexed. A field's type cannot change after creation.

## Batch writes and transactions [#batch-writes-and-transactions]

One POST inserts at most **500 rows**. Each request is atomic: a failure in any row rolls back the entire batch. There are no transactions across requests. Put rows that must succeed together in one batch POST. For links, query the target first, then insert the referencing rows. Querying first does not prevent concurrent changes; still handle a missing-link error at insert time.

Rows in a batch are filled to the union of their keys with `null` (PostgREST behavior), so omitting a required field in one row can reject the whole batch. Send `Prefer: missing=default` to use field defaults for missing keys. Explicit `null` remains null, and required fields without defaults must still be provided.

The default `return=minimal` returns 201 with an empty body. Send `Prefer: return=representation` to read back inserted rows, or chain `.select()` after supabase-js `.insert(...)`.

## JSON Schema [#json-schema]

A `json` field requires JSON Schema in `options.schema`. For example, this field only accepts objects containing a string `summary`.

```json
{
  "name": "detail",
  "type": "json",
  "options": {
    "schema": {
      "type": "object",
      "properties": { "summary": { "type": "string" } },
      "required": ["summary"],
      "additionalProperties": false
    }
  }
}
```

Schema updates constrain subsequent writes without rewriting existing rows. Use `detail->>summary` to extract its string value in queries.

## Links and deletion protection [#links-and-deletion-protection]

A link's `options.linked_table_id` points to a target table in the same database. `multiple` selects a single row ID or an array of row IDs. The target cannot change after creation.

```json
{
  "name": "project",
  "type": "link",
  "options": { "linked_table_id": "tbl_example", "multiple": false }
}
```

Replace `tbl_example` with an existing table ID. References to nonexistent rows are rejected. Deleting a row that other rows still reference is also rejected; rows that reference each other can be deleted together in one delete. Remove references before deleting the target; deletion does not cascade automatically.

## Use \_version to avoid overwriting another update [#use-_version-to-avoid-overwriting-another-update]

A new row starts at `_version: 1`, increasing on every update. Filter by both the row ID and the \_version you read. An empty result means the condition no longer matches; read again before deciding what to do next.

```typescript
const { data: row, error: readError } = await supabase.from("tasks")
  .select("*,_version").eq("id", rowId).single();
if (readError) throw readError;

const { data, error } = await supabase.from("tasks")
  .update({ done: true })
  .eq("id", row.id)
  .eq("_version", row._version)
  .select();
if (error) throw error;
if (data.length === 0) throw new Error("Read the row again before updating");
```

## Restore a deleted database within 7 days [#restore-a-deleted-database-within-7-days]

After deleting a database, its rows cannot be read or written. [Restore the database](/en/docs/api/native/database/databases#restore-database) within 7 days to use it and its retained tables and rows again. After that window, recovery is unavailable. Recovery applies to the whole database, not individually deleted tables, fields or rows.

[API reference →](/en/docs/api/native/database) · [Limits and errors →](/en/docs/developers/database/limits-and-errors)
