anytype 0.5.0

An ergonomic Anytype API client in rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
# anytype

An ergonomic Anytype API client in Rust.

[![release](https://img.shields.io/github/v/tag/stevelr/anytype?sort=semver&filter=anytype-v*&label=release)](https://github.com/stevelr/anytype/releases?q=anytype-v&expanded=true)
[![docs.rs](https://img.shields.io/docsrs/anytype?label=docs.rs)](https://docs.rs/anytype)
[![crates.io](https://img.shields.io/crates/v/anytype.svg)](https://crates.io/crates/anytype)

**[Home](https://github.com/stevelr/anytype)   |   [Documentation](https://docs.rs/anytype)   |   [Examples](https://github.com/stevelr/anytype/blob/main/anytype-api/examples/)**

## Overview

`anytype` provides a fluent Rust client for [Anytype](https://anytype.io). It
supports listing, search, and CRUD operations for objects, properties, spaces,
tags, types, members, views, files, and chats, with credential storage and
client-side caching. REST is preferred when it has equivalent functionality;
gRPC supplies capabilities that REST does not expose or represents with less
fidelity.

HTTP calls require an access token. gRPC calls require an account key or
session token. The library can generate and store both credential families in
a `KeyStore`.

Call `AnytypeError::is_authentication()` when an embedding application needs
stable authentication guidance. The predicate recognizes direct HTTP and
configuration failures plus structurally typed nested gRPC authentication
failures without exposing or parsing response messages, URLs, or credentials,
and callers do not need a direct `anytype-rpc` dependency.

The first `grpc_client()` call selects a nonempty stored session token before
falling back to an account key and initializes one cached channel. Concurrent
first callers share that initialization. `find_grpc()` discovers a local
Anytype listener on Linux and macOS by filtering `lsof` listeners and probing
candidate ports in order. Each candidate gets one two-second local budget for
both connection and the unauthenticated `AppGetVersion` probe; unsupported
platforms and unavailable discovery return `None`.

### Features

- Broad coverage of the Anytype REST API 2025-11-08: nearly every documented REST
  operation is called directly over HTTP (see [Status and Compatibility]#status-and-compatibility
  for the known exceptions)
- gRPC back end provides rich file operations, structured chat messages and
  streams, typed body blocks, archived-object cleanup, space backup, and
  process watching
- Paginated responses and async Streams
- Integrates with OS Keyring for secure storage of credentials (HTTP + gRPC)
- HTTP middleware with secret-safe metadata logging, retries, and rate limit handling
- Client-side caching (spaces, properties, types)
- Space administration through typed APIs for chat-space creation, deletion,
  invitations, and sharing controls
- Deterministic name and ID resolution for spaces, types, templates, chats,
  views, properties, and tags, with bounded scans and actionable ambiguity
  errors
- Typed, bounded body-block reads (`body` module): validated block trees with
  exact IDs and order over gRPC `ObjectShow`, plus verified typed create,
  append, update, delete, move, and bounded non-transactional batch operations
- Nested filter expression builder
- Parameter validation
- Metrics
- Used by [anyr]https://github.com/stevelr/anytype/tree/main/anyr for Anytype
  automation from the command line and
  [any-edit]https://github.com/stevelr/anytype/tree/main/any-edit for editing
  Anytype documents as Markdown

Numeric filters support `eq`, `ne`, `lt`, `lte`, `gt`, and `gte`; checkbox
filters support `eq` and `ne`. Typed values pass through unchanged in search
expressions and become canonical number text or lowercase boolean text only
where a list endpoint requires URL query values. The client does not coerce
strings to numbers or booleans, accept checkbox `1`/`0` aliases, or emulate
server filtering after pagination. When typed list filters include one positive
type filter, the client maps it to search's dedicated type selector instead of
sending it as a generic property condition.

### REST model fidelity

`Type`, `Property`, `Tag`, and `Member` retain the REST response's `object`
discriminator. Responses that omit it use the model's expected discriminator
for compatibility, while an observed value is preserved. `Member.icon` uses
the typed `Icon` model.

gRPC file details accept an integral numeric `addedDate` as Unix seconds as
well as the established RFC 3339 string form. `FileObject::target_object_id`
is populated only from `targetObjectId`. `createdInContext` remains upload
context.

### Bounded HTTP responses

Buffered REST responses have finite byte ceilings. Ordinary JSON defaults to
8 MiB, single-object/document JSON to 64 MiB, bounded error bodies to 64 KiB,
and raw file downloads to a separate 256 MiB policy. Truthful oversized
`Content-Length` responses are rejected before their body is read; responses
without a usable length are stopped at the first byte over the ceiling. SSE
chat events remain incremental rather than buffered as JSON, but each pending
event (including its delimiter) has a separate 1 MiB default ceiling. Incoming
transport chunks are consumed without copying them into the event buffer, and
one chunk may contain several independently bounded events. Overflow terminates
the stream with `AnytypeError::ChatSseEventTooLarge` before the one-over byte is
appended. Stream space and chat IDs are validated as path-safe before URL
construction or diagnostic logging.

Applications can lower or raise the defaults within the library's hard
maxima. An individual object read can choose a smaller ceiling but cannot
exceed the configured document allowance:

```rust,no_run
use anytype::prelude::*;

# async fn example() -> Result<(), AnytypeError> {
let config = ClientConfig {
    response_limits: ResponseLimits {
        json_bytes: 4 * 1024 * 1024,
        document_bytes: 24 * 1024 * 1024,
        error_bytes: 32 * 1024,
        file_bytes: 128 * 1024 * 1024,
        chat_sse_event_bytes: 512 * 1024,
    },
    ..ClientConfig::default()
};
let client = AnytypeClient::with_config(config)?;
let object = client
    .object("space-id", "object-id")
    .response_limit_bytes(12 * 1024 * 1024)
    .get()
    .await?;
# let _ = object;
# Ok(())
# }
```

The 64 MiB document default accommodates worst-case JSON escaping of a valid
10 MiB outgoing markdown body. The hard maxima are 64 MiB for ordinary and
document JSON and chat SSE events, 1 MiB for error bodies, and 1 GiB for raw
files. `AnytypeError::ResponseTooLarge` contains only
the selected ceiling and optional declared length; it never retains a response
body, URL, request payload, or credential.

### Retry safety

Automatic response, rate-limit, and transport retries are restricted to HTTP
reads: `GET`, `HEAD`, and `OPTIONS`. `POST`, `PATCH`, mutation `DELETE`, and
`PUT` without documented endpoint-specific replay approval are sent exactly
once. A logical deadline or transport failure after dispatch returns an
indeterminate mutation outcome. A 408, 429, 504, or other server failure is
also indeterminate because the server may have applied the write before
returning or losing the response. Observe fresh server state before deciding
whether to retry. Connection-establishment and request-construction failures
occur before any possible dispatch, keep their typed transport error and
reqwest source, and are safe to retry immediately.

The client disables reqwest's lower-level retry and redirect handling so every
additional send passes through this method-aware policy and its metrics. A 3xx
response is returned as an API error without forwarding the bearer credential
or request body to the `Location`. Consequently, redirect or retry policies
set on a `ClientBuilder` passed to `AnytypeClient::with_client` are intentionally
overridden; timeout, proxy, DNS, TLS, and user-agent customization is retained.

`ClientConfig::rate_limit_max_retries` continues to control consecutive 429
retries for replay-safe requests; zero disables that rate-limit-specific cap.
Independently, one cumulative ceiling permits at most six physical attempts
across 429, retryable-status, and connection failures, and the counter never
resets when the failure class changes. Caller transport timeouts terminate the
logical request so the shorter caller boundary wins. Retry count does not opt
mutation requests into replay. HTTP metrics expose independent
`logical_operations` and `physical_attempts` counters; the existing
`total_requests` field retains its physical-request meaning.

### HTTP deadlines

Each REST request has one absolute logical deadline that covers every physical
send, retry wait, rate-limit delay, response header, buffered body, and JSON
decode. Ordinary requests default to 120 seconds. File and multipart requests
default to 600 seconds. Each paginated page receives a fresh ordinary deadline.
`AnytypeClient::with_config` also installs a fixed 30-second connection timeout.
`AnytypeClient::with_client` preserves the caller's connection and request
timeouts while applying the logical policy.

Set an explicit policy when an embedding application owns different request or
stream boundaries:

```rust,no_run
use anytype::prelude::*;
use std::time::Duration;

# fn client() -> Result<AnytypeClient, AnytypeError> {
let policy = HttpTimeoutPolicy {
    standard_operation: Some(Duration::from_secs(60)),
    long_operation: Some(Duration::from_secs(900)),
    sse_open: Some(Duration::from_secs(60)),
    sse_error_body: Some(Duration::from_secs(30)),
    sse_idle: Some(Duration::from_secs(90)),
    sse_total_lifetime: None,
};
let config = ClientConfig::default().http_timeouts(policy);
AnytypeClient::with_config(config)
# }
```

Finite values range from one through 3,600 seconds. `None` in an explicit
policy disables that boundary. Without an explicit policy,
`ANYTYPE_HTTP_TIMEOUT_SECS=1..3600` replaces the four buffered/open defaults
with one value, while `0` disables those four logical boundaries. Malformed,
non-Unicode, signed, whitespace-bearing, overflowed, and larger values reject
client construction. The environment never enables established SSE idle or
lifetime limits.

Successful SSE headers disarm the open deadline before the response body is
returned. Non-success bodies receive a fresh error-body deadline. Established
streams have no idle or total-lifetime deadline by default; when configured,
any nonempty transport chunk resets the idle timer and the lifetime timer never
resets. `AnytypeError::HttpTimeout` and `error.diagnostic()` report a closed
class and outcome, sanitized method and path, elapsed time, and physical
attempt count. Timeout metrics are available through `http_metrics().timeout`,
`transport_timeouts`, and `timeout_outcome_count`.

`http_credential_generation()` exposes only a monotonic process-local number.
It advances whenever the in-memory HTTP key is set or cleared, allowing
principal-bound caches to invalidate entries without reading, retaining, or
hashing the credential itself. Credential replacement and generation advance
share one synchronization boundary; no observer can see a mixed pair.

### gRPC deadlines

`ClientConfig::grpc_timeouts` configures the logical gRPC policy used by the
client's cached `AnytypeGrpcClient`; the fluent `grpc_timeouts(...)` builder
sets an explicit policy. With no explicit policy,
`ANYTYPE_GRPC_TIMEOUT_SECS=1..3600` supplies one inherited credential,
ordinary, long-operation, and stream-setup value, while `0` disables those
four. The environment does not enable established-stream idle or lifetime
limits and does not alter the five-second cleanup default. Without either
setting, the defaults are 120 seconds for credential, ordinary, and stream
setup, 30 minutes for long operations, and five seconds for cleanup.

An explicit policy ignores the environment. `None` disables an individual
boundary; finite values are validated before keystore or network side effects.
Credential, ordinary, setup, idle, and lifetime values may be at most one
hour, long operations two hours, and cleanup 30 seconds. Invalid programmatic
or environment policy rejects client construction with `AnytypeError::Validation`.

Ordinary and long reads return an aborted-read outcome on expiry. A mutation
that may have been dispatched returns `mutation_indeterminate`; inspect fresh
server state before deciding whether retry is safe. Cleanup uses its own short
bound and is also indeterminate after possible dispatch. Runtime deadline and
stream-control failures remain structurally available below
`AnytypeError::Grpc` without placing peer status text in standard diagnostics.
Deadline-service transport failures consume and discard the original error
value after deriving a closed tonic status code. The generic error-type marker
remains for source compatibility, while standard source traversal exposes only
a synthetic status with that code and fixed redacted text.

Each request uses the earliest policy duration, absolute enclosing workflow
deadline, or existing tighter `grpc-timeout`, including time spent waiting for
service readiness. The library's `StreamSetup` profile stops at successful
response headers and is deliberately not propagated as `grpc-timeout`, which
tonic treats as a whole-stream limit. For this class, only an explicit caller
whole-call timeout is propagated, reduced to its remaining absolute budget
after readiness; the library setup and enclosing budgets remain local.

Applications that compose several client operations can call
`scope_grpc_deadline` with one absolute Tokio instant. Every nested generated
gRPC call observes the remaining budget and propagates only that remainder
where the method profile permits it. Channel connection keeps its separate
fixed 30-second boundary. The scope does not change the one-way dependency:
callers use the public `anytype` API rather than depending on `anytype-rpc`
directly.

Configured stream idle is reset by raw nonempty transport progress before
message decoding; total lifetime and enclosing deadlines never reset.
`chat_stream` keeps capped exponential reconnect backoff, resubscription, and
watermark catch-up inside those bounds. Once a raw event has been decoded, its
chat events enter a private pending queue and are delivered before an
already-ready close, transport, or saturation boundary is handled. Output
backpressure therefore does not discard them. An idle expiry can interrupt
delivery; retained items drain first after reconnect, while a lifetime or
enclosing expiry terminates the workflow. Watermarks advance only for delivered
items. The reconnect-attempt counter resets after exactly two delivered decoded
events. An interrupted control mutation is not replayed and may be
indeterminate. `ProcessWatcher` logs only the status code for stream-read
failures and numeric progress counters, not peer status text, process IDs, or
progress messages.

`grpc_client().client_commands()` is deadline-aware. Callers that deliberately
obtain the underlying raw `channel()` bypass these logical boundaries; use
`deadline_channel()` when constructing another generated tonic client. The
dependency direction is unchanged: `anytype` uses `anytype-rpc`, while
`anytype-rpc` remains independent of this crate.

### Secret-safe HTTP diagnostics

The library-owned HTTP diagnostics remain metadata-only at every `RUST_LOG`
level. The `anytype::http` target reports stable error variants with an HTTP
status, validated method, and bounded path-only context when available.
`anytype::http_json=trace` adds request/response byte counts and query-field
counts, but never logs request or response bodies.

No directive for those two HTTP targets enables query values, headers, full
URLs, bearer tokens, credential-bearing URL components, or Anytype document
content. This guarantee is HTTP-specific: other `anytype` tracing targets are
outside its scope, so applications enabling them need an appropriate filter.

Standard `AnytypeError` `Display` and `Debug` output and its error source chain
exclude all free-form messages, identities, candidate values, last errors,
paths from malformed targets, and typed upstream sources that could contain
request or document content. Use `error.diagnostic()` for structured
application logs. Raw public fields, including `ApiError::message`,
`RateLimitExceeded::header`, validation messages, resolver identities, and
typed sources, remain available through explicit variant matching and must not
be logged without an application policy.

## Quick start

```rust,no_run
use anytype::prelude::*;

# async fn example() -> Result<(), AnytypeError> {
let client = AnytypeClient::new("my-app")?;
let spaces = client.spaces().list().await?;
let Some(space) = spaces.iter().next() else {
    return Ok(());
};

let page = client
    .new_object(&space.id, "page")
    .name("Meeting notes")
    .body("# Decisions")
    .create()
    .await?;

let results = client
    .search_in(&space.id)
    .text("meeting notes")
    .types(["page", "note"])
    .sort_desc("last_modified_date")
    .limit(10)
    .execute()
    .await?;
for object in results.iter() {
    println!("{}", object.name.as_deref().unwrap_or("(unnamed)"));
}

client.object(&space.id, &page.id).delete().await?;
# Ok(())
# }
```

Search pagination limits must be between 1 and 1000 inclusive. Both global and
space-scoped search reject `0` or larger values with `AnytypeError::Validation`
before sending an HTTP request.

See the [Examples](./examples/README.md) folder for more code samples.

Universal object links are constructed locally and do not call Heart's retired
`ObjectShareByLink` RPC. Use `object.get_link()` for an object returned by the
API, or `client.get_share_link(space_id, object_id)?` when both validated IDs
are already known. `object.get_link_shared(cid, key)?` adds an existing space
invite to the link.

For soft-delete workflows that reconcile uncertain responses themselves,
`client.object(space_id, object_id).delete_once()` sends exactly one HTTP
request attempt. Ordinary `delete()` retains the client's replay-safe DELETE
retry policy.

Anytype's canonical Markdown read representation is not always safe to send
back unchanged: for example, a literal underscore in a plain line is returned
escaped. `objects::plain_markdown_representation` provides separate `wire()`
and `canonical()` forms for the deliberately closed subset of empty bodies and
single plain lines containing alphanumeric characters, internal ASCII spaces,
and underscores. It accepts either raw or already-canonical values and is
idempotent on replay. It returns `None` for punctuation, multiline Markdown,
and ambiguous backslash forms; callers must reject or separately verify those
forms rather than guess at Markdown equivalence or blindly replay export bytes.

The ignored disposable-space matrix in
`tests/test_markdown_fidelity.rs` characterizes the current server's narrower
export/replacement behavior with two stable REST reads and two fresh
`ObjectShow` reads on each side of an exact exported-Markdown replacement.
Representative headings, bullet/numbered lists, checkboxes, a one-line quote,
a link, Unicode, and multiline paragraphs retain byte-identical exports.
Consecutive quote lines, fenced code, tables, literal underscores, and explicit
backslash escapes drift at the byte and typed-block-content levels; they have
no replay-stability contract. Every tested PATCH also replaces block IDs, even
when exported bytes stay identical, so exported-Markdown replacement never
promises block identity. The matrix currently establishes no intermediate
typed-semantic-only cohort.

## Archived Object Cleanup

```rust,no_run
use anytype::prelude::*;

# async fn example(client: &AnytypeClient, space_id: &str) -> Result<(), AnytypeError> {
let count = client.count_archived(space_id).await?;
println!("archived before delete: {count}");

// Use a page budget when exhaustive work is not acceptable. The budget
// includes the empty continuation probe needed to prove an exact full page.
let bounded_count = client.count_archived_bounded(space_id, 3).await?;
println!("exact archived count within three logical pages: {bounded_count}");

let deleted = client.delete_all_archived(space_id).await?;
println!("deleted archived objects: {deleted}");
# Ok(())
# }
```

`count_archived` retains its exhaustive behavior. `count_archived_bounded`
returns a count only after proving exhaustion within `max_pages`; each logical
page can make two gRPC requests while probing the supported archive-relation
key, and an exact multiple of 500 rows needs one additional empty probe page.
Offset scans assume archive membership and ordering remain stable for the
duration of the count. The archived search adapter validates ID-only type
metadata but cannot construct the complete key required by `Type`, so listed
archived objects leave `r#type` unset instead of constructing a partial type.

## Files

Simple uploads, byte downloads, and deletion use REST. File listing, search,
metadata, preload, URL upload, and uploads with style/context options use gRPC.

```rust
let space_id = "space_id";
let file_id = "file_object_id";
let bytes = client.files().download_bytes(space_id, file_id).await?;
tokio::fs::write("/tmp/download", bytes).await?;
```

For image variants, byte ranges, cache validators, or response metadata, use
the configurable request API. It preserves `206 Partial Content`,
`304 Not Modified`, `412 Precondition Failed`, and `416 Range Not Satisfiable`
statuses for the caller to handle:

```rust
let response = client
    .files()
    .download_request(space_id, file_id)
    .width(640)
    .byte_range(0, 4096)
    .response_limit_bytes(4097)
    .error_limit_bytes(64 * 1024)
    .header_evidence_limit_bytes(4096)
    .max_attempts(6)
    .if_none_match("\"cached-etag\"")
    .download()
    .await?;

println!("status: {}, type: {:?}", response.status, response.metadata.content_type);
```

These controls are per request: they never widen or mutate the configured
global response limits. Successful GETs require one canonical `Content-Length`
that matches the buffered body. Partial responses additionally require one
canonical `Content-Range` consistent with the requested range and body.
`Content-Type`, `ETag`, `Last-Modified`, and `Accept-Ranges` are parsed and
validated; duplicates, non-UTF-8 values, contradictions, truncation, and
allowlisted header evidence over the selected ceiling fail with typed,
secret-safe errors. The header ceiling is checked independently before body or
retry processing on every physical response, including intermediate 429 and
retryable-status responses. The attempt ceiling is cumulative across 429,
retryable status, and connection replays.

Use `files().metadata(space_id, file_id)` for a simple `HEAD` request. File
deletion moves the object to the bin by default; permanent deletion is explicit:

```rust
client
    .files()
    .delete_request(space_id, file_id)
    .permanently()
    .delete()
    .await?;
```

Server compatibility, verified against `anytype-cli` 0.3.6 (API `2025-11-08`):
the file endpoint advertises `Accept-Ranges: bytes` and returns 206, 412, and
416, but supplies neither `ETag` nor `Last-Modified`, so `304 Not Modified`
cannot be triggered there. File requests use the 600-second long-operation
deadline by default; permanent deletion's independent live regression guard
remains 180 seconds.

`files().upload(space).from_path(path).upload()` selects REST for a simple
path upload and returns a normalized `FileObject`. Adding `file_type`, `style`,
`details`, or creation-context options selects the richer gRPC upload.

Callers that already hold an authorized asynchronous reader can stream it
without reopening a path or buffering the complete payload:

```rust
let file = tokio::fs::File::from_std(opened_file);
let uploaded = client
    .files()
    .upload(space_id)
    .reader("report.bin", file, exact_length)
    .mime("application/octet-stream")
    .multipart_limit_bytes(exact_length + 1024 * 1024)
    .upload()
    .await?;
```

The declared length participates in the complete multipart ceiling. Reader
uploads fail if the source ends early or yields an extra byte, use REST, and
reject gRPC-only rich options.

REST uploads can apply request-local ceilings without changing the client
configuration:

```rust
let file = client
    .files()
    .upload(space_id)
    .bytes("report.txt", b"bounded bytes".to_vec())
    .mime("text/plain")
    .multipart_limit_bytes(71_680)
    .response_limit_bytes(65_536)
    .error_limit_bytes(65_536)
    .upload()
    .await?;
```

The multipart ceiling includes the complete boundary and part headers and is
checked before authentication or network I/O. The successful and error-body
ceilings are independent, and the REST upload POST is sent at most once.

Call `resolve_space_id_bounded(reference, page_limit)` when a workflow needs a
request-local ceiling on every name-resolution page. Stable space IDs still
return without I/O; names retain the normal finite scan and ambiguity rules.

`files().preload(space)` accepts either `from_path(path)` or `from_url(url)` as
its source and always runs over gRPC, returning the preload file id.

## Attached Discussions (REST + gRPC)

Pages and notes can own one derived discussion object. This is not an ordinary
space chat: scope begins with the exact parent, and successful discovery proves
the derived object's space, discussion smart-block type, discussion layout, and
deterministic `discussion-<parent_id>` unique key.

```rust,no_run
use anytype::prelude::*;

# async fn example(client: &AnytypeClient) -> Result<(), AnytypeError> {
let current = client
    .attached_discussion("space_id", "parent_object_id")
    .get()
    .await?;

if current.discussion_id().is_none() {
    let attached = client
        .attached_discussion("space_id", "parent_object_id")
        .ensure()
        .await?;
    println!("{}", attached.discussion_id().unwrap_or_default());
}
# Ok(())
# }
```

`get` returns the closed `AttachedDiscussion::Absent` or
`AttachedDiscussion::Attached` state after a cache-independent REST parent
preflight and bounded gRPC reads. The exact REST wire requires an explicit
layout, and only Basic- and Note-layout parents are accepted. `ensure` reads
first and never calls the upstream attachment RPC for an already attached
parent. When absent, it dispatches at most one mutation and then rereads the
parent and independently verifies the derived discussion; transport errors,
malformed evidence, and an unconfirmed final state are not retried. Once
dispatch begins, reconciliation continues in an owned task even if the caller
cancels its future. Each gRPC call has a finite deadline capped at five seconds,
the whole operation has a caller-adjustable absolute deadline capped at thirty
seconds. A show that returned a usable view or has an indeterminate dispatch
outcome owns a separate bounded close; a definitive pre-acceptance
authentication or permission rejection returns directly without manufacturing
a close that could mask the original error.
The total budget reserves time for each owned close and, once a write is
admitted, for one fresh reconciliation read.

`AttachedDiscussionErrorKind` provides closed, payload-free classifications for
unsupported layouts, malformed identity evidence, RPC and operation deadlines,
cleanup failure, upstream failure, owned-task failure, and indeterminate
mutation outcomes. gRPC unauthenticated and permission-denied statuses remain
structural authentication errors without retaining status text. Use
`client.attached_discussion_metrics()` to inspect cumulative parent GET, show,
accepted-show, close, successful-close, write-dispatch, and reconciliation
counters.

## Chats

Space-scoped chat listing, creation, plain-message CRUD, lookup/search,
reactions, read state, and per-chat SSE streams use REST:

```rust
use futures::StreamExt;

let chats = client.chats().in_space("space_id");
let page = chats
    .list()
    .filter(Filter::text_contains("name", "team"))
    .limit(20)
    .list()
    .await?;
let message_id = chats
    .add_message("chat_id", MessageContent::new().bold("Hello"))
    .send()
    .await?;
let first_history = chats.older_messages("chat_id").limit(8).get().await?;
if let Some(before) = first_history.next_before {
    let older = chats
        .older_messages("chat_id")
        .before(before)
        .limit(8)
        .get()
        .await?;
    println!("{} older messages", older.messages.len());
}
let edit = chats
    .edit_message(
        "chat_id",
        &message_id,
        MessageContent::new().italic("Edited"),
    )
    .send_verified()
    .await?;
assert!(edit.after.modified_at > edit.before.modified_at);
let mut events = chats
    .message_stream("chat_id")
    .limit(20)
    .heartbeat_seconds(15)
    .open()
    .await?;
while let Some(event) = events.next().await {
    if let ChatHttpEvent::MessageAdded { message } = event? {
        println!("{}", message.content.text);
    }
}
```

Structured message blocks, full-fidelity reads, cross-chat previews, reconnect
watermarks, and dynamic subscription control remain available as gRPC
extensions because the REST representation omits blocks and per-user state.

`ChatClient::read_all_account` is account-global. Heart's `ChatReadAll` request
has no space or chat field, and its handler traverses every chat known to the
current session. Only run this operation when the account's complete chat
inventory is safe to mark read. The deprecated `read_all(space_id)` form
validates its argument but does not send it on the wire. A separate live tier
runs the global mutation alone against a fresh account and tears down its
server process tree afterward.

Older REST history uses a typed page with a 1 through 12 item limit. Its
`next_before` value is an equality-only opaque server token limited to 256
ASCII graphic bytes. Pass it only to the next `older_messages` request; do not
parse or sort it. Each returned window preserves Heart's oldest-to-newest
order, while continuation moves to an older window. Message timestamps fail
closed when the server value is out of range and format canonically with UTC
millisecond precision through `canonical_chat_timestamp`. `send_verified`
performs GET, PATCH, and an independent GET and fails when the supported edit
does not strictly advance `modified_at`.

## Rich Chat Streaming (gRPC)

```rust
use anytype::prelude::*;
use futures::StreamExt;

// print chat messages as they arrive
async fn follow_chat(client: AnytypeClient, chat_obj_id: &str) -> Result<(), AnytypeError> {
    let ChatStreamHandle { mut events, .. } = client
        .chat_stream()
        .subscribe_chat(chat_obj_id)
        .build();

    while let Some(event) = events.next().await {
        if let ChatEvent::MessageAdded { chat_id, message } = event {
            println!("[{chat_id}] {}: {}", message.creator, message.content.text);
        }
    }
    Ok(())
}
```

## Body Blocks (gRPC)

The `body` module reads the rich body of an object (paragraphs, headings,
lists, callouts, tables, bookmarks, LaTeX/Mermaid/YouTube embeds) as a typed,
bounded tree with exact block IDs and exact child order:

```rust
use anytype::prelude::*;

async fn print_body(client: &AnytypeClient) -> Result<(), AnytypeError> {
    let snapshot = client.blocks().body("space_id", "object_id").fetch().await?;
    for block in snapshot.iter() {
        if let BlockContent::Text(text) = &block.content {
            println!("{:?}: {}", text.style, text.text);
        }
    }
    Ok(())
}
```

Reads are fail-closed: duplicate, cyclic, orphaned, dangling, oversized, or
malformed block graphs fail whole with a typed `AnytypeError::BodyGraph`
error. A partial or truncated tree is never returned. Per-request
`BodyLimits` can tighten (never widen) the hard ceilings on block count,
depth, fanout, text size, and mark count. Content the typed layer does not
model (dataviews, widgets, unknown styles or marks from newer servers) reads
as an explicit `Unsupported` marker carrying only a content-free structural
summary, so trees from newer hearts stay complete, ordered, and honest.
Every possibly accepted `ObjectShow` owns bounded cleanup established before
the show is polled. A complete foreground `ObjectClose` is required for
success; cancellation or drop may start at most one bounded fallback close on
the current Tokio runtime. Cleanup failure takes precedence over the show or
application response.

`BodyRpcConfig` supplies one absolute deadline, a per-RPC timeout, decoder
limits, and a cloneable `BodyRpcMetrics` observer. `ObjectShow` is capped at
4,194,304 decoded bytes and every mutation and close response at 65,536 bytes;
callers may tighten but never raise those limits. Reuse one configuration for
the body read and editor when a workflow needs one deadline and one exact set
of payload-free counters:

```rust,no_run
use std::time::Duration;
use anytype::prelude::*;

async fn append_with_one_budget(client: &AnytypeClient) -> Result<(), AnytypeError> {
    let rpc = BodyRpcConfig::for_timeout(Duration::from_secs(10));
    let snapshot = client
        .blocks()
        .body("space_id", "object_id")
        .rpc_config(rpc.clone())
        .fetch()
        .await?;
    snapshot
        .edit(client)
        .rpc_config(rpc.clone())
        .append(NewBlock::paragraph("bounded write")?)
        .await?;
    assert_eq!(rpc.metrics().snapshot().write_polls, 1);
    Ok(())
}
```

The write counter advances immediately before the one write future is first
polled. A zero counter therefore proves that validation, authentication,
acquisition, deadline, or cancellation stopped the operation before dispatch.
Higher-level workflows whose steps need independent absolute deadlines can
attach clones of one `BodyRpcMetrics` observer with
`BodyRpcConfig::with_metrics`; its snapshot accounts for every configured step
without retaining payloads or identifiers.
After it advances, transport failure, timeout, malformed or oversized response,
cleanup failure, and exhausted verification are
`BodyMutationIndeterminate`; callers must reread before considering a retry.
Inline emoji marks and callout emoji are 1..64 UTF-8 bytes and control-free.
Mark start and end values are independently validated as ordered, in-bounds
UTF-16 offsets at Unicode scalar boundaries.

Downstream contract suites may opt into the disabled-by-default
`test-fixtures` Cargo feature. It exposes narrow, production-validated typed
snapshot constructors for exact block-count, read-restriction, and
canonical-table boundary tests. The same feature provides a boolean-only
keystore check that proves a test-owned byte buffer contains none of the
configured HTTP or gRPC credential bytes without returning those credentials.
It does not add deserialization or a general snapshot-forging API and must not
be enabled by production dependents.

Downstream HTTP contract suites may instead opt into `scripted-http-fixture`.
It provides a finite loopback HTTP script that records bounded method, path,
and body bytes in arrival order. Each script has fixed request, header, path,
body, and response ceilings; its errors and `Debug` implementations report
only categories and sizes, leaving payload access explicit. This feature is
also disabled by default and must not be enabled by production dependents.

Mutations start from a snapshot and accept only typed constructors and targets
that belong to that snapshot. Every write is sent once, then a bounded fresh
`ObjectShow` read must prove the exact ID, rich state, and sibling/parent
position before success is returned:

```rust,no_run
use anytype::prelude::*;

async fn append_checked_item(
    client: &AnytypeClient,
    snapshot: &BodySnapshot,
) -> Result<BlockMutation, AnytypeError> {
    snapshot
        .edit(client)
        .append(NewBlock::checkbox("verified task", false)?)
        .await
}
```

`apply_all` is explicitly non-transactional: it returns verified receipts for
the completed prefix, the first failure, and the untouched suffix. Timeout,
transport uncertainty, or verification exhaustion returns
`BodyMutationIndeterminate` with the last complete snapshot when available;
callers must reread before retrying. Bookmark creation has an SSRF-safe policy:
it validates and stores an unfetched absolute HTTP(S) URL but never invokes the
server's URL-fetch RPC. YouTube embeds accept only canonical-izable HTTPS
`youtube.com`/`youtu.be` video URLs. Divider style and the complete link-card
appearance (card style, icon size, description mode, and bounded relation-key
list) are typed updates. System singleton, file, table-structural, unsupported,
and operation-restricted targets are rejected before dispatch.
That fail-closed anchor policy also applies to a sibling target's parent and
the existing first child used to encode a first-child insertion. Verified
table creation proves the canonical ordered columns/rows layout regions,
direct column and row membership, dimensions, exact first-row header state,
and Heart's sparse initial cells: no cells without a header, or one ordered
empty paragraph leaf with grey background per column under the header row
only. Missing, extra, misplaced, nonempty, nested, structurally typed, or
noncanonical-presentation cells fail receipt verification; aggregate
descendant counts are never accepted as table evidence.

## Cache-independent Space Reads

Use `client.space(space_id).get_direct()` when an exact mutation preflight or
read-after-write check must bypass the process space cache. It performs one
scoped REST GET, rejects a response carrying a different space ID, and returns
the exact result without reading or priming the cache.

Property and tag mutation builders also provide `no_cache_refresh()`. The
default behavior continues to refresh a primed property cache, including all
tag pages for select properties. The cache-independent mode performs no hidden
tag reads after the write and instead invalidates that space's property cache;
use `property(...).get_direct()` and an explicitly limited `tags(...).limit(n)`
page for bounded semantic readback.

## Space Description Updates

`client.update_space(id)` keeps three operations distinct. Not calling
`description(..)` omits the field and leaves the description untouched;
`description("text")` replaces it; `clear_description()` sends
`"description": ""`, the only wire form that clears on current servers (a JSON
`null` is silently ignored upstream and is never sent). Servers always return
`description` as a string: a cleared description and a never-set one both read
back as `Some("")`, so callers should treat `None` and `Some("")` identically
or use `Space::description_text()`, which maps both to `None`. The live test
`test_space_description` keeps this normalization verified against a real
server (anytype-cli v0.3.6, API 2025-11-08).

## Type Property Classification (REST + gRPC)

`Type.properties` is the REST server's flattened visible list: featured
properties appear before ordinary recommended properties, but the wire model
does not expose the boundary and may omit system-featured definitions. Do not
infer replaceability from list position or known property keys. Use the
source-backed classification read when preparing or verifying an exact type
property replacement:

```rust,no_run
use anytype::prelude::*;

# async fn example(client: &AnytypeClient) -> Result<(), AnytypeError> {
let properties = client
    .get_type("space_id", "type_id")
    .classify_properties()
    .await?;

for property in properties.replaceable() {
    println!("{} ({})", property.name, property.key);
}
# Ok(())
# }
```

The read does not inspect or prime the all-types or all-properties caches. It
combines one cache-independent REST type GET with one gRPC `ObjectShow` of the
same type and reconciles the REST definitions against Heart's authoritative
`recommendedFeaturedRelations` and `recommendedRelations` source lists. The
returned `recommended` list is the complete non-featured set replaced by
`UpdateTypeRequest::properties` and cleared by `clear_properties`.

`ObjectShow` and its exact matching `ObjectClose` both carry tonic deadlines
and outer timeouts. A close guard is armed before show dispatch; cancellation
or timeout during either boundary starts at most one detached five-second
close fallback. `classify_properties()` uses the five-second Show maximum,
while `classify_properties_with_deadline()` accepts a nonzero Show budget of at
most five seconds. Every explicit or detached close owns a fresh independent
five-second window, even when a caller's readback budget has expired. Public
counters expose Show, Close, fallback, and confirmed cleanup success/failure
work without retaining payloads. Cleanup failures take precedence over Show
response errors.

The source lists are capped at 1,000 combined links. Duplicate, overlapping,
malformed, missing, extra, or cross-source-inconsistent evidence fails the
whole read rather than truncating or guessing. The transports are not an
atomic snapshot, so a concurrent edit or eventual-consistency window may
require rereading. gRPC credentials are required. `featured_ids` preserves the
exact source list; `featured` contains only definitions visible on the REST
type. Hidden and file recommendation lists are separate Heart concepts and are
not part of this replaceable-property model.

## Members

List members with `client.members(space_id).list()` and read one exact member
with `client.member(space_id, member_id).get()`. The exact-read builder accepts
the REST API's object-shaped IDs, `_participant` IDs, and network identities;
the value must remain a URL-unreserved path segment of at most 256 bytes.

## Direct Collection Membership

Saved collection views can hide members through filters and pagination. Use
`observe_collection_membership` when a workflow needs bounded evidence about
one exact object in one exact manual collection:

```rust,no_run
use anytype::prelude::*;

# async fn example(client: &AnytypeClient) -> anytype::Result<()> {
let observation = client
    .observe_collection_membership("space-id", "collection-id", "object-id")
    .await?;
match observation.state {
    CollectionMembershipState::Present => println!("direct member"),
    CollectionMembershipState::Absent => println!("not a direct member"),
}
# Ok(())
# }
```

The read exact-checks the REST collection and object identities and rejects
Set/query lists. It runs an independent unscoped exact-object query before the
collection-scoped query; an absent result also requires the same unscoped proof
afterward. This control/scoped/control sequence prevents a transient missing
index row from being misreported as absence. Saved view filters and sorts are
never consulted. Each app-global Heart subscription has a unique client-owned
ID, a finite deadline, and cancellation-resilient bounded cleanup. Missing
counters, malformed identities, cleanup failures, or incomplete control
evidence return an error rather than `Absent`. After a mutation has been
dispatched, callers must treat every such error as an indeterminate mutation
outcome and perform a fresh read before deciding whether retry is safe.

Use `collection_member_add` when a workflow must add exactly one member and
classify a completed HTTP rejection conservatively:

```rust,no_run
use anytype::prelude::*;

# async fn example(client: &AnytypeClient) -> anytype::Result<()> {
match client
    .collection_member_add("space-id", "collection-id", "object-id")
    .await?
{
    CollectionMemberAddOutcome::Acknowledged => {}
    CollectionMemberAddOutcome::Rejected { status } => eprintln!("HTTP {status}"),
    CollectionMemberAddOutcome::Indeterminate { status } => {
        eprintln!("HTTP {status}; observe membership before retrying")
    }
}
# Ok(())
# }
```

The method sends one POST attempt, never follows a redirect, and returns the
exact completed non-success status without reading or exposing its response
body. HTTP 408, 429, 504, and all server failures are indeterminate and require
a fresh membership observation before retry. A transport failure, incomplete
or oversized success response, or malformed success body remains an error for
the same reason. `view_add_objects` remains the general multi-object API and
does not provide this status-preserving contract.

Use `collection_membership_page` to enumerate the same canonical direct
membership scope without consulting a selected or saved view:

```rust,no_run
use anytype::prelude::*;

# async fn example(client: &AnytypeClient) -> anytype::Result<()> {
let first = client
    .collection_membership_page("space-id", "collection-id", 20, None)
    .await?;
if let Some(next) = first.continuation {
    let second = client
        .collection_membership_page("space-id", "collection-id", 20, Some(next))
        .await?;
    println!("{} direct members so far", first.object_ids.len() + second.object_ids.len());
}
# Ok(())
# }
```

Public pages contain at most 61 validated 1..256-byte safe entity IDs in
Heart's direct collection order. Collection scopes ignore an `id` sort, so the
request carries no sort and the client preserves the returned order without
post-sorting. Each page performs one cache-independent logical HTTP
GET (one through six physical attempts through the shared no-seventh-send
pipeline), one non-replayed Heart subscribe, and one foreground unsubscribe;
an interrupted or failed cleanup can arm only one bounded drop fallback.
A continuation reads one private overlap row to prove its prior boundary and
total are unchanged, then discards that row. Real Heart offset windows report
the complete total while leaving both relative counters at zero, so checked
total/offset/row arithmetic determines whether another page exists. Changed
totals or boundaries, overlap-only results, malformed or nonzero relative
counters, unexpected dependencies, cleanup failure, and Set/query targets fail
closed instead of producing an empty or truncated page. Separate pages are not
snapshot-isolated; restart from the first page after concurrent membership
changes.

`client.collection_membership_metrics()` returns cumulative, payload-free
counters for validated direct-observer query phases, membership query rounds,
subscribe attempts, foreground close attempts and successes, fallback close
attempts, and collection add/remove dispatches. The observer count starts only
after the exact REST collection and object identities pass validation, so a
Set/query rejection can be distinguished from a canonical membership query.
Cloned clients share the same counters; the snapshot never retains collection,
object, or subscription identifiers.

## Status and Compatibility

The crate targets the Anytype REST API dated 2025-11-08. Coverage is described
in two parts, because the two transports do not cover the same ground:

`objects(space).filter(...).list()` keeps ordinary filters on the documented
object-list endpoint. Requests containing number or checkbox filters use the
space-scoped REST search endpoint internally, because the object-list query
parser in `anytype-cli` 0.3.6 rejects those typed values after URL decoding.
The public builder, AND composition, HTTP-only authentication, pagination, and
archived-object behavior remain unchanged.

- **Direct REST coverage** - operations the crate performs over HTTP against
  the documented REST surface. Nearly every documented operation is covered
  directly, including auth, spaces, types, properties, tags, objects,
  templates, views, members, search, basic file transfer (upload, byte
  download, metadata, ranges, conditional requests, delete), and space-scoped
  chats (list/create, plain message add/edit/get/list/search/delete, reactions,
  read state, and the single-chat Server-Sent Events stream). No exact
  percentage is published, because the upstream operation list changes with
  each Anytype release, and a few surfaces (such as cross-space chat
  discovery) are deliberately reached only through gRPC.
- **gRPC-equivalent coverage** - capabilities reached through anytype-heart's
  gRPC service where REST has no operation or returns less information. These
  are additional coverage, not a substitute for a missing REST call, and they
  require gRPC credentials at runtime.

The current transport mapping - which method uses which transport, and why -
is recorded in [API surface](./docs/http-grpc-overlap.md).

Plus:

- View Layouts (grid, kanban, calendar, gallery, graph) implemented in the desktop app but not in the api spec 2025-11-08.

- gRPC back-end provides API extensions for features not available in the REST api:
  - File metadata, listing/search, preload, URL upload, and rich upload options.
  - Structured chat blocks, full-fidelity message reads, chat-object search,
    name resolution, cross-chat previews, and reconnecting subscriptions.
  - Exact featured versus replaceable type-property classification.

### Apis not covered

The current Anytype http backend api does not provide access to some data in Anytype vaults.

- ~~Files~~ *Update:* REST supports basic transfer; gRPC supplies richer file operations.
- ~~Chats and Messages~~ *Update:* REST supports chat management and plain message operations; gRPC supplies structured messages and richer streams.
- Blocks. Pages and other document-like objects can be exported as markdown, but markdown export is somewhat lossy, for example, in tables, markdown export preserves table layout, with bold and italic styling, but foreground and background colors are lost.
- Relationships - only a subset of relation types are available in the REST api.

### Cargo features

The crate has no default features (`default = []`), and there is no `grpc`
Cargo feature. `anytype-rpc` is an unconditional dependency, so every
gRPC-backed method is always compiled and callable; what a gRPC-backed method
needs is gRPC credentials in the keystore at run time, not a build-time flag.
Building with `--no-default-features` therefore changes nothing.

Both optional features are disabled by default and reserved for tests:
`test-fixtures` exposes narrow typed snapshot constructors and a boolean-only
credential-leak check, while `scripted-http-fixture` exposes the finite
loopback HTTP script. Production dependents must not enable either feature.

## Keystore

A Keystore stores authentication tokens for http and grpc endpoints. Various implementations store keys in memory, on disk, or in the OS Keyring

`GrpcCredentials::from_cli_config` reads account credentials from the Anytype
CLI's default `~/.anytype/config.json`, or from an explicit path, without
storing them. A missing file is reported separately from malformed or
unreadable configuration so account-bootstrap callers can fail safely.

See the [keystore reference](https://docs.anytype-toolbox.org/reference/keystores/)
for backend selection, environment credentials, and encrypted file storage.

## Known issues & Troubleshooting

See [Troubleshooting](./Troubleshooting.md)

For keystore-related issues, see the
[keystore reference](https://docs.anytype-toolbox.org/reference/keystores/).

## Eventual Consistency

Anytype servers have "eventual consistency" (This is a feature of practical distributed systems, not a bug!). How you might encounter this in your programs:

- Create a new property and then immediately create a type with the property, and get an error that the property does not exist.
- Create a new type and then create an object with the type, and get an error that the type does not exist.
- Delete an object, then immediately search for it, and find it.

The amount of time needed for "settling" seems to be 1 second or less.

`anytype` can perform validation checks after creating objects (objects, types, properties, and spaces) to ensure they are present before `create()` returns. Since this verification can cause delays, it's opt-in. While there are some knobs you can tune to adjust backoff time and number of retries, the easiest way to add verification is to call `ensure_available()` before `create` for critical calls:

```rust,no_run
let obj = client.new_object("space_id", "page").name("Quick note").ensure_available().create().await?;
```

For mutation workflows that must confirm more than availability, use
`verify_semantic` with a predicate over a freshly fetched value. It retries
successful-but-stale values as well as transient not-found, transport, retry,
and server failures. Verification always has both a wall-clock deadline and a
validated nonzero attempt cap no larger than `MAX_VERIFY_ATTEMPTS`; legacy zero
and oversized values safely clamp to that hard ceiling, and zero-delay
configurations remain finite and cancellation-safe. Fetched values and upstream
error text are never retained in the terminal verification timeout.

To enable verification for *all* new objects, types, and properties, add `.ensure_available(VerifyConfig::default())` to the config when creating the client. Setting this in the client configuration is not recommended except for an environment like unit tests where you're hammering the server and need to get results immediately. If verification is enabled in the client config, it will be applied to all `create` calls, unless disabled on a per-call basis by using `.no_verify()`:

```rust,no_run
let obj = client.new_object("space_id", "page").name("Quicker note").no_verify().create().await?;
```

## Building

Requirements:

- protoc (from the protobuf package) in your PATH. On macos, `brew install protobuf`
- libgit2 in your library path.

```sh
cargo build
```

## Testing

The maintained [HTTP/gRPC coverage inventory](docs/api-test-coverage.md)
separates direct unit and live coverage from cross-crate integration evidence
and records the remaining blocked or deferred gaps.

Set environment flags for unit and integration tests. You'll also need a
running Anytype server (CLI or desktop).

```sh
# HTTP endpoint. Default: http://127.0.0.1:31012
#    Headless cli default port is 31012. Desktop app uses port 31009
export ANYTYPE_URL=http://127.0.0.1:31012
# Set the same for ANYTYPE_TEST_URL
export ANYTYPE_TEST_URL=$ANYTYPE_URL
# optional: set keystore to custom path
export ANYTYPE_KEYSTORE=file:path=$HOME/.local/state/anytype-test-keys.db
# required: prefix for uniquely named, cleanup-owned integration-test spaces
export ANYTYPE_TEST_SPACE_PREFIX=xtest
# optional: enable debug logging. Default "info"
export RUST_LOG=
# optional: disable rate limits. If not disabled, tests will take longer to run
export ANYTYPE_DISABLE_RATE_LIMIT=1
```

Keystore modifiers use `:key=value` boundaries. Path values may contain a
Windows drive colon or ordinary colons that are not followed by another
modifier key and `=`.

Test helpers honor `ANYTYPE_KEYSTORE` when it is set and use the in-memory
`env` keystore otherwise. Set the required HTTP and optional gRPC credentials
in the environment when tests need authenticated server access.
Each shared integration-test context creates a fresh space whose name starts
with `ANYTYPE_TEST_SPACE_PREFIX`, then deletes that exact space after the test,
including callback error and panic paths. Reserve the prefix for automated
tests. A missing or invalid prefix fails setup before authentication with a
configuration error; no ambient space-ID environment variable is consulted.
The disposable-space recovery harness stores its ledgers in a private runtime
directory: Unix ownership and permissions are verified from open handles. On
Windows, the owner and every access-granting ACL entry must name the process
user, LocalSystem, or Built-in Administrators. Links and reparse points fail
closed.
Unauthenticated control tests explicitly use unique empty temporary file
keystores, so ambient `env` credentials cannot change their expected result.

Run smoke test

```sh
cargo test --test smoke_test -- --nocapture
```

Run all tests

```shell
cargo test -- --nocapture
```

When the real server's mutation rate limit remains enabled, use
`cargo test -- --test-threads=1` to keep the full live suite from flooding its
shared endpoint. Pagination coverage owns a uniquely filtered, cleanup-tracked
object cohort and does not depend on unrelated ambient-space objects.
Space-creation requests validate a nonempty bounded name before HTTP; validation
coverage never probes this rule by creating an untracked unnamed space.
Empty-filter coverage likewise owns its expected object rather than depending
on pre-existing content in the configured test space.

Integration tests require a running Anytype server and environment variables. See `src/client.rs` for details.

On `anytype-cli` 0.3.6, `DELETE ...?skip_bin=true` can take about 154 seconds
to return `204 No Content`. The permanent-delete live test keeps the request
under a finite 180-second wall-clock ceiling, matching the CLI live-test command
budget while still preventing an unresponsive endpoint from wedging the suite.

The crate no longer ships a semantic gRPC mock server. Successful gRPC
behavior is covered with cleanup-owned resources against the configured real
Anytype server. Protocol and reducer edge cases use scripted transport handlers
or constructed values without pretending to implement Anytype semantics.
Disconnect, latency, and other connection-fault scenarios require the reviewed
external fault-injection harness and are not emulated by an in-process gRPC
service.

Chat resolver integration tests create cleanup-owned chats and messages in a
fresh prefix-authorized space on the configured real HTTP and gRPC endpoints.
Supporting REST reads and the REST SSE test use the same disposable tier so the
resolver and stream files remain runnable when the server has no ambient
spaces. Broader pre-existing REST CRUD, search, reaction, and read-state cases
remain in the ambient `test_chats` tier and are not part of the mock migration.
Every created message is registered immediately, before stream waits or
assertions, and the gRPC stream worker is shut down before teardown.

Body reader integration tests create cleanup-owned objects in a fresh
prefix-authorized space on the configured real HTTP endpoint, then verify typed
reads, ordering, close-safe repeat reads, tightened limits, and missing-object
failures through the configured gRPC endpoint. The adjacent dataview test was
not formerly mock-backed, but shares the disposable tier so the body test file
does not require ambient inventory.

The required tier also creates a disposable collection and a source-backed Set,
then proves both server-created views and their object listings without reading
or registering ambient list objects. The Set fixture uses the authenticated
Heart creation RPC because REST object creation cannot supply its internal
source. Run every required case through the admitted serial driver:

```sh
test -n "${ANY_MCP_HEADLESS_ENV_FILE:-}"
test -r "$ANY_MCP_HEADLESS_ENV_FILE"
set -a
source "$ANY_MCP_HEADLESS_ENV_FILE"
set +a
test "${ANYTYPE_KEYSTORE:-}" = env
test -n "${ANYTYPE_KEYSTORE_SERVICE:-}"
test -n "${ANYTYPE_KEY_HTTP_TOKEN:-}"
test -n "${ANYTYPE_KEY_SESSION_TOKEN:-${ANYTYPE_KEY_ACCOUNT_KEY:-}}"
export ANYTYPE_DISPOSABLE_TEST_PROCESS=1
python3 anytype-api/scripts/run-live-gate.py required anytype-api/tests/live-gate-manifest.toml
```

The checked-in live-gate manifest assigns every ignored test to the required,
manual soak, or excluded tier. The manual workflow selector can run either live
tier or both. Verify that closed inventory without a server:

```sh
cargo test --locked -p anytype --test live_gate_manifest
```

The driver runs every admitted entry in its own process and rejects zero-test
and skip results. The required tier uses a sync-isolated server. The small soak
tier uses a connected disposable server because Heart's space-sharing command
calls its coordinator service; every created resource remains cleanup-owned.
Sharing enablement retries only Heart's definitive `NO_SUCH_SPACE` response
while a newly REST-created space enters the administration service.

With the same protected environment loaded, reproduce the two focused Set/view
entries exactly:

```sh
cargo test --locked -p anytype --test test_views test_views_list_collection_and_set -- --ignored --exact --test-threads=1 --nocapture
cargo test --locked -p anytype --test test_views test_view_list_objects_collection_and_set -- --ignored --exact --test-threads=1 --nocapture
```

Process watcher import-finish coverage uses a real Markdown import in the fresh
cleanup-owned space created by `with_disposable_space_context`. The watcher
subscribes and unsubscribes from the configured gRPC server, accepts empty-space
fallback events only for import requests that explicitly enable the fallback,
and applies fixed timeouts to every live stage. The test is ignored under
ordinary runs because it requires a configured real server and explicit
disposable-process admission. The real server may complete the ordinary import
process before publishing the import-finish event; the test uses the same
subscription for a second bounded wait and proves that no new process was
correlated while observing that fallback.

Tests that need a custom collection can use the hidden
`TestContext::create_collection_type_fixture` helper. Anytype's REST type
create/update contract rejects collection layout, so this test-only helper uses
the narrow heart RPC, registers the returned type for cleanup before any
follow-up read, and verifies it through the ordinary scoped REST getter.

Tests must create the object through
`TestContext::create_collection_fixture`; ordinary cleanup registration does
not grant view-mutation authority. This helper accepts only a collection type
owned by the context, takes a complete type-scoped pre-create snapshot, and
atomically records its cleanup dispatch and exact `(space, object, type)`
provenance. Any collision with an authoritative cleanup ID or existing private
claim is rejected without changing either registry.
`TestContext::create_collection_view_fixture` then requires that provenance,
requires the REST object to retain the exact proven type ID, and cross-checks
every REST-visible default-view field against the exact
`ObjectShow` root and `dataview` block, clones the full proto, and issues one
`BlockDataviewViewCreate` RPC. It requires exactly one matching view-set event,
a distinct server-assigned ID, and complete nested-view equality before a
finite exact two-view REST verification. Collection teardown owns the added
view; there is no general view-create production API.
`TestContext::add_collection_name_filter_fixture` may then add one exact-name
filter only to that cleanup-owned view. It accepts initially unfiltered REST
and `ObjectShow` evidence, sends one authenticated filter-add RPC, and requires
the assigned filter ID and complete value to reread identically through both
surfaces. Collection teardown owns the filter with the view; this remains test
infrastructure, not a production view-filter API.

Representative Kanban tests can use `TestContext::create_kanban_fixture` inside
`with_disposable_space_context`. The helper creates and immediately registers a
custom card type, its Select grouping property and two status options, a
collection, an existing server view converted to Kanban, and three cards. It
adds the grouping relation through heart before setting the layout, rejects
pre-existing filters, resolves Heart's internal relation key separately from
the REST property key, and independently rereads the exact relation format,
view grouping key, tags, membership, and card values. Membership verification
uses two-item pages so pagination is exercised rather than bypassed.
`move_kanban_item_fixture` performs an ordinary object Select-property update
and requires the moved card and complete board to reread exactly. Missing or
wrong-format relations, removed options, filtered views, malformed pagination,
or unregistered resources fail closed. Collection deletion owns view cleanup;
property cleanup owns its options.

Tests that need disposable spaces should use
`TestContext::create_space_fixture`. It creates through the authenticated REST
API after taking a complete bounded inventory whose pagination, IDs, names, and
uniqueness are validated. A response is registered exactly once only when its
valid ID was absent from that inventory, its name exactly matches the request,
and it is a regular space distinct from the context space. The private registry
retains that exact ID/name provenance. Untrusted or ambiguous responses are
allowed to leak rather than authorize deletion of ambient state. Registration
occurs before follow-up verification. Teardown revalidates exact ID/name/model
provenance through the same strict inventory before Anytype's irreversible
`SpaceDelete` RPC, then requires complete bounded REST evidence that the ID is
gone even when the delete response is uncertain. The test-only ownership
registry remains separate from the explicit `AnytypeClient::delete_space` API,
which callers must protect with their own confirmation policy.

Whole live suites should prefer `with_disposable_space_context`. It creates a
fresh cleanup-owned space under the mandatory `ANYTYPE_TEST_SPACE_PREFIX`.
That ASCII prefix is an explicit authorization to delete **every** space whose
current name starts with it, case-insensitively; reserve it exclusively for
tests. Missing or invalid configuration returns a typed `DisposableRun::Skipped`
before credential access or filesystem I/O. One same-host backend-wide file
lease serializes participating runs. An owner-private durable ledger and
disk-backed enumerate-before-delete offset-pagination plans recover interrupted
matching runs without a count ceiling or an in-memory inventory. Each fixed
pagination window shares one deadline; a changing total discards the plan and
restarts at offset zero. New names use 128 bits of operating-system randomness.
Readiness has a hard 20-second and 50-attempt budget. It resolves the exact
`@page` key without a cache, then direct-GETs that returned type through the same
validated space path and requires identical ID, `page` key, and non-archived
state. A failure reports only its final closed stage/category and completed
attempt count. Create failures likewise expose only a closed setup stage and
category, distinguishing rejected or indeterminate requests from invalid ID,
model, name, or ambient-identity evidence without rendering response values.
The numeric/checkbox acceptance callback similarly reports only a closed
fixture or comparison stage and a payload-free `TestError`/API diagnostic
category, which proves whether execution crossed the callback boundary without
exposing fixture identities, queries, endpoints, or upstream bodies. Its
ignored compatibility matrix executes all eleven fixed cases independently on
both endpoints even when an earlier check fails, then reports all 22 static
endpoint/case pairs in canonical order with only their closed categories and
validated HTTP status/classes when available. The regression assertion is
evaluated only after disposable cleanup completes.
All three diagnostic paths store exhaustive enums rather than caller-provided
strings, so `Display`, `Debug`, and accessors can render only the documented
closed vocabulary. The filter fixture resolves its prerequisite `due_date`
property through the bounded, cache-independent property resolver because the
disposable client intentionally disables cache state.
The two immediate pre-delete checks and final absence proof also
use cache-disabled direct exact-ID reads. The helper cleans registered children first and retains callback,
cleanup, deletion, absence, ledger, and panic outcomes; an unproven absence is
always dominant without discarding the original typed error or simultaneous
cleanup evidence. Remote backends require an equivalent scheduler lease and are
otherwise rejected. Operators must not create, rename, or delete spaces through
another client while the helper holds its lease.
Disposable runs require `ANYTYPE_KEYSTORE=env`, an explicit
`ANYTYPE_KEYSTORE_SERVICE`, a nonempty `ANYTYPE_KEY_HTTP_TOKEN`, and at least
one nonempty gRPC session token or account key. They must run in a dedicated
single-threaded integration-test process admitted with
`ANYTYPE_DISPOSABLE_TEST_PROCESS=1`; the process must not mutate its environment.
File, keyring, implicit, unknown, malformed, and over-budget credential forms
skip before authentication, private state, or mutation. The helper creates no
credential file. For a spawned production child, call
`ctx.disposable_child_environment().unwrap().configure(&mut command)` before
spawn, then register an idempotent stop-and-wait handle with
`ctx.spawn_owned_child(...)`. Configuration uses `env_clear`, reconstructs only
the approved endpoints, finite limits, MCP settings, and exact accepted
credential names, and rechecks the whole environment/argument block budget.
The helper records child-running state before invoking the spawn closure and
stops all registered children before resource cleanup and space deletion.
Recovery refuses every cleanup plan and prefix sweep while a prior ledger says
its child may still run. The first refusal durably records that the operator
must prove the child stopped or is gone. Only after that proof may the operator
set `ANYTYPE_DISPOSABLE_RECOVER_STOPPED_RUN` to the exact recorded `.json` run
handle for one invocation; the helper persists the stopped transition before
applying that ledger's plan, and rejects stale or repeated confirmations.
Destructive execution is enabled only where owner and owner-only permissions
can be proved for the runtime directory and every recovery target. Unix opens
and removes exact components relative to verified directory handles with
no-follow semantics. Windows creates protected ACLs, admits only the process
user, LocalSystem, or Built-in Administrators as owner and access-granting
principals, and rejects reparse points before recovery I/O. Recovery files are
flushed before publication; NTFS supplies directory-entry persistence because
Windows rejects `FlushFileBuffers` on directory handles.

Tests that need templates can use the hidden
`TestContext::create_template_fixtures` helper with one to sixteen source
names. It creates a private custom type and source object for each requested
template, invokes the authenticated heart template-from-object RPC exactly once
per source, and verifies the returned IDs through a finite complete type-scoped
list plus exact GETs. Complete bounded type, space-wide active/archived object,
and global template inventories prove create responses did not reuse
pre-existing IDs; the global inventory also proves the new template is owned
only by the expected type, while list and GET generic-template identities must
agree. The helper registers every created ID before classifying
the RPC response or reading it back. Teardown issues each template, source, and
type archive request once in reverse dependency order, then proves the
templates absent and the sources and type archived. Production consumers do
not gain a template mutation API.

## License

Apache License, Version 2.0

## Contributing

Feedback, Issues and Pull Requests are welcome.