Realm Sync vs Firebase in 2023

Hello, everyone! Apologies if this is yet another discussion of the topic, but all threads I found are relatively outdated, and it looks like many things have changed.

I am researching a solution for adding sync functionality to an existing app that exists independently on iOS and Android. The app has ~1m users and is currently fully offline. Android app is built on Sqlite + Room, iOS uses Realm offline. My first choice was Firebase, but then I downloaded Realm’s Kotlin template app and was very pleased by how it uses modern API’s like coroutines, flows, and how fast it works offline. That kind of made me look deeper into Realm and frankly, now I’m torn apart :smiley:

Realm has very strong points for first-class offline support. That is exactly what I’m looking for. I want the app to continue being fully functional offline, with sync being only available to users who have upgraded to paid tier app. Firebase-only solution would force me to always have an anonymous user, which means that even my free users would incur potentially significant costs. I want to understand how easy it would be to avoid with Realm. Do I understand correctly that I will need to keep a separate Realm for free users, and then make it synced when they update/create account? What if I want to only sync some data for users, and keep some on the device? Would it be possible to have two realms side by side simultaneously?

One significant issue that puts me off using Realm was that adding realm to my app has added ~7mb apk size for each ABI split. That would basically double my app size and I pride myself on having a small APK. Is there any way to reduce this, are there any plans for reducing the size of bundled native libraries?

Another thing is I’m looking to offload some of the duplicate code for the app to the server, specifically daily aggregation of statistics for each user (it’s an education app, and every time user answers a question, I store an entry and then I aggregate them and also create daily exercises for users based on their answers). I am quite familiar with GCP ecosystem but don’t know much about MongoDB ecosystem. Would that be possible to do fully on the server? Do I understand correctly that Atlas Functions can help me with this? Would it be possible to verify purchases on the backend using both Google Play and App Store APIs?

I’m also considering building the entire data layer in apps using KMM if I go Realm way, for which Firebase doesn’t seem to have an out of the box support, but wondering how truly production ready that is for iOS + Android?

Apologies for so many questions, just want to gain a full understanding before committing. :slight_smile:

Hi, first off, I am glad you have been having a great experience using Realm and Atlas Device Sync so far. Ill try to answer your questions in the order you asked them.

Do I understand correctly that I will need to keep a separate Realm for free users, and then make it synced when they update/create account? What if I want to only sync some data for users, and keep some on the device? Would it be possible to have two realms side by side simultaneously?

Yes, we have many users that want to sync some data but not sync other data and just let it live locally on the device. The best way to do this is to just open a local-only realm and a sync-only realm (just initialize 2 different realms). As for the “upgrade”, you are correct that it will involve taking the objects in the local realm and inserting them into the new “synced” realm. This should be relatively easy with the Kotlin SDK. Additionally, I think you can use the copyToRealm() function to help with some of the logic.

One significant issue that puts me off using Realm was that adding realm to my app has added ~7mb apk size for each ABI split. That would basically double my app size and I pride myself on having a small APK. Is there any way to reduce this, are there any plans for reducing the size of bundled native libraries?

I do not have the full information on this, but I will ask a member of the Kotlin team to respond to this. I do know that sometimes the issue here is in the differences between the production library and the debug library (where the latter is much larger), but I will let the Kotlin team respond to this one.

Would that be possible to do fully on the server? Do I understand correctly that Atlas Functions can help me with this?

