appdb
appdb is a lightweight SurrealDB helper library for embedded Rust applications,
especially local-first desktop apps. It gives domain models a small, derive-driven
API for persistence, typed projections, graph relations, encrypted fields,
schema registration, raw SurrealQL, and explicit transaction work.
The workspace publishes two crates:
appdb: the runtime, model APIs, query helpers, and public re-exportsappdb-macros: the procedural macros re-exported byappdb
The current workspace targets Rust 2024 and requires Rust 1.94.0 or newer.
Installation
Application crates usually need appdb, serde, surrealdb, tokio, and an
error type such as anyhow:
appdb re-exports its derive macros, so application code can import Store,
View, Sensitive, Bridge, and Relation from appdb.
Quick Start
use *;
use Store;
use ;
use SurrealValue;
async
The generated model methods are the intended application-facing API. Repo<T>
remains public for advanced integration seams, but normal application code should
stay on the model type.
Runtime
init_db(path) opens an embedded SurrealKV database, selects the app/app
namespace and database, applies registered schema items, and installs the handle
used by model, graph, view, query, and transaction helpers.
Use init_db_with_options(path, InitDbOptions::default()...) when the database
needs SurrealKV versioning, retention, query timeouts, transaction timeouts,
changefeed garbage collection, or AST payload storage.
Use DbRuntime::open* when a caller needs to own a runtime and install it later
with DbRuntime::install_global().
Store Models
#[derive(Store)] turns a struct with named fields into a persisted model. It
generates table metadata, id helpers, stored-shape conversion, lookup metadata,
and model-level methods such as:
save,save_many,get,get_recordlist,list_limit,list().order_by(...)create_at,upsert_at,update_atdelete,delete_all,existsfind_one_id,list_record_ids
use *;
use Store;
use ;
use SurrealValue;
async
#[unique] registers schema indexes for automatic lookup. appdb can resolve a
record from the declared lookup fields, including foreign-backed lookup fields
that first resolve to child RecordId values.
#[pagin] registers a stable keyset-pagination index and enables pagin_desc /
pagin_asc, which return Page<T> with items and an optional PageCursor.
Ordering a full list is limited to id and the declared pagination field, which
keeps list ordering explicit instead of accepting arbitrary field names.
Explicit-id writes use full RecordId values. create_at fails on conflict,
while upsert_at and save update the same addressed record.
Auto-filled Fields
AutoFill is an appdb-managed scalar for fields that should be filled by the
write path. Application code passes AutoFill::pending() instead of manually
writing the value. The Store write path resolves pending values before
persistence and returns the resolved value in the saved model.
Today the supported fill policy is #[fill(now)]. It writes the current UTC
timestamp as a string, normalized with a fixed nine-digit fractional second:
YYYY-MM-DDTHH:MM:SS.nnnnnnnnnZ. It is not an integer millisecond timestamp.
use *;
use ;
use ;
use SurrealValue;
async
Pending values are resolved on save and save_many; already resolved values
are preserved. Use AutoFill::resolved(value) only when importing or replaying a
known timestamp that should not be replaced.
Foreign Fields
Use #[foreign] when a model field should store record links but hydrate back
into full caller-facing values.
use *;
use Store;
use ;
use SurrealValue;
Supported shapes are recursive Option<_> and Vec<_> wrappers whose leaf type
implements appdb's foreign bridge. Store models implement that bridge
automatically. Use #[derive(Bridge)] on an enum when one foreign field can point
to multiple Store model types.
#[table_as(Target)] lets an alias model reuse another Store model's table and
lookup metadata. That is useful when a caller needs a narrower Rust shape over an
existing persisted table.
For explicit-id writes where related records already exist, call
model.foreign().field_name(record_id_shape).upsert_at(...). The generated
foreign-write builder can also return a View directly with
create_at_returning::<View>, upsert_at_returning::<View>, and
update_at_returning::<View>.
Relation Fields And Graph Helpers
Use #[relate("edge_table")] or #[back_relate("edge_table")] when a field
should live in a SurrealDB relation table instead of being stored inline on the
parent row.
use *;
use Store;
use ;
use SurrealValue;
Relation-backed fields support direct, Option<T>, Vec<T>, and
Option<Vec<T>> shapes. save, save_many, create, get, list, and
list_limit synchronize and hydrate those relation fields.
For direct graph work, use #[derive(Relation)], GraphRepo, the free graph
functions, or generated model methods such as relate_by_name,
back_relate_by_name, unrelate_by_name, outgoing_ids, incoming_ids,
outgoing, incoming, outgoing_count, incoming_count,
outgoing_count_as, and incoming_count_as.
use ;
;
Read-only Views
#[derive(View)] defines a typed read projection. A table-backed View reads only
its declared fields from a Store source, so callers can expose list or detail
surfaces without loading every field from the source model.
use *;
use ;
use ;
use SurrealValue;
async
View list ordering accepts id, declared View fields, and the source model's
#[pagin] field even when that field is not exposed by the View.
Nested Views are declared with #[view(nested)] and can use direct, Option<_>,
Vec<_>, or recursive Option / Vec wrappers:
Table-backed Views expose list, list().order_by(...), get, get_record,
find_one, find_one_id, list_records, outgoing_records,
incoming_records, and batch relation queries that preserve each owner record.
SQL-backed Views
Views can also be backed by a custom SurrealQL statement. SQL-backed Views use
typed parameters and are queried through View::query(params).
use ;
use RawSqlStmt;
use *;
use View;
use ;
use ;
async
SQL-backed Views do not support table-source methods such as list() and
get(), because their source is the custom query itself.
Sensitive Fields
#[derive(Sensitive)] encrypts fields marked with #[secure] before
persistence and decrypts them on read.
use *;
use ;
use ;
use SurrealValue;
Supported secure shapes include String, Option<String>, nested
Sensitive children, Option<Child>, Vec<Child>, and
SensitiveValueOf<T> for enum-bearing payloads inside an approved secure
container.
Sensitive models auto-register crypto metadata on first runtime use. Override
defaults globally with set_default_crypto_service,
set_default_crypto_account, or set_default_crypto_config, and refine models
or fields with #[crypto(...)].
By default, auto-registered crypto contexts use the OS keyring and a local
protected backup for the symmetric key. Secure field ciphertext is therefore
bound to the machine/user environment that generated that key. Copying the
database to another machine does not make #[secure] fields decryptable there;
only non-secure fields remain portable by themselves. Cross-machine decryption
requires explicitly registering a crypto context backed by the same externally
managed key.
Schema And Vector Indexes
#[unique], #[pagin], impl_schema!, and impl_hnsw_index! register schema
items through inventory. Runtime initialization applies those items
idempotently.
use ;
;
impl_hnsw_index!;
HNSW index definitions validate plain identifiers, support nested field paths
such as items.embedding, and render SurrealDB DEFINE INDEX ... HNSW DDL.
Raw SQL And Transactions
For query shapes outside the derive-driven API, use RawSqlStmt and the raw
query helpers. Unbound helpers include query_raw, query_checked,
query_take, and query_return; bound helpers include query_bound,
query_bound_checked, query_bound_take, and query_bound_return.
use *;
async
Use run_tx and TxStmt when several statements must run inside one explicit
SurrealDB transaction.
Errors are normalized through DBError / DBErrorKind for common cases such as
not found, missing table, conflict, decode failure, invalid models, and query
response errors.
Workspace Layout
core/: source for the publishedappdbcratemacros/: source for the publishedappdb-macroscrate
Development
Run the workspace checks from the repository root: