Admin UI
[0022]The dashboard ships inside the binary.
The admin app is plain HTML, CSS and JavaScript, baked in at compile time with include_str!. rpocketbase serve needs no frontend build and no second server. The script files are joined in a fixed order into one scope, without a bundler.
- Path
/_/, with/adminas an alias- Built
- Embedded at compile time, served from memory
- Scripts
script-src 'self'with no inline script- Covers
- Collections, rules, API keys, backups, system health, and the API docs at
/_/#/docs
Realtime (SSE)
[0020]One long-lived request, fanned out in process.
A client opens a single request to the realtime endpoint and keeps it open. The first message gives it a clientId, and record changes stream in after that. The hub that fans events out lives inside the process. That keeps it simple and fast, and it also makes rPocketBase a single instance, not a cluster.
GET /api/realtime
event: PB_CONNECT
data: {"clientId": "…"}
- Keep-alive
- Every 15 seconds
- Auth
EventSourcecan't set headers, so it uses a short-lived ticket:POST /api/realtime/ticket, then?ticket=.?token=is rejected- API keys
- A key's collection allowlist applies to events too
- Scale
- One instance: the hub and the rate limits live in memory
REST API
[0018]PocketBase-shaped, on purpose.
Every collection's records live at one predictable address. Filtering, sorting and paging are query parameters, written in the same expression language as the rules below. Files are served with short-lived tickets, so a session token never ends up in a URL.
GET /api/collections/tasks/records?filter=(status='open')&sort=-created
- Records
/api/collections/{name}/records- One record
…/records/{id}. There is no/record/.- Files
/api/files/{collection}/{id}/{file}- Tickets
rpbf_<expiry>_<hmac-sha256>- Caching
- API responses are never cached
- Batch
POST /api/batch, up to 50 JSON record writes in one transaction- Health
/api/healthreports liveness and version only; build metadata needsRPB_HEALTH_VERBOSE=1or the superuser checklist
Auth
[0016]Slow to guess, fast to check.
Passwords are hashed with Argon2id. A login for an email that doesn't exist still runs a full Argon2 check against a dummy hash, so the response time doesn't reveal which accounts exist. Sessions are signed tokens with hard limits, and machine keys never get superuser powers.
$argon2id$v=19$m=19456,t=2,p=1$…
- Hash
- Argon2id, 19 MiB of memory, 2 passes, 1 lane
- Sessions
- HS256 JWT, valid 24 hours; renewable for up to 7 days from sign-in
- Identity
- Email and username match without regard to case, so
Jon@…signs in asjon@…. Any other identity field you configure matches exactly - Limits
- 20 failed logins per 15 min per email, 40 per IP, then HTTP 429
- Sign-ins
- Every attempt is recorded in
_auth_audit— success, wrong password, unknown identity, throttled — hashed into its own chain. The password never is - API keys
rpbk_…, never superusers: the collection allowlist is checked first, then the normal rules- Compare
- Constant-time for keys and token digests
- First run
- The installer locks after the first superuser is created
Rules engine
[0014]Every rule becomes SQL, or nothing.
Each collection has list, view, create, update and delete rules, written in a PocketBase-compatible expression language. List rules compile to parameterized SQL. If a rule can't be compiled, the query becomes WHERE 1 = 0: you get an empty list, never an unfiltered one.
listRule owner = @request.auth.id && status != "archived"
- Null rule
- Superusers only
- Empty rule
- Public
- Operators
= != > >= < <= ~ !~, plus?any-of forms.=and?=match any value in a multi-value relation or select- Context
@request.auth.id,collectionId,collectionName, and stored fields of the signed-in record.@request.body.*and@now- Auth names
- A name the login's collection does not have is refused when the rule is saved.
password,tokenKey, and secrets stay unreadable - @collection
- Compiles to
EXISTSon a list rule. On create, update, delete, and view it matches nothing, and the editor warns. The rule still saves - Relations
- The value in a rule is the related id. Naming another field, or a dotted
@request.bodypath, warns in the editor. The rule still saves - Modifiers
:isset:changed:length:lower- Guardrail
- Client filters can't reference
@collection,passwordortokenKey
Schema
[0012]Collections are tables. Fields are columns. Views are queries.
A collection is the unit of schema. Creating one creates a SQLite table below; each field adds a column. A collection can also carry behavior packs, such as append-only, record audit, a unique key, and TTL expiry. The whole schema travels as JSON. An import applies one collection at a time: a failure names what already landed, and a listed collection is made to match the file, so a field the file omits is dropped with its data.
- Kinds
- Base collections, auth collections, and views
- Portable
- Export and import the schema as JSON
- Behaviors
- Append-only, record audit, unique key, TTL. Soft delete and draft/publish are admin packs that add a flag.
- Indexes
- A declared
CREATE INDEX, unique and partial included, is created with the collection. A unique index that existing rows break is refused and not stored. A duplicate write returns 400
12aFields
A field's type decides its storage type in SQLite, what is checked on write, and what comes back on read. There are fourteen.
| Type | Stored as | What it adds |
|---|---|---|
| text | TEXT | Minimum and maximum length. pattern is checked on create, update, and batch; a pattern that is not a regular expression is refused when the field is saved. A unique key on this field can fill a prefixed sequence or another generated id |
| number | REAL | Minimum and maximum value |
| bool | INTEGER | Stored as 0 or 1 |
| TEXT | Format checked on write | |
| url | TEXT | Format checked on write |
| date | TEXT | ISO-8601 text, comparable with @now and the date macros |
| json | TEXT | Any JSON value, kept as written |
| file | TEXT | Filenames in pb_data/storage, served through /api/files. A refused delete or replace leaves them in place; files go only after the write commits |
| password | TEXT | Argon2id hash. Never returned, and never usable in a client filter |
| autodate | TEXT | Timestamp written on create, on update, or both |
| select | TEXT | A fixed list of values. maxSelect > 1 makes it multi-select |
| relation | TEXT | Record ids in another collection. maxSelect > 1 stores a list; = and ?= match any id in it. cascadeDelete removes this row with the target. restrictDelete refuses to delete the target while this field still points at it |
| editor | TEXT | Long text, stored as written, with the same length and pattern checks as text |
| secret | TEXT | An AES-256-GCM envelope; sealed, see 12b. Never returned as plain text |
- Every field
required,hidden,presentable,system,primaryKey- Per type
min/max,pattern,maxSelect,collectionId,cascadeDelete,restrictDelete,onCreate/onUpdate,secretMode
12bSecret fields
One of the fourteen types is sealed. What lands in SQLite is an AES-256-GCM envelope, not plain text. It unlocks with the server key or with a passphrase (stretched with Argon2id), and only through an explicit reveal request, which can be audited.
POST /api/collections/{name}/records/{id}/reveal
- Cipher
- AES-256-GCM
- Keys
- The server key (
.vault_key) or a passphrase stretched with Argon2id - Reveal
- A dedicated endpoint, with configurable audit destinations
- Caution
- Backups include
.vault_key
12cViews
A view is a collection with a query instead of columns. Before it is created, the query has to prove it only reads: write keywords are rejected, and a column policy blocks password and tokenKey from being selected out under another name. The fields are then read back from the database rather than declared, so the view's shape always matches what the query actually returns.
viewQuery SELECT id, owner, COUNT(*) AS runs FROM jobs GROUP BY owner
- Runs where
- It becomes a real SQLite view, so the query runs in layer 10 on every read
- Columns
- Read back from the database, not declared by hand
- Rules
listRuleandviewRuleapply as they do anywhere else- Writes
- Refused: "View collections are read-only."
- Admin
- The view builder previews columns and sample rows before you save
SQLite
[0010]One file, written ahead.
Data lives in an embedded SQLite database in pb_data, opened in write-ahead-log (WAL) mode so reads don't wait on writes. Views are real SQLite views, checked to be read-only before they're created. Uploaded files sit next to the database in pb_data/storage.
- Journal
- WAL
- Integrity
- Foreign keys on; 5,000 ms busy timeout
- Views
- Read-only
viewQuery, with columns read back from the database - Expiry
- Collection-level TTL, plus a sweep an admin can run
- Backups
- A zip of the database, WAL, file storage and secrets. Treat it as secret.
10aStarting the books
The protections above mean an issued invoice or a recorded payment cannot be deleted through the API — which also means demo data entered while trying an app would count in the real books forever. records reset is the one way out: offline, with the server stopped, and recorded.
$ rpocketbase records reset --prefix app_ --except app_settings --dry-run
- Scope
- Only the collections named; system collections and views are refused
- Deletes
- Every row including locked, append-only and restrict-protected ones, in one transaction, then their stored files
- Refuses
- While a server holds the directory, and when a kept record requires one being reset
- Records it
- One
resetrow in the audit trail, so/api/audit/verifystill passes - Numbering
- Follows the rows that remain, so the next invoice is
INV-1
One binary
[0100]Everything above is one process.
rpocketbase serve starts all seven layers from a single Rust binary: no Node, no separate frontend, no external database. If you want TLS, put Caddy in front of it. Caddy is the one piece that runs outside the binary.
$ rpocketbase serve
rPocketBase listening on 127.0.0.1:8090
- Language
- Rust
- Offline CLI
backup,collections,superuser,doctor,records reset,records prune,files gc,db compact,audit prune— the same binary, run againstpb_data. The read-only ones work while it serves; anything that writes refuses until the server stops, and says which process holds the directory- Stopping
SIGTERMdrains what is in flight, checkpoints the WAL, closes the pool and releases the directory — a stop, not a kill- Default
127.0.0.1:8090, data inpb_data/- TLS
- Optional Caddy in front, outside the binary
- License
- MIT. A clean-room project, independent of PocketBase.