Yes, you can use Atlas Functions (https://www.mongodb.com/docs/atlas/app-services/functions/) to deduplicate some logic and have them be run in a serverless environment where you just pay for use. You can call functions directly from the SDK (https://www.mongodb.com/docs/realm/sdk/kotlin/app-services/call-function/). Additionally, you can run functions in reaction to operations on your collections (see Database Triggers) or on a CRON schedule (Scheduled Triggers).

Please let me know if you have any other questions!
Tyler

1 Like

Hello. Thank you for your detailed response.

I’d like to clarify that I was talking specifically about the release APK, which was also minified with R8. Debug APK was significantly larger indeed.

One thing that confused me about Functions was this quote from the documentation here: “Common use cases include low-latency, short-running tasks”. What does short-running mean in this case? I imagine running some data aggregation task for each user daily will not be that short-running and that of course also depends on the amount of users.

Thanks,
Alex.

Hi, like I said, I will let the APK size questions be answered by the Realm SDK team who I have sent this to.

As for your question on functions, I think the idea here is that Atlas Functions functions are designed to be short-running tasks (do some database calls, send some HTTP requests, hit a Twilio API, etc). For this reason, function execution is limited to around 2 minutes I believe.

I think this quote from the docs is meant to distinguish us from another service like Lambda which is designed for these kinds of longer-running scripts/actions. Our functions are best used to replace application logic that you would otherwise write and manage in your own backend.

Does that answer your question?

Yes, thank you, that is very useful.

I just clarified about the release build as that could be helpful info for the SDK team. Looking forward to hearing back from them :slight_smile:

Hi, just a quick clarification on my end. Functions are limited to 4 minutes: https://www.mongodb.com/docs/atlas/app-services/functions/#constraints

Hi @TheHiddenDuck

Regarding the size of the APK, it is true that the current size of the library is bigger than we would like and the reason is our native code.

There are a couple of reasons for this:

  • We include a static STL to avoid conflicts with other native code or the device not supporting our library. This is following Google best practices and we haven’t really considered allowing users to control this, but it would save 500kb-ish if we did,
  • We are currently shipping native SSL support because the Sync network code runs in C++, but we are working towards a unified network stack where all network code runs on Android/iOS instead of in C++. This will allow us to remove our SSL dependency and save ~1-2 MB. This is on the medium roadmap though, so the timeline is probably something like 6 months.
  • We continuously evaluate our compiler flags to ensure a good tradeoff between size and performance, unfortunately, they are often at odds. But last we tested it, being fully aggressive on size only saved a couple of hundred of kilobytes.

So we have some ideas for shaving down the size, but realistically I wouldn’t expect us to get below 5MB for arm64. I realize this might not be what you wanted to hear and ultimately the tradeoff is up to you. There are advantages to shipping the native code though:

  • With SQLite, you are stuck with whatever is on the device and some advanced features are not available on all devices. Also, your tests might not use the same version of SQLite as the devices has. With Realm you can ship all our features to all devices and testing is done on the same version.
  • Realm ships with inbuilt encryption support, doing the same for SQLite would require you to ship binaries roughly the same size as Realm.

I hope this answers your questions?

Best,
Christian (Kotlin Team Lead)

2 Likes

Ups missed this:

I’m also considering building the entire data layer in apps using KMM if I go Realm way, for which Firebase doesn’t seem to have an out of the box support, but wondering how truly production ready that is for iOS + Android?

Yes, KMM (iOS + Android) support should be just as stable and production ready as Android support alone. It is the same code.

1 Like

Nice to see one more KMM enthusiast here. Sharing a few of my repo’s for your reference

1 Like

Hi Christian! Thank you for the detailed response. Of course, I understand it’s challenging to reduce the native code size, but happy to hear that it is on your radar and that improvements are being made! Even 1-2 mb is a significant cut!

Nice! Thank you for sharing! :raised_hands:

Hello @TheHiddenDuck great seeing you! lol You know me by my other username in the other place.

I commonly go back and forth between both Firebase and Realm, being formerly a Realm TSE I can every interview ends up going in the direction of questions you ask. I’m not only going to answer your questions with the typical marketing “yeah you can, yada yada” but I’m actually going to give you examples.

Anyways, I actually get this question a ton in interviews of Realm vs Firebase, and here’s my take on this and in relation to your concerns:

Sure, I’d be happy to provide examples in C#!

First, regarding Realm’s offline support, you can definitely use Realm to create a fully functional offline app that only syncs data for paid users. To do this, you can create two separate Realms - one for free users and one for paid users - and only sync the paid user’s Realm. Here’s an example of how you could do this:

// Create a local, unsynced Realm for free users
var freeRealmConfig = new RealmConfiguration { IsReadOnly = false };
var freeRealm = Realm.GetInstance(freeRealmConfig);

// Create a synced Realm for paid users
var user = await AuthenticatePaidUser(); // authenticate the user however you choose
var syncConfig = new SyncConfiguration(user, realmServerUrl);
var paidRealm = await Realm.GetInstanceAsync(syncConfig);

With this setup, the freeRealm will only exist on the user’s device and won’t be synced to the server. The paidRealm, on the other hand, will be synced to the server and only be available to authenticated paid users.

If you want to only sync some data for users and keep some on the device, you can use Realm’s partitioning feature to create separate partitions for synced and unsynced data. Here’s an example:

// Create a local, unsynced Realm for all users
var localConfig = new RealmConfiguration { IsReadOnly = false };
var localRealm = Realm.GetInstance(localConfig);

// Create a synced Realm for paid users with a partition key of "paid"
var user = await AuthenticatePaidUser();
var syncConfig = new SyncConfiguration(user, realmServerUrl) { PartitionValue = "paid" };
var paidRealm = await Realm.GetInstanceAsync(syncConfig);

// Create a synced Realm for some data with a partition key of "shared"
var sharedSyncConfig = new SyncConfiguration(user, realmServerUrl) { PartitionValue = "shared" };
var sharedRealm = await Realm.GetInstanceAsync(sharedSyncConfig);

With this setup, the localRealm will only exist on the user’s device and won’t be synced to the server. The paidRealm will be synced to the server and only contain data with a partition key of “paid”, which can be used to store data that only paid users should have access to. The sharedRealm will also be synced to the server, but contain data with a partition key of “shared”, which can be used to store data that should be available to all users.

Regarding the APK size increase when adding Realm to your app, there are a few things you can do to reduce the size. First, you can use APK splits to only include the native libraries for the ABIs that your app supports. Here’s an example:

// In your project file, define the ABIs you support
<ItemGroup>
  <SupportedAbis Include="armeabi-v7a" />
  <SupportedAbis Include="arm64-v8a" />
  <SupportedAbis Include="x86" />
  <SupportedAbis Include="x86_64" />
</ItemGroup>

// In your AndroidManifest.xml file, enable APK splits
<manifest ...>
  <dist:module dist:onDemand="true" />
  <dist:split dist:abi="armeabi-v7a" />
  <dist:split dist:abi="arm64-v8a" />
  <dist:split dist:abi="x86" />
  <dist:split dist:abi="x86_64" />
</manifest>

By using APK splits, you can significantly reduce the size of your app by only including the native libraries for the ABIs that your app actually supports.

Another thing you can do is to leverage Realm’s built-in partitioning feature to separate data between free and paid users. Partitioning allows you to logically separate data in a Realm database and control access to it based on a partition key. In your case, you could use a partition key to separate data for free and paid users, and then only synchronize the data for paid users.

Regarding the size of the bundled native libraries, Realm does offer a feature called “fat APK” splitting, which can significantly reduce the size of the APK. This feature splits the native libraries into multiple APKs, one per CPU architecture, and downloads only the required APK at runtime. This way, users only download the native libraries that they need, instead of downloading everything. You can read more about fat APK splitting in the Realm documentation.

As for offloading some of the duplicate code to the server, MongoDB Atlas Functions can indeed help you with that. Atlas Functions are serverless functions that allow you to run JavaScript code directly in MongoDB Atlas, using triggers like database events, HTTP requests, or scheduled intervals. You can use Atlas Functions to implement your daily aggregation of statistics and exercises, as well as to verify purchases using Google Play and App Store APIs. You can read more about Atlas Functions in the MongoDB documentation.

Finally, building the entire data layer in apps using KMM is definitely a viable option, especially if you are already considering using Kotlin and Realm. KMM allows you to share business logic and data models across multiple platforms, while still using native UI components and frameworks. You can read more about KMM in the Kotlin documentation. Keep in mind, however, that KMM is still a relatively new technology, and you may encounter some limitations or issues as you develop your app.

Regarding your question about verifying purchases on the backend using both Google Play and App Store APIs, it is definitely possible to do so. Both Google Play and App Store provide APIs that allow you to verify purchases made by users in your app. With Google Play, you can use the Google Play Developer API to verify purchases, while with App Store, you can use the StoreKit API.

As for your question about KMM, while KMM is a relatively new technology, it has been gaining popularity and adoption in the mobile development community. KMM provides a way to share code between iOS and Android, allowing for faster development, easier maintenance, and more consistent behavior across platforms. While it may not have out-of-the-box support for Firebase, there are ways to integrate Firebase with KMM, such as using a common Kotlin module. However, as with any new technology, there may be limitations or issues that you may encounter during development.

Overall, it seems like you have a lot of options to consider for adding sync functionality to your app. Firebase and Realm both offer strong offline support, but come with their own tradeoffs and considerations. If you are already familiar with the GCP ecosystem, using MongoDB may require a bit of a learning curve, but it could provide a way to offload some of the duplicate code to the server. Similarly, using KMM may require some additional setup and configuration, but it could provide a way to share code between platforms and speed up development. Ultimately, the best solution will depend on your specific needs and requirements, so it may be helpful to experiment with different options and see which one works best for your use case.

In conclusion, both Firebase and Realm offer strong offline support for mobile apps, but they have different strengths and limitations.

Firebase offers a comprehensive suite of tools and services for app development, including real-time database, authentication, cloud messaging, and more. It provides a straightforward way to add sync functionality to your app, but it may incur costs for anonymous users and may not be as fast as Realm when working offline.

Realm, on the other hand, provides a first-class offline experience and modern APIs such as coroutines and flows. It allows you to keep your app fully functional offline and sync data only for paid users. However, adding Realm to your app can significantly increase the APK size, and it may require more effort to set up compared to Firebase.

If you are familiar with GCP and want to offload some of the duplicate code to the server, MongoDB Atlas Functions can be a good option to consider. It allows you to run serverless functions on Atlas to perform data aggregation and other tasks.

When it comes to building the entire data layer using KMM, it can be a viable option if you are comfortable with Kotlin and want to share code between iOS and Android. Keep in mind, however, that KMM is still a relatively new technology, and you may encounter some limitations or issues as you develop your app.

Ultimately, the best solution will depend on your specific needs and requirements, so it may be helpful to experiment with different options and see which one works best for your use case.

Anything else, feel free to ask.

@TheHiddenDuck

I rewrote sample codes in Kotlin for you:

// Create a local, unsynced Realm for free users
val freeRealmConfig = RealmConfiguration.Builder()
.readOnly(false)
.build()
val freeRealm = Realm.getInstance(freeRealmConfig)

// Create a synced Realm for paid users
val user = authenticatePaidUser() // authenticate the user however you choose
val syncConfig = SyncConfiguration.Builder(user, realmServerUrl)
.build()
val paidRealm = Realm.getInstanceAsync(syncConfig).await()
// Create a local, unsynced Realm for all users
val localConfig = RealmConfiguration.Builder()
.readOnly(false)
.build()
val localRealm = Realm.getInstance(localConfig)

// Create a synced Realm for paid users with a partition key of "paid"
val user = authenticatePaidUser()
val syncConfig = SyncConfiguration.Builder(user, realmServerUrl)
.partitionValue("paid")
.build()
val paidRealm = Realm.getInstanceAsync(syncConfig)

// Create a synced Realm for some data with a partition key of "shared"
val sharedSyncConfig = SyncConfiguration.Builder(user, realmServerUrl)
.partitionValue("shared")
.build()
val sharedRealm = Realm.getInstanceAsync(sharedSyncConfig)

And getting more into your Kotlin Specific cases:

Yes, Realm adds significant size to the app, especially when multiple ABI splits are needed. However, there are a few ways to reduce the size of bundled native libraries:

  1. Enable Proguard or R8 obfuscation and minification, which can remove unused code and shrink the size of the library.

  2. Use the @Keep annotation on classes and methods that you want to keep in the code, which prevents them from being removed during the build process.

  3. Use ABI filters to only include the native libraries for the specific ABIs that your app is targeting. For example, if your app only targets ARM and ARM64, you can exclude the x86 and x86_64 libraries to reduce the size.

Here’s an example of how to use ABI filters in your build.gradle file:

android {
    defaultConfig {
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a'
        }
    }
}

