Documentation Index
Fetch the complete documentation index at: https://mintlify.com/org-quicko/silo/llms.txt
Use this file to discover all available pages before exploring further.
The silo_client Dart package exposes the same surface as the TypeScript and Java clients — immutable path-based handles, typed entries, filter builders, pagination, search, media, and variables — targeting the Dart VM, Flutter, and the web. It is published to pub.dev as silo_client. Where Dart forces a naming difference the client follows the Java conventions, so a developer reading both should not encounter two vocabularies for the same concept.
Installation
dependencies:
silo_client: ^1.0.0 # check pub.dev for the latest version
Basic usage
import 'package:silo_client/silo_client.dart';
final silo = Silo.at('https://silo.example.com', key: Platform.environment['SILO_KEY']);
final posts = silo
.scope('acme', 'prod')
.collection('posts')
.withConverter(fromJson: Post.fromJson, toJson: (post) => post.toJson());
final created = await posts.create(const Post(title: 'Hello', status: 'draft'));
final published = await posts.replace(created.id, created.rev, const Post(title: 'Hello', status: 'published'));
await for (final post in posts.all()) {
print('${post.id} ${post.fields.title}');
}
await posts.delete(published.id, published.rev);
silo.close();
silo.project('acme').environment('prod') (or the shorthand silo.scope('acme', 'prod')) sends no request — it only builds the path. Nothing needs an await until the actual read.
Typed collections with converters
Use withConverter to attach a typed converter to a collection. The entry shape then becomes Entry<YourType> with a strongly-typed fields property.
final movies = silo
.scope('moviespace', 'prod')
.collection('movies')
.withConverter(fromJson: Movie.fromJson, toJson: (m) => m.toJson());
final page = await movies.list(limit: 10);
for (final movie in page.entries) {
print(movie.fields.title); // typed
}
Entry shape
Like the Java client, the Dart client uses Entry<F> to separate the envelope from your fields. Dart has no intersection type, so the envelope sits beside your fields.
final entry = await movies.get(entryId);
// Envelope
final id = entry.id;
final rev = entry.rev;
final createdAt = entry.createdAt;
// Your fields (typed via withConverter, or Map<String, dynamic> without one)
final fields = entry.fields;
CRUD operations
// Create
final created = await movies.create({
'title': 'Arrival',
'year': 2016,
'status': 'draft',
'genres': ['sci-fi'],
});
// Replace — takes the revision you read; raises ConflictException if stale
await movies.replace(created.id, created.rev, {
'title': 'Arrival',
'year': 2016,
'status': 'published',
'genres': ['sci-fi', 'drama'],
});
// Delete
await movies.delete(created.id, created.rev);
// Read raw before editing
final draft = await movies.get(entryId, const EntryReadOptions.raw());
Reading raw before editing
Silo resolves {{VARIABLE}} references in content on the way out. Pass const EntryReadOptions.raw() when you intend to edit an entry, to avoid writing a resolved value back over a template reference.
final draft = await movies.get(entryId, const EntryReadOptions.raw());
// draft.fields['trailerUrl'] → '{{CDN_URL}}/trailers/arrival.mp4'
await movies.replace(draft.id, draft.rev, {
...draft.fields,
'status': 'published',
});
Filters
Filters are untyped — Dart has no equivalent of TypeScript’s keyof. Use string field names directly. The filter operators follow the TypeScript names (equals, notEquals, etc.).
import 'package:silo_client/silo_client.dart';
final page = await movies.list(
where: Filter.field('status').equals('published')
.and(Filter.each('genres').equals('sci-fi')),
sort: Sort.recentlyUpdated(),
limit: 20,
);
Filter operators: equals, notEquals, contains, greaterThan, atLeast, lessThan, atMost, oneOf, exists — combined with and, or, not.
all() and pages() return Streams rather than async iterators.
// Stream of individual entries
await for (final entry in movies.all(where: filter)) {
print(entry.fields['title']);
}
// Stream of pages
await for (final page in movies.pages(limit: 100)) {
print('page ${page.pageNumber} of ${page.total}');
}
// Manual pagination
final first = await movies.list(limit: 25);
first.total; // 137
first.hasMore; // true
final second = await first.next();
Variables
Declare a variable once per project, then give it a value per environment. Silo substitutes {{NAME}} in entries on the way out.
// Declare for the project with an initial value.
await silo.project('moviespace').variables.declare('CDN_URL',
DeclareVariableOptions(environment: 'prod', value: 'https://cdn.moviespace.com'));
// List and set per environment.
final env = silo.scope('moviespace', 'prod');
final vars = await env.variables.list();
await env.variables.set('CDN_URL', 'https://cdn.moviespace.com');
await env.variables.unset('CDN_URL');
An unset variable leaves {{CDN_URL}} standing in the response. An empty value substitutes as empty.
The media library is instance-global and hangs off the client.
final poster = await silo.media.upload(MediaUpload(
bytes: bytes,
filename: 'arrival.jpg',
folder: 'posters',
));
// Store the reference, never the URL.
fields['poster'] = poster.reference; // silo://media/<id>
Asset operations:
final asset = await silo.media.get(id);
await asset.rename('arrival-2016.jpg');
await asset.moveTo('posters/2016');
await asset.setTags(['poster']); // replaces the list
// Swaps bytes; keeps id, reference, URL and filename.
await asset.replace(MediaReplace(bytes: bytes, filename: 'arrival-hd.jpg'));
// Refused while an entry references it, unless forced.
await asset.delete();
await asset.delete(const DeleteOptions(force: true));
// Usage information.
final usages = await asset.usages();
usages.total; // the true referrer count
usages.visible; // how many this key may see
Cancellation
CancellationSignal rides package:http’s abortTrigger. Pass a signal to any call.
final signal = CancellationSignal();
signal.cancel(); // or set a deadline
await movies.list(limit: 20, cancellationSignal: signal);
package:http surfaces a cancelled request as a ClientException. The client inspects the signal before interpreting the exception, so cancellation is reported as RequestAbortedException rather than NetworkException.
Search
await movies.search(query: 'arrival'); // one collection
await environment.search(query: 'arrival'); // one environment
await silo.search(query: 'arrival'); // everything the key can read
Error handling
import 'package:silo_client/silo_client.dart';
try {
await movies.replace(movie.id, movie.rev, fields);
} on ConflictException {
final current = await movies.get(movie.id);
await movies.replace(current.id, current.rev, fields);
} on ValidationFailedException catch (e) {
print(e.details);
}
SiloException is the base for anything Silo refused. NetworkException, RequestTimeoutException, RequestAbortedException, and InvalidResponseException are not SiloException — Silo never answered.
Naming conventions
Where Dart forces a difference, the client follows Java for structural conventions. Filter operators use the same names as TypeScript (equals, notEquals, etc.), not Java’s isEqualTo.
| Dart | TypeScript | Reason |
|---|
CollectionCatalog | CollectionPage | follows Java naming |
...Exception | ...Error | Dart/Java exception convention |
Entry types (Entry<F>), untyped filters, and ...Exception names all follow the Java client.
Anonymous access
Omit the key to reach collections whose schema does not require authentication.
final silo = Silo.at('https://cms.moviespace.com');
Optional caching
final silo = Silo(SiloOptions(
url: url,
key: key,
cache: const CacheOptions.on(ttl: Duration(seconds: 30), maxSize: 1000),
));
silo.cache.statistics.hitRate;
silo.cache.clear();
The cache is off by default. Only get and list are cached. A write through this client drops that collection’s cached entries. Clients created with withKey() or withUrl() start with an empty cache.
Source and releases
The Dart client lives in packages/silo-client-dart in the Silo repository and releases independently under silo-client-dart-v* tags. It is published by hand to pub.dev.
route_inventory_drift_test.dart parses the TypeScript route inventory and holds the Dart list equal to it, keeping all three clients in sync as routes are added to Silo.