Instead of SQL, you define your data model by annotating your persistent object types on the programming language level.
ObjectBox - Database Persistence with Entity Annotations
ObjectBox is a database that persists objects, which we sometimes also call entities.
To let ObjectBox know which classes are entities you annotate them with @Entity. This annotation identifies the class User in the following example as a persistable entity. This will trigger ObjectBox to generate persistence code tailored for this class:
@Entity\npublicclassUser{\n\n@Id\nprivatelong id;\n\nprivateString name;\n\n // Not persisted:\n@Transient\nprivateint tempUsageCount;\n\n // TODO: getters and setters.\n}
Note:
It’s often good practice to model entities as “dumb” data classes (POJOs) with just properties.
Entities must have a no-args constructor, or for better performance, a constructor with all properties as arguments. In the above example, a default, no-args constructor is generated by the compiler.
@Entity\ndataclassUser(\n@Idvar id: Long=0,\nvar name: String? =null,\n// Not persisted:\n@Transientvar tempUsageCount: Int=0\n)
Note:
It’s often good practice to model entities as “dumb” data classes (POJOs) with just properties.
Entities must have a no-args constructor, or for better performance, a constructor with all properties as arguments. In the above example, a default, no-args constructor is generated by the compiler.\nFor Kotlin data classes this can be achieved by adding default values for all parameters. (Technically this is only required if adding properties to the class body, like custom or transient properties or relations, but it's a good idea to do it always.)
@Entity()\nclass User {\n // Annotate with @Id() if name isn't \"id\" (case insensitive).\n int id = 0;\n String? name;\n \n // Not persisted:\n @Transient\n int tempUsageCount = 0;\n)
It’s often good practice to model entities as “dumb” data classes with just properties.
For ObjectBox to be able to construct objects read from the database, entities must have a no-args constructor or a constructor with argument names matching the properties, for example:
User({this.id, this.name});
@Entity()\nclass User:\n id = Id\n name = String\n temp_usage_count = None
Available Property types include: Bool, Char, Bytes, String, Int8/16/32/64, Float32/64 *Vector and *List variants (e.g. Float32Vector)
Object IDs: @Id
In ObjectBox, entities must have one 64-bit integer ID property with non-private visibility (or non-private getter and setter method) to efficiently get or reference objects.
@Entity\npublicclassUser{\n@Idpubliclong id;\n // Note: You can use the nullable java.lang.Long, but we do not recommend it.\n\n...\n}
@Entity\ndata class User(\n @Id var id: Long = 0,\n ...\n)
@Entity()\nclass User {\n // Annotate with @Id() if name isn't \"id\" (case insensitive).\n int id;\n\n ...\n}
@Entity()\nclass User:\n id = Id # Use either class Id or instance notation (e.g. Id())
If you need to use another type for IDs (such as a string UID given by a server), model them as regular properties and use queries to look up objects by your application-specific ID. Also, make sure to index the property, and if it's a string use a case-sensitive condition, to speed up lookups. To prevent duplicates it is also possible to enforce a unique value for this secondary ID.
@Entity\nclass StringIdEntity {\n @Id public long id;\n @Index public String uid;\n // Alternatively:\n // @Unique String uid;\n}\n\nStringIdEntity entity = box.query()\n .equal(StringIdEntity_.uid, uid, StringOrder.CASE_SENSITIVE)\n .build().findUnique()
@Entity\ndata class StringIdEntity(\n @Id var id: Long = 0,\n @Index var uid: String? = null\n // Alternatively:\n // @Unique uid: String? = null\n)\n\nval entity = box.query()\n .equal(StringIdEntity_.uid, uid, StringOrder.CASE_SENSITIVE)\n .build().findUnique()
@Entity()\nclass StringIdEntity(\n int id;\n \n @Index() // or alternatively use @Unique()\n String uid;\n)\n\nfinal objects = box.query(StringIdEntity_.uid.equals('...')).build().find();
ID properties are unique and indexed by default.
When you put a new object you do not assign an ID. By default IDs for new objects are assigned by ObjectBox. See the page on Object IDs for details.
ObjectBox needs to access the data of your entity’s properties (e.g. in the generated code). You have two options:
Make sure properties do not have private visibility.
Provide standard getters (your IDE can generate them easily).
To improve performance when ObjectBox constructs your entities, you might also want to provide an all-properties constructor.
@Entity\npublic class Order {\n \n // Option 1: field is not private.\n @Id long id;\n \n // Option 2: field is private, but getter is available.\n private String name;\n \n public ToOne<Customer> customer;\n public ToMany<Order> relatedOrders;\n \n // At minimum, provide a default constructor for ObjectBox.\n public Order() {\n }\n\n // Optional: all-properties constructor for better performance.\n // - make sure type matches exactly,\n // - for ToOne add its virtual ID property instead,\n // - for ToMany add no parameter.\n public Order(long id, String name, long customerId) {\n this.id = id;\n this.name = name;\n this.customer.setTargetId(customerId);\n }\n \n public String getName() {\n return this.name;\n }\n}
// For Kotlin a data class with default values\n// meets all above requirements. \n@Entity\ndata class User(\n @Id var id: Long = 0,\n var name: String? = null\n)
@Entity()\nclass User {\n int id;\n \n String? _name;\n \n String get name {...}\n \n set name(String value) {...}\n \n User(this.id);\n}
Supported property types
ObjectBox supports a wide range of property types including integers, floats, strings, dates, lists, and flex properties for dynamic data. See the dedicated page for details:
@Transient marks properties that should not be persisted. In Java static or transient properties will also not be persisted.
@Transient\nprivate int notPersisted;
@Transient\nvar notPersisted: Int = 0
@Transient()\nint? notPersisted;
NameInDb
Only available for Java/Kotlin at the moment
@NameInDb lets you define a name on the database level for a property. This allows you to rename the property without affecting the property name on the database level.
@NameInDb only works with inline constants to specify a column name.
@NameInDb(\"username\")\nprivate String name;
@NameInDb(\"username\")\nvar name: String? = null
Property Indexes
Annotate a property with @Index to create a database index for the corresponding database column. This can improve performance when querying for that property.
@Index\nprivate String name;
@Index is currently not supported for String[], byte[], float and double
@Index\nvar name: String? = null
@Index is currently not supported for Array<String>, ByteArray , Float and Double
@Index()\nString name;
@Index is currently not supported for double and listsList<String>, List<int>, Uint8List, Int8List
@Entity()\nclass User:\n name = String(index=Index())
An index stores additional information in the database to make lookups faster. As an analogy, we could look at Java-like programming languages where you store objects in a list. For example, you could store persons using a List<Person>. Now, you want to search for all persons with a specific name so you would iterate through the list and check for the name property of each object. This is an O(N) operation and thus doesn't scale well with an increasing number of objects. To make this more scalable you can introduce a second data structure Map<String, Person> with the name as a key. This will give you a constant lookup time (O(1)). The downside of this is that it needs more resources (here: RAM) and slows down add/remove operations on the list a bit. These principles can be transferred to database indexes, just that the primary resource consumed is disk space.
Index types (String)
For scalar properties, ObjectBox uses a value-based index. Because String properties typically require more storage space than scalar values, by default ObjectBox uses a hash index for strings instead.
To override the default and use a value-based index for a String property, specify the index type:
Keep in mind that for String, depending on the length of your values, a value-based index may require more storage space than the default hash-based index.
ObjectBox supports these index types:
Not specified or DEFAULT Uses the best index based on property type (HASH for String, VALUE for others).
VALUE Uses property values to build the index. For String, this may require more storage than a hash-based index.
HASH Uses a 32-bit hash of property values to build the index. Occasional collisions may occur which should not have any performance impact in practice. Usually, a better choice than HASH64, as it requires less storage.
HASH64 Uses a 64-bit hash of property values to build the index. Requires more storage than HASH and thus should not be the first choice in most cases.
Limits of hash-based indexes: Hashes work great for equality checks, but not for \"starts with\" type conditions. If you frequently use those, you should use value-based indexes instead.
Vector Index for Nearest Neighbor Search
To enable nearest neighbor search, a special index type for vector properties is available:
The REPLACE strategy will add a new object with a different ID. As relations (ToOne/ToMany) reference objects by ID, if the previous object was referenced in any relations, these need to be updated manually.
Unique constraints are based on an index, so it is possible to further configure the index with an @Index annotation.
Change database type
Use @Type in Java/Kotlin or the type attribute on @Property in Dart to override how the value of a property is stored and interpreted in the database.
// Store 64-bit integer as time in nanoseconds.\n@Type(DatabaseType.DateNano)\nprivate long timeInNanos;
// Store 64-bit integer as time in nanoseconds.\n@Type(DatabaseType.DateNano)\nvar timeInNanos: Long = 0
ObjectBox supports a variety of data types inside the stored objects including various list types and even special \"flex\" types for dynamic and schema-less data.
ObjectBox stores data as objects, which are instances of entity classes you define. Objects consist of properties, which are the individual fields that hold your data, like a user's name or age. This page covers the property types ObjectBox supports:
Standard types for single values of a fixed type
List/Vector types for collections of values of a fixed type
The flex type for dynamic and schema-less data
Standard Types
Standard types include boolean, integers, floating point types, string, and date types. These are the most common types you will use in typical applications.
Quick Reference
Category
ObjectBox Type
Stored as
Boolean
Bool
1 byte
Integer (8-bit)
Byte
1 byte
Integer (16-bit)
Short
2 bytes
Integer (32-bit)
Int
4 bytes
Integer (64-bit)
Long
8 bytes
To-One Relation
Relation
8 bytes
Float (32-bit)
Float
4 bytes
Float (64-bit)
Double
8 bytes
Text Character
Char
2 bytes
String
String
UTF-8 bytes
Date (ms)
Date
8 bytes (long)
Date (ns)
DateNano
8 bytes (long)
Nullable types: All ObjectBox property types support null values. In Java, use wrapper classes (Integer vs int) for nullable types. In Kotlin, Dart, and Python, nullability is part of the type system.
Integers
ObjectBox supports all standard integer types, from 8-bit to 64-bit. Rule of thumb: use large enough integer types that are safe for your use case in the future. Use smaller types only if you are certain about the range of values as you cannot change the type of an existing property later.
@Entity\npublic class Sensor {\n @Id public long id;\n \n public byte status; // -128 to 127\n public short temperature; // -32,768 to 32,767 \n public int count; // ~2 billion range\n public long timestamp; // Large numbers, IDs\n}
@Entity\ndata class Sensor(\n @Id var id: Long = 0,\n var status: Byte = 0,\n var temperature: Short = 0,\n var count: Int = 0,\n var timestamp: Long = 0\n)
Kotlin unsigned types (UByte, UShort, UInt, ULong) are also supported—they're stored as their signed equivalents.
Note: Dart just has int and does not differentiate between the integer types. You can still use ObjectBox types to pick the storage type and align with other languages (e.g. when using Sync).
@Entity()\nclass Sensor {\n @Id()\n int id = 0;\n \n @Property(type: PropertyType.byte)\n int status = 0; // 8-bit\n \n @Property(type: PropertyType.short)\n int temperature = 0; // 16-bit\n \n @Property(type: PropertyType.int)\n int count = 0; // 32-bit\n \n int timestamp = 0; // 64-bit (default)\n}
@Entity()\nclass Sensor:\n id = Id\n status = Int8\n temperature = Int16\n count = Int32\n timestamp = Int64
Floating Point
Choose 32-bit floats for memory efficiency when ~7 digits of precision is enough. Use 64-bit doubles when you need ~15 digits.
Strings are stored as UTF-8 encoded bytes without a length limit.
public String name;\npublic String description; // Can be very long
Dates and Times
ObjectBox stores dates as 64-bit integers (\"timestamps\") representing time since the Unix epoch (January 1, 1970, 00:00:00 UTC). Choose your precision:
Precision
ObjectBox Type
Use case
Milliseconds
Date (the default)
Most applications
Nanoseconds
DateNano
High-precision timestamps
Note: while DateNano values are always stored as nanoseconds, the actual precision of the values depends on the platform and programming language. For example, Dart uses only microsecond precision (the last three digits of the nanosecond are \"truncated\").
// Millisecond precision (default)\npublic Date createdAt;\n\n// Nanosecond precision\n@Type(DatabaseType.DateNano)\npublic long preciseTimestamp;
// Millisecond precision (default)\n@Property(type: PropertyType.dateUtc)\nDateTime? createdAt;\n\n// Stored with nanosecond precision; however,\n// Dart uses only microsecond precision! \n@Property(type: PropertyType.dateNanoUtc)\nDateTime? preciseTimestamp;
Proper UTC handling requires ObjectBox for Dart/Flutter 5.1 with the dateUtc and dateNanoUtc annotations. Prior versions do not support proper UTC handling. Please update and avoid the old date and dateNano annotations.
IDs and Relations
Every ObjectBox entity must have exactly one ID property of type Long (64-bit integer). See Entity Annotations for details.
\"Relation\" is used for one of two relation types available in ObjectBox. It's a 64-bit integer that references the ID of another object. Depending on the programming language you use, it may translate to a \"to-one\" construct. See Relations for complete documentation.
Lists and Arrays
Store collections of values directly; no special tables or joins required. The internal type names are \"vectors\", but they may translate to lists or arrays in the programming language you use.
Binary data is stored as a ByteVector. In other databases, this is may be called a \"BLOB\" (Binary Large Object).
For vector search: Float vectors used for embeddings and similarity search have special indexing support. See On-Device Vector Search.
Flex Properties
Flex properties can hold data values of various types, including complex structures, in a single property. They serve two main purposes:
Dynamic data: you want to process JSON-like data dynamically
Nested objects: as an alternative to relations, you can embed map-like data directly
When syncing with MongoDB, flex properties can hold nested documents.
@Entity\npublic class Customer {\n @Id long id;\n\n // Stores any supported type at runtime\n @Nullable Object tag;\n\n // Or explicitly use a String map\n @Nullable Map<String, Object> stringMap;\n\n // Or a list\n @Nullable List<Object> flexList;\n \n public Customer(Object tag) {\n this.id = 0;\n this.tag = tag;\n }\n \n public Customer() {} // For ObjectBox\n}\n\nCustomer customerStrTag = new Customer(\"string-tag\");\nCustomer customerIntTag = new Customer(1234);\nbox.put(customerStrTag, customerIntTag);
@Entity\ndata class Customer(\n @Id var id: Long = 0,\n\n // Stores any supported type at runtime\n var tag: Any? = null,\n\n // Or explicitly use a String map\n var stringMap: MutableMap<String, Any?>? = null\n\n // Or a list\n var flexList: MutableList<Any?>? = null\n)\n\nval customerStrTag = Customer(tag = \"string-tag\")\nval customerIntTag = Customer(tag = 1234)\nbox.put(customerStrTag, customerIntTag)
@Entity()\nclass Customer {\n @Id()\n int id = 0;\n \n // Stores any supported type at runtime\n dynamic tag;\n \n // Or explicitly use a String map (JSON-like)\n Map<String, dynamic>? stringMap;\n \n // Or a list with mixed types\n List<dynamic>? flexList;\n \n // Or a list of maps\n List<Map<String, dynamic>>? nestedList;\n}\n\nfinal customerStrTag = Customer()..tag = \"string-tag\";\nfinal customerIntTag = Customer()..tag = 1234;\nbox.putMany([customerStrTag, customerIntTag]);
Relic is a modern, high-performance web server framework inspired by Shelf and built by the Serverpod team to meet the demanding requirements of modern web applications. Relic provides a robust, type-safe, and efficient foundation for building scalable web services.
While Shelf has been a solid foundation for Dart web applications, several areas for improvement were identified. Rather than work around these limitations, Relic was created as a next-generation framework that maintains Shelf's familiar structure while delivering significant enhancements.
Relic aims to be a comprehensive web server framework for Dart. It provides a wide range of features out of the box and allows you to extend it with middleware. Here are some of the key features:
\n\n
🔒 Type Safe
Strongly typed APIs with validated HTTP headers.
⚡ Fast
High performance through trie-based routing and LRU caching.
🔌 WebSockets
Built-in WebSocket support for real time communication.
🔧 Middleware
Composable middleware pipeline with router integration.
👨💻 Dev Ready
Great IDE support, clear errors, and full documentation.
🚀 Production
Production-ready with high test coverage (3,000+ tests).
Relic started out as a clone of Shelf, but has since diverged. An adapter is being built so that you can use Shelf middleware with Relic. Here is a comparison of some of the key differences:
Once you have Dart installed, it only takes a few lines of code to set up your Relic server. These are the steps you need to take to get a simple Hello world server up and running.
import'package:relic/relic.dart'; /// A simple 'Hello World' server demonstrating basic Relic usage. Future<void>main()async{ // Setup the app. final app =RelicApp() // Route with parameters (:name & :age). ..get('/user/:name/age/:age', helloHandler) // Middleware on all paths below '/'. ..use('/',logRequests()) // Custom fallback - optional (default is 404 Not Found). ..fallback =respondWith( (_)=>Response.notFound( body:Body.fromString(\"Sorry, that doesn't compute.\\n\"), ), ); // Start the server (defaults to using port 8080). await app.serve(); } /// Handles requests to the hello endpoint with path parameters. ResponsehelloHandler(finalRequest req){ final name = req.rawPathParameters[#name]; final age = int.parse(req.rawPathParameters[#age]!); returnResponse.ok( body:Body.fromString('Hello, $name! To think you are $age years old.\\n'), ); }
\n
What this code does:
\n\n
Router and route: RelicApp() configures routing and registers a /user route.
\n
Middleware: use('/', logRequests()) logs each request for all paths under /.
\n
Server and fallback: app.serve() starts on port 8080; fallback handles unmatched routes and returns a 404 (not found).
\n\n
The result is a server that responds with a personalized greeting when you send a GET request matching /user/:name/age/:age.
Then, open your browser and visit http://localhost:8080/user/Nova/age/27, or use curl:
\n
curl http://localhost:8080/user/Nova/age/37
\n
You should see:
\n
Hello Nova! To think you are 27 years old.
\n
Congratulations! You just ran your first Relic server.
\n
tip
Relic works with hot reload out of the box. ⚡️ To use it, start your server in Debug mode from your IDE (or use dart run --enable-vm-service bin/hello_world.dart). Ensure that Hot Reload On Save is enabled in your IDE settings (typically, set it to manual which will trigger hot reload when you save your file).