By only including the armeabi-v7a and arm64-v8a ABIs, you can significantly reduce the size of the bundled native libraries.

Overall, while Realm does add some additional size to your app, it’s important to weigh the benefits of its offline-first features against the impact on APK size. But needless to say you’re not the only person to have problems with how big Realm is lol.

Also @TheHiddenDuck This is how you can do it in React.Native instead, I’m not sure what type of app you’re making, but React.Native is probably the easiest for apps if you don’t have to use a lot of device hardware features.

// Create a local, unsynced Realm for free users
const freeRealmConfig = new Realm.Configuration({
readOnly: false,
});
const freeRealm = new Realm(freeRealmConfig);

// Create a synced Realm for paid users
const user = authenticatePaidUser(); // authenticate the user however you choose
const syncConfig = new Realm.Sync.Configuration({
user: user,
server: realmServerUrl,
});
const paidRealm = await Realm.open(syncConfig);

// Create a local, unsynced Realm for all users
const localConfig = new Realm.Configuration({
readOnly: false,
});
const localRealm = new Realm(localConfig);

// Create a synced Realm for paid users with a partition key of "paid"
const user = authenticatePaidUser();
const syncConfig = new Realm.Sync.Configuration({
user: user,
server: realmServerUrl,
partitionValue: "paid",
});
const paidRealm = await Realm.open(syncConfig);

