Documentation Index
Fetch the complete documentation index at: https://docs.cocobase.buzz/llms.txt
Use this file to discover all available pages before exploring further.
Flutter SDK
Complete guide to using Cocobase with Flutter and Dart applications.
Installation
Add Cocobase to your pubspec.yaml:
dependencies:
coco_base_flutter: ^1.0.0
Install dependencies:
Quick Start
Initialize Cocobase
import 'package:coco_base_flutter/coco_base_flutter.dart';
void main() {
final db = Cocobase(
CocobaseConfig(
apiKey: "YOUR_API_KEY",
baseUrl: "https://api.cocobase.buzz", // Optional
authStore: myAuthStore, // Optional - custom token storage
),
);
runApp(MyApp());
}
Basic Operations
// List documents
final docs = await db.listDocuments("posts");
// Get single document
final doc = await db.getDocument("posts", "doc-id");
// Create document
final created = await db.createDocument("posts", {
"title": "My First Post",
"content": "Hello World!"
});
// Update document
await db.updateDocument("posts", "doc-id", {
"title": "Updated Title"
});
// Delete document
await db.deleteDocument("posts", "doc-id");
Querying Data
CocoBase Flutter provides two powerful ways to query your data: Filter Map (simple) and QueryBuilder (advanced).
Method 1: Filter Map (Recommended for Simple Queries)
Pass filters as a simple Map<String, dynamic>:
// Find active users older than 18
final users = await db.listDocuments("users", filters: {
'status': 'active',
'age__gt': 18, // __gt = greater than
});
Why Filter Map?
- ✅ Simple and readable
- ✅ No need to learn QueryBuilder syntax
- ✅ Works with all operators
- ✅ Perfect for beginners
Method 2: QueryBuilder (For Complex Queries)
For more complex queries, use the fluent QueryBuilder:
final users = await db.listDocuments("users",
queryBuilder: QueryBuilder()
.where('status', 'active')
.whereGreaterThan('age', 18)
.whereContains('email', '@gmail.com')
.orderByDesc('createdAt')
.limit(10),
);
Why QueryBuilder?
- 🔗 Chainable methods - Build queries step by step
- 📝 Self-documenting - Method names are clear (
whereGreaterThan, not __gt)
- 🔍 IDE support - Get autocomplete and type hints
- 🎯 Complex queries - Combine multiple conditions easily
Operators Reference
Comparison Operators
| Operator | Filter Map | QueryBuilder Method | Example |
|---|
| Equal | field: value | .where(field, value) | 'age': 25 |
| Greater Than | field__gt | .whereGreaterThan(field, value) | 'age__gt': 18 |
| Greater or Equal | field__gte | .whereGreaterThanOrEqual(field, value) | 'age__gte': 18 |
| Less Than | field__lt | .whereLessThan(field, value) | 'age__lt': 65 |
| Less or Equal | field__lte | .whereLessThanOrEqual(field, value) | 'age__lte': 65 |
| Not Equal | field__ne | .whereNotEqual(field, value) | 'status__ne': 'deleted' |
String Operators
| Operator | Filter Map | QueryBuilder Method | Example |
|---|
| Contains | field__contains | .whereContains(field, value) | 'title__contains': 'flutter' |
| Starts With | field__startswith | .whereStartsWith(field, value) | 'email__startswith': 'admin' |
| Ends With | field__endswith | .whereEndsWith(field, value) | 'domain__endswith': '.com' |
Array/List Operators
| Operator | Filter Map | QueryBuilder Method | Example |
|---|
| In Array | field__in | .whereIn(field, values) | 'status__in': 'active,pending' |
| Not In Array | field__notin | .whereNotIn(field, values) | 'status__notin': 'deleted,archived' |
Special Operators
| Operator | Filter Map | QueryBuilder Method | Example |
|---|
| Is Null | field__isnull | .whereIsNull(field, bool) | 'deletedAt__isnull': true |
OR Queries
CocoBase supports three types of OR queries for different needs.
Type 1: Simple OR Conditions
Use when you want “field1 = value1 OR field2 = value2”:
// Find users with admin role OR with email verified
final query = QueryBuilder()
.or('role', 'admin')
.or('emailVerified', true);
final users = await db.listDocuments("users", queryBuilder: query);
Type 2: Multi-Field Search
Use when you want to search across multiple fields:
// Search for "john" in name, email, or phone
final query = QueryBuilder()
.searchInFields(['name', 'email', 'phone'], 'john');
final users = await db.listDocuments("users", queryBuilder: query);
Type 3: Named OR Groups
Use when you want to group OR conditions:
// Users where: (role=admin OR role=moderator) AND status=active
final query = QueryBuilder()
.orGroup('roleGroup', 'role', 'admin')
.orGroup('roleGroup', 'role', 'moderator')
.where('status', 'active');
final users = await db.listDocuments("users", queryBuilder: query);
Sorting and Pagination
Sorting
Using QueryBuilder:
// Sort by creation date (newest first)
final query = QueryBuilder()
.orderByDesc('createdAt');
// Sort by price (lowest first)
final query = QueryBuilder()
.orderByAsc('price');
// Or use sortBy with explicit order
final query = QueryBuilder()
.sortBy('name', 'asc');
Using Filter Map:
final books = await db.listDocuments("books", filters: {
'orderBy': 'title', // Sort field
'order': 'asc', // Sort direction
});
Using QueryBuilder:
// Get the first 10 documents
final query = QueryBuilder()
.limit(10);
// Skip first 20, get next 10 (page 3)
final query = QueryBuilder()
.limit(10)
.offset(20);
// Aliases available
queryBuilder.take(10); // same as limit(10)
queryBuilder.skip(20); // same as offset(20)
Using Filter Map:
final books = await db.listDocuments("books", filters: {
'limit': 10,
'offset': 20,
});
Field Selection and Population
Select Specific Fields
Include only certain fields in the response:
// Only get title and price, not full document
final query = QueryBuilder()
.select('title')
.select('price');
// Or select multiple at once
final query = QueryBuilder()
.selectAll(['title', 'price', 'author']);
final books = await db.listDocuments("books", queryBuilder: query);
Population (Relationships)
Load related documents automatically:
// Single relationship
final query = QueryBuilder()
.populate('author'); // Load author details
// Multiple relationships
final query = QueryBuilder()
.populateAll(['author', 'publisher']);
final books = await db.listDocuments("books", queryBuilder: query);
Type Conversion & Type Safety
Learn how to work with strongly-typed documents and eliminate null safety issues.
Why Type Safety?
Without Type Safety (Dynamic):
final docs = await db.listDocuments("books"); // Returns dynamic data
// ❌ No autocomplete - what fields exist?
print(docs[0].data['title']); // Might be null, no type checking
// ❌ Easy to make mistakes
print(docs[0].data['titulo']); // Typo - no error at compile time
// ❌ Manual type casting required
final price = (docs[0].data['price'] as double) * 2;
With Type Safety (Converted):
final books = await db.listDocuments<Book>("books");
// ✅ Full autocomplete - IDE knows all fields
print(books[0].data.title); // Perfect!
// ✅ Compile-time type checking
// books[0].data.titulo; // ❌ ERROR: no property 'titulo'
// ✅ No casting needed
final price = books[0].data.price * 2; // Dart knows it's double
Creating Models
Basic Model:
class Book {
final String title;
final String author;
final double price;
Book({
required this.title,
required this.author,
required this.price,
});
// Create from JSON (required for type conversion)
factory Book.fromJson(Map<String, dynamic> json) {
return Book(
title: json['title'] as String,
author: json['author'] as String,
price: (json['price'] as num).toDouble(),
);
}
// Convert back to JSON (required for createDocument)
Map<String, dynamic> toJson() {
return {
'title': title,
'author': author,
'price': price,
};
}
}
Model with Optional Fields:
class Product {
final String name;
final double price;
final String? description; // Optional
final List<String>? tags; // Optional list
Product({
required this.name,
required this.price,
this.description,
this.tags,
});
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
name: json['name'] as String,
price: (json['price'] as num).toDouble(),
description: json['description'] as String?,
tags: (json['tags'] as List<dynamic>?)?.cast<String>(),
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'price': price,
'description': description,
'tags': tags,
};
}
}
Registration Methods
Method 1: Global Registration (Recommended)
Register converters once in your app initialization:
void main() async {
final config = CocobaseConfig(apiKey: "YOUR_KEY");
final db = Cocobase(config);
// Register all your converters here
CocobaseConverters.register<Book>(Book.fromJson);
CocobaseConverters.register<User>(User.fromJson);
CocobaseConverters.register<Product>(Product.fromJson);
runApp(const MyApp());
}
Advantages:
- ✅ Register once, use everywhere
- ✅ Cleaner API calls
- ✅ Best for production code
Method 2: Explicit Converter (Alternative)
Pass converter directly to method:
// Use when you need one-off conversions
final books = await db.listDocuments<Book>(
"books",
converter: Book.fromJson, // Explicit parameter
);
Method 3: Check Before Registering
if (!CocobaseConverters.hasConverter<Book>()) {
CocobaseConverters.register<Book>(Book.fromJson);
}
// Safe to use
final books = await db.listDocuments<Book>("books");
Using Converted Documents
List of Typed Documents:
final books = await db.listDocuments<Book>("books");
// books is List<Document<Book>>
// Each doc.data is a Book instance
for (var doc in books) {
print('ID: ${doc.id}');
print('Title: ${doc.data.title}'); // Type-safe!
print('Price: \$${doc.data.price}');
print('Created: ${doc.createdAt}');
}
Single Typed Document:
final doc = await db.getDocument<Book>("books", "doc-id");
// doc is Document<Book>
// doc.data is a Book instance
print(doc.data.title);
print(doc.data.author);
With Query Filters:
// Type-safe querying
final books = await db.listDocuments<Book>("books",
queryBuilder: QueryBuilder()
.where('status', 'published')
.whereGreaterThan('price', 10)
.populate('author')
.orderByDesc('publishedAt')
.limit(20),
);
// Process with full type safety
for (var book in books) {
print('${book.data.title} - \$${book.data.price}');
}
Creating Documents with Type Safety
When you create new documents, your model class must have a toJson() method:
// Create an instance of your model
final newBook = Book(
title: 'Clean Code',
author: 'Robert Martin',
price: 45.99,
);
// Pass it to createDocument - toJson() is called automatically
final created = await db.createDocument<Book>("books", newBook);
// Result contains the new document ID from the server
print('Created with ID: ${created.id}');
print('Title: ${created.data.title}');
How it works:
- You create a
Book instance with your data
createDocument<Book>() calls toJson() automatically to serialize it
- The API receives the JSON and stores it
- The response is converted back to a
Book instance using fromJson()
- You get a
Document<Book> with the server-assigned ID
Batch Operations
// Batch create - takes List<Map<String, dynamic>>
final result = await db.batchCreateDocuments(
"books",
[
{"title": "Book 1", "author": "Author 1", "price": 19.99},
{"title": "Book 2", "author": "Author 2", "price": 24.99},
],
);
print('Created: ${result.created} documents');
for (var doc in result.documents) {
print('ID: ${doc.id}');
}
// Batch update
final updateResult = await db.batchUpdateDocuments(
"books",
[
{"id": "doc-1", "status": "published"},
{"id": "doc-2", "status": "archived"},
],
);
print('Updated: ${updateResult.updated}');
// Batch delete
final deleteResult = await db.batchDeleteDocuments(
"books",
["doc-id-1", "doc-id-2", "doc-id-3"],
);
print('Deleted: ${deleteResult.deleted}');
Advanced Queries
Count Documents
final count = await db.countDocuments("users",
queryBuilder: QueryBuilder().where("status", "active"),
);
print('Total: ${count.count}');
Aggregate Documents
final result = await db.aggregateDocuments("orders",
field: "price",
operation: "avg", // sum, avg, min, max
filters: {"status": "completed"},
);
print('Result: ${result.result}');
Group By Field
final grouped = await db.groupByField("users",
field: "role",
queryBuilder: QueryBuilder().where("status", "active"),
);
for (var group in grouped.items) {
print('${group.value}: ${group.count} users');
}
Get Collection Schema
final schema = await db.getCollectionSchema("users");
for (var field in schema.fields) {
print('${field.name}: ${field.type}');
}
Upload File
final url = await db.uploadFileFromPath(
filepath: "/path/to/file.jpg",
fileName: "avatar.jpg",
);
print('File URL: $url');
Collection Management
// Create collection
final col = await db.createCollection("my-collection");
// List all collections
final cols = await db.listCollections();
// Get a collection
final col = await db.getCollection("my-collection");
// Update collection name
await db.updateCollection("my-collection", "new-name");
// Delete collection (WARNING: deletes all documents!)
await db.deleteCollection("my-collection");
Advanced Query Examples
Example 1: Search with Filters
Find published books by specific authors:
final books = await db.listDocuments<Book>("books",
queryBuilder: QueryBuilder()
.where('status', 'published')
.searchInFields(['title', 'description'], 'flutter')
.whereIn('authorId', ['auth1', 'auth2', 'auth3'])
.orderByDesc('publishedAt')
.limit(20),
);
Example 2: Price Range Query
Find products in a price range:
final products = await db.listDocuments<Product>("products",
queryBuilder: QueryBuilder()
.whereGreaterThanOrEqual('price', 10)
.whereLessThanOrEqual('price', 100)
.populate('category'),
);
Example 3: Complex OR Logic
Find premium users (verified OR have payment method):
final query = QueryBuilder()
.orGroup('premium', 'emailVerified', true)
.orGroup('premium', 'paymentMethodId__isnull', false)
.where('status', 'active');
final users = await db.listDocuments<User>("users", queryBuilder: query);
Example 4: Using Filter Map for Complex Query
final orders = await db.listDocuments<Order>("orders", filters: {
'status': 'completed',
'totalAmount__gte': 100,
'createdAt__gte': '2024-01-01',
'orderBy': 'createdAt',
'order': 'desc',
'limit': 20,
'offset': 0,
});
Authentication
Initialize Auth
// Restore session from custom auth store on app start
await db.initAuth();
Login
await db.login("user@example.com", "securePassword123");
// Get user after login
final user = await db.getCurrentUser();
print('User ID: ${user.id}');
print('Email: ${user.email}');
Register
await db.register(
"user@example.com",
"securePassword123",
data: {"username": "johndoe"}, // optional
);
// Get user after registration
final user = await db.getCurrentUser();
Logout (synchronous)
Check Authentication
if (db.isAuthenticated()) {
print("User is logged in");
}
Get Current User
final user = await db.getCurrentUser();
Access Current User Object
// db.user holds the cached user object after login/register
print(db.user?.email);
Update User
final updated = await db.updateUser(
data: {"bio": "Updated bio"},
email: "newemail@example.com",
password: "newpassword123",
);
Real-time Data
Watch Collection
// Watch a collection for real-time updates
final conn = db.watchCollection(
"posts",
(event) {
print('Event: ${event['event']}'); // create, update, delete
print('Data: ${event['data']}');
},
connectionName: 'posts-watcher', // optional
onConnected: () => print('Connected!'), // optional
onConnectionError: () => print('Error!'), // optional
);
// Close the connection when done
db.closeConnection(conn);
Building Reusable Queries
Create query builders as functions:
QueryBuilder publishedBooksQuery(String searchTerm) {
return QueryBuilder()
.where('status', 'published')
.searchInFields(['title', 'description'], searchTerm)
.orderByDesc('publishedAt')
.limit(20);
}
// Use it anywhere
final results = await db.listDocuments<Book>("books",
queryBuilder: publishedBooksQuery('flutter'),
);
Debugging Queries
Print the Query String
final query = QueryBuilder()
.where('status', 'active')
.whereGreaterThan('age', 18)
.limit(10);
print(query.build());
// Output: status=active&age__gt=18&limit=10
Best Practices
1. Always Define fromJson() and toJson()
class Book {
// ... fields ...
// Required for reading from API
factory Book.fromJson(Map<String, dynamic> json) { ... }
// Required for creating/updating via API
Map<String, dynamic> toJson() { ... }
}
2. Handle Optional Fields
class Book {
final String? subtitle; // Optional field
factory Book.fromJson(Map<String, dynamic> json) {
return Book(
subtitle: json['subtitle'] as String?, // Can be null
);
}
}
3. Use Type Casting in fromJson()
factory Book.fromJson(Map<String, dynamic> json) {
return Book(
title: json['title'] as String, // Type cast for safety
price: (json['price'] as num).toDouble(), // Handle int or double
age: json['age'] as int,
);
}
4. Register Converters Early
void main() {
// Register all converters at app startup
CocobaseConverters.register<Book>(Book.fromJson);
CocobaseConverters.register<User>(User.fromJson);
CocobaseConverters.register<Post>(Post.fromJson);
runApp(MyApp());
}
5. Use Filter Map for Simple Queries
// Good - simple and readable
final users = await db.listDocuments("users", filters: {
'status': 'active',
'age__gte': 18,
});
// Use QueryBuilder only when you need complex logic
6. Always Limit Results
// Good - always set a limit
final posts = await db.listDocuments("posts", filters: {'limit': 20});
// Bad - could return thousands of documents
final posts = await db.listDocuments("posts");
7. Error Handling
try {
final doc = await db.getDocument<Book>("books", "doc-id");
print(doc.data.title);
} catch (e) {
print('Error: $e');
}
Troubleshooting
Issue: Type mismatch error
Cause: Field type mismatch in fromJson()
// ❌ Wrong - expects String but API returns int
final age = json['age'] as String;
// ✅ Correct
final age = json['age'] as int;
Issue: Converter not registered
Cause: Forgot to register converter
// ✅ Register first
CocobaseConverters.register<Book>(Book.fromJson);
// Now it works
final books = await db.listDocuments<Book>("books");
Issue: Null safety error
Cause: Optional field treated as required
// ✅ Use nullable type
class Book {
final String? subtitle; // Optional field
factory Book.fromJson(Map<String, dynamic> json) {
return Book(
subtitle: json['subtitle'] as String?, // Can be null
);
}
}
Query Limits
- Maximum limit: 1000 documents per request
- Offset range: 0 to 100,000
- Field name length: 255 characters
- Filter value length: 10,000 characters
Next Steps