// Create a synced Realm for some data with a partition key of "shared"
const sharedSyncConfig = new Realm.Sync.Configuration({
user: user,
server: realmServerUrl,
partitionValue: "shared",
});
const sharedRealm = await Realm.open(sharedSyncConfig);

Also I do want to make it clear, React Native is flat out the easiest to use to reduce the size of Realm.

Here’s an example of how to reduce the size of the bundled native libraries for Realm in React Native:

For iOS:

  1. Open your project in Xcode.
  2. Click on your project in the navigator to open project settings.
  3. Click on the “Build Settings” tab.
  4. Find the “Other Linker Flags” setting and add “-ObjC”.
  5. Find the “Enable Bitcode” setting and set it to “No”.

For Android:

  1. In your project’s build.gradle file, add the following code to the android block:
android {
  ...
  defaultConfig {
    ...
    ndk {
      abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
    }
  }
}
  1. In the same build.gradle file, add the following code to the dependencies block:
dependencies {
  ...
  implementation "io.realm:realm-android-library:${realmVersion}"
  implementation "io.realm:realm-annotations:${realmVersion}"
  implementation "io.realm:realm-gradle-plugin:${realmVersion}"
  implementation "io.realm:realm-sync:${realmVersion}"
  implementation "io.realm:realm-kotlin:${realmVersion}"
}
  1. In the gradle.properties file in your project’s root directory, add the following code:
android.useAndroidX=true
android.enableJetifier=true
realmVersion=10.7.2

Make sure to replace realmVersion with the version of Realm you’re using.

These changes should help reduce the size of the bundled native libraries for Realm in your React Native app.

Hello! Thank you for your detailed responses; that is very useful! Although I’m a bit intrigued now, what that other place and username could be :smiley: