My Query-Based Sync ToDo App

Prerequisites:

Before we get started we need a few things setup; the prerequisites for this project are:

  • Xcode 9.0 or later.

  • An environment set up to run React Native applications. If you don't have that, please follow the React Native instructions to set up your environment before you do more here.

  • The URL of your Realm Cloud instance (you can get it by logging in to the cloud portal and clicking the Copy Instance URL link).

  • Realm Studio (optional) -- throughout the tutorial we will test components of the sync process where directly viewing and editing the Realm Object Server is helpful.

Overview:

We will make a simple work management application called "ToDo", which will allow users to create "projects". Within each project users will be able to create any number of associated "tasks".

Realm works by defining a data model that configures the schema of a "realm". A realm is an instance of a Realm Mobile Database container. In this project, our client realm will connect and sync data with the centralized Realm Object Server through a process called query-based synchronization.

Step 1: Create a React Native Project

Navigate to your desktop or a directory your would like to save our project to. On the command line create a new React Native project. Let's name it "ToDo".

react-native init ToDo

This will create the basic application structure and a package.json file for our dependencies.

Before we dive into adding code, it's a good idea to make sure that the environment is configured correctly and that we can successfully compile the app template we just created.

Navigate to the project directory > ios and open ToDo.xcodeproj. Start the new application by selecting a simulator and clicking Build/Run icon. Your new app should look like the screen below.

Step 2: Install the Realm Framework

To use Realm Cloud in your React Native app, you'll need to add the Realm framework to the project. This can most easily be accomplished using NPM.

Inside your project directory, add the realm package with the command:

npm install --save realm

Next, link your project to the realm native module:

react-native link realm

If you receive a node-pre-gyp install --fallback-to-build error when running your app, it is possible your version of Node is incompatible with Realm. Either upgrade or downgrade your installation to the latest LTS.

Step 3: Realm Model and Application overview

From this point you can use a text editor of your choice to edit the JS files directly (we recommend VS code). In your project directory create a new folder called src. Open the folder and create a schemas.js file. Just like any other database system, we will need to give each property a type, and establish a primary key to reference specific objects. Copy the following models into our newly created schema.js file:

It is important that you use the 'copy to clipboard' link in the top right of each snippet instead of highlighting and copying. This prevents formatting errors.

schemas.js
export const Project = {
name: "Project",
primaryKey: "projectId",
properties: {
projectId: "string",
owner: "string",
name: "string",
timestamp: "date",
items: "Item[]"
}
};
export const Item = {
name: "Item",
primaryKey: "itemId",
properties: {
itemId: "string",
body: "string",
isDone: "bool",
timestamp: "date"
}
};

Step 4: Create a Constants File and Set the Realm Instance Address

Create a new javascript file called constants.js inside thesrc directory. Open constants.js and paste in the code snippet below. Replace MY_INSTANCE_ADDRESS with your own Realm Object Server address. We will access this file later when we configure our first realm.

constants.js
// **** Realm Cloud Users:
// **** Replace MY_INSTANCE_ADDRESS with the hostname of your cloud instance
// **** e.g., "mycoolapp.us1.cloud.realm.io"
// ****
// ****
// **** ROS On-Premises Users
// **** Replace the SERVER_URL string with the fully qualified versions of
// **** address of your ROS server, e.g.: "http://127.0.0.1:9080".
const MY_INSTANCE_ADDRESS = "YOUR-INSTANCE-ID.cloud.realm.io"; // <- update this
export const SERVER_URL = `https://${MY_INSTANCE_ADDRESS}`;

NOTE: The Realm Cloud Portal presents fully specified URLs (e.g., https://appname.cloud.realm.io); be sure to paste in only the host name part (e.g., appname.cloud.realm.io) into your copy of the constants.js file.

Step 5: Adding a Login Form Dialogue

Next we are going to create a welcome page where we will authenticate the user. This will be the page you see when the app starts up and will be used to log in to the app and connect your application to the Realm Cloud. Inside our src directory create a new folder called components. This will contain all of our components and pages.

Before we set out to complete our LoginForm component, which will house our authentication logic, we need to create two other components which the LoginForm will depend on--a custom ModalView and a Button.

Creating the ModalView:

Create a new javascript file called ModalView.js inside of our new components folder. We will greet the user with a modal asking for a login nickname that we will use to connect to the Realm Object Server. To do this we will need to import modals from the React Native Community:

npm install --save react-native-modal

You can read more about the dependency here.

Import React and Component from the 'react' library for the basic building blocks of our class. Import the Modal component from 'react-native-modal' for our render() method. We will also import PropTypes for our state, thenStylesheets , View , Text , and TextInput from the 'react-native' library.

ModalView.js
import PropTypes from "prop-types";
import React, { Component } from "react";
import { View, Text, TextInput } from "react-native";
import Modal from "react-native-modal";
import { StyleSheet } from "react-native";
const white = "white";
const styles = StyleSheet.create({
content: {
flexDirection: "column",
backgroundColor: white,
padding: 16,
justifyContent: "center",
alignItems: "center",
borderRadius: 4
},
input: {
width: "100%",
textAlign: "center"
},
buttons: {
flexDirection: "row",
marginTop: 10
}
});
//component will be added later
import { Button } from "./Button";
export class ModalView extends Component {
render() {
}
}

When we create a ModalView component in our LoginForm we will pass several props to it. The placeholder and confirmLabel will be text passed in to customize the prompt of the Modal. The error prop will display the state error and isModalVisible is a boolean that determines if the modal is active. Finally, handleSubmit passes in the method to be called for submission.

We will add three methods to this class. First, componentDidUpdate will reset the text state when the modal becomes visible. The onChangeText will update the text state when the input is changed by the user. And onConfirm will fire the handleSubmit prop.

ModalView.js
//...
//import statements and stylesheet
//...
export class ModalView extends Component {
static propTypes = {
confirmLabel: PropTypes.string,
error: PropTypes.object,
handleSubmit: PropTypes.func,
isModalVisible: PropTypes.bool,
placeholder: PropTypes.string,
toggleModal: PropTypes.func
};
state = {
text: ""
};
componentDidUpdate(prevProps) {
// Reset the text state when the modal becomes visible
if (!prevProps.isModalVisible && this.props.isModalVisible) {
this.setState({ text: "" });
}
}
render() {
const {
confirmLabel,
error,
isModalVisible,
placeholder,
toggleModal
} = this.props;
return (
<Modal isVisible={isModalVisible}>
<View style={styles.content}>
{error && <Text>{error.message}</Text>}
<TextInput
style={styles.input}
autoFocus={true}
placeholder={placeholder}
onChangeText={this.onChangeText}
value={this.state.text}
onSubmitEditing={this.onConfirm}
/>
<View style={styles.buttons}>
<Button onPress={this.onConfirm}>
{confirmLabel || "Confirm"}
</Button>
{toggleModal ? <Button onPress={toggleModal}>Cancel</Button> : null}
</View>
</View>
</Modal>
);
}
onChangeText = text => {
this.setState({ text });
};
onConfirm = () => {
this.props.handleSubmit(this.state.text);
};
}

Creating the Button:

The Button component that we imported earlier allows the parent to pass in variable confirm prompts. It builds off the React Native Elements toolkit, so you will need to install it with:

npm install --save react-native-elements
npm install --save react-native-vector-icons
react-native link react-native-vector-icons
Button.js
import PropTypes from "prop-types";
import React from "react";
import { StyleSheet } from "react-native";
import * as ReactNativeElements from "react-native-elements";
const styles = StyleSheet.create({
container: {
marginLeft: 6,
marginRight: 6,
marginTop: 4,
marginBottom: 4
}
});
export const Button = ({ onPress, children, style }) => (
<ReactNativeElements.Button
backgroundColor="lightblue"
buttonStyle={style}
containerViewStyle={styles.container}
color="black"
borderRadius={4}
onPress={onPress}
title={children}
/>
);
Button.propTypes = {
onPress: PropTypes.func,
children: PropTypes.string,
style: PropTypes.object
};

Creating the LoginForm:

Now that we have the Button and ModalView components set up, we can create the LoginForm. The LoginForm is where we will receive the input from the user and make our first authenticated connection to the Realm Object Server. The skeleton for this component looks like this:

LoginForm.js
import React, { Component } from "react";
import { View, StyleSheet } from "react-native";
//We will add "react-native-router-flux in the next step
import { Actions } from "react-native-router-flux";
import Realm from "realm";
import { SERVER_URL } from "../constants";
import { Project, Item } from "../schemas";
import { Button } from "./Button";
import { ModalView } from "./ModalView";
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center"
},
buttons: {
flex: 1,
justifyContent: "center",
alignItems: "center"
}
});
export class LoginForm extends Component {
state = {};
render() {
}
}

The first method that will fire within the class is componentWillMount. Here we will check to see if there is a user logged in, and if not we will start the login process.

LoginForm.js
componentDidMount() {
// Check if we're already authenticated
if (Realm.Sync.User.current) {
this.onAuthenticated(Realm.Sync.User.current);
} else {
this.setState({ isModalVisible: true });
}
}

Our render method draws upon the two components we created earlier:

LoginForm.js
render() {
// Show the modal if the user is not authenticated
const isAuthenticated = !!Realm.Sync.User.current;
return (
<View style={styles.container}>
<ModalView
placeholder="Please Enter a Username"
confirmLabel="Login"
isModalVisible={!isAuthenticated}
handleSubmit={this.handleSubmit}
error={this.state.error}
/>
{isAuthenticated && (
<View style={styles.buttons}>
<Button onPress={this.onLogout}>Logout</Button>
<Button onPress={this.onOpenProjects}>Go to projects</Button>
</View>
)}
</View>
);
}

When the modal is displayed to the user, the user has input a username, and has clicked confirm, the handleSubmit method in our class will run. It should look like this:

LoginForm.js
handleSubmit = async nickname => {
try {
// Reset any previous errors that might have happened
this.setState({ error: undefined });
// Attempt to authenticate towards the server
const user = await Realm.Sync.User.registerWithProvider(SERVER_URL, {
provider: "nickname",
providerToken: nickname
});
// Hide the modal
this.setState({ isModalVisible: false });
this.onAuthenticated(user);
} catch (error) {
this.setState({ isModalVisible: true, error });
}
}

Realm supports a number of authentication methods: For prototyping (or low-security applications) We've chosen the Nickname provider for this tutorial because it is convenient in during prototyping, since it does not require a password It should not be used in a production environment. You can read about the different authentication types Realm supports here. Nickname credentials are easily constructed with:

Realm.Sync.User.registerWithProvider()

Where provider is set to nickname. This method registers and signs in a user to the Realm Object Server associated with the address that you added earlier. The last portion of the method then calls onAuthenticated().

LoginForm.js
onAuthenticated(user) {
// Create a configuration to open the default Realm
const config = user.createConfiguration({
schema: [Project, Item]
});
// Open the Realm
const realm = new Realm(config);
// Navigate to the main scene
Actions.authenticated({ user, realm });
}

The code snippet above does three things.

It creates a default configuration object with the schema we defined in schema.js.

It opens the realm. This creates a connection to the Realm Object Server and allow us to read and write data.

It navigates to the app's next page while passing in the reference to the logged in user and the openrealm .

The last two methods of the class provide options to logout or proceed to the projects screen.

LoginForm.js
onLogout = () => {
if (Realm.Sync.User.current) {
Realm.Sync.User.current.logout();
this.forceUpdate();
}
};
onOpenProjects = () => {
if (Realm.Sync.User.current) {
this.onAuthenticated(Realm.Sync.User.current);
}
};

Connecting the classes to our root file:

If we were to run the application now, you would still see the React-native default screen because we have not connected any of our components to the main AppRegistry. To do that, go to the main project directory and move the App.js file to our component folder. Once that's done, we will need to edit the index.js file to import App.js from ./src/components/App instead of ./App. The new line should look like:

index.js
//...
import { App } from "./src/components/App"
//...

Now we will add the react-native-router-flux dependency to our project:

npm install --save react-native-router-flux@4.0.0-beta.31

The dependency works by creating a hierarchy of scenes, where each scene is designated a key and a component. To navigate to any scene, you simply invoke the key and the desired component will be mounted with any attached props.

Replace the current contents of the App.js file with:

App.js
import React from "react";
import { Scene, Stack, Router } from "react-native-router-flux";
import { LoginForm } from "./LoginForm";
import { ProjectList } from "./ProjectList";
//import { ItemList } from "./ItemList";
export const App = () => (
<Router>
<Scene hideNavBar={true}>
<Scene key="login" component={LoginForm} title="Please Login" />
<Stack key="authenticated">
<Scene key="projects" component={ProjectList} title="Projects" />
{/* <Scene key="items" component={ItemList} title="Items" /> */}
</Stack>
</Scene>
</Router>
);

To test the code that we currently have we will need to create a ProjectList. For now let's create a placeholder.

ProjectList.js
import PropTypes from "prop-types";
import React, { Component } from "react";
import { View, FlatList, Text, StyleSheet } from "react-native";
import { Actions } from "react-native-router-flux";
import { List, ListItem } from "react-native-elements";
import { v4 as uuid } from "uuid";
const projectKeyExtractor = project => project.projectId;
const styles = StyleSheet.create({
placeholder: {
textAlign: "center",
padding: 10
}
});
import { ModalView } from "./ModalView";
//import { SwipeDeleteable } from "./SwipeDeleteable";
export class ProjectList extends Component {
static propTypes = {
user: PropTypes.object,
realm: PropTypes.object
};
state = {
dataVersion: 0,
isModalVisible: false
};
render() {
return(
<View>
<Text>User identity is: {this.props.user.identity}</Text>
</View>
)
}
}

Let's make sure everything is working correctly. Click the build/run button in Xcode. The result should look similar to this:

Step 6: Querying the Realm and displaying Projects

Now we will fill out the ProjectList, implement a process so the user can create projects, and query the realm for data.

Most of our logic for fetching the project data will be in the componentDidMount below. On line 5 we add a navigation button allowing the user to create a new project object. If it is toggled true we use the same ModalView as we did in the LoginForm. Line 13 creates a query that will return a results object filtered by project author. On line 19 we add a listener that will notify React of changes. On line 26 we subscribe to the query we made on 13, which will tell the Realm Object Server to sync that data to our device's realm.

ProjectList.js
componentDidMount() {
const { realm } = this.props;
// Register an action to create a project
Actions.refresh({
rightTitle: " Create",
onRight: () => {
this.toggleModal();
}
});
// Get a result containing all projects
const projects = realm
.objects("Project")
.filtered("owner == $0", this.props.user.identity)
.sorted("timestamp", true);
// When the list of projects changes, React won't know about it because the Result object itself will not change.
projects.addListener(() => {
// Bump a data version counter that we'll pass to components that should update when the projects change.
this.setState({ dataVersion: this.state.dataVersion + 1 });
});
// Create a subscription and add a listener
// Remember to remove the listener when component unmounts
this.subscription = projects.subscribe();
this.subscription.addListener(this.onSubscriptionChange);
// Update the state with the projects
this.setState({ projects });
}

In our render() method we utilize the FlatList component, which receives our data in the project state variable and uses the renderProject method to create each ListItem.

ProjectList.js
render() {
const { dataVersion, isModalVisible, projects } = this.state;
return (
<View>
{!projects || projects.length === 0 ? (
<Text style={styles.placeholder}>Create your first project</Text>
) : (
<List>
<FlatList
data={projects}
extraData={dataVersion}
renderItem={this.renderProject}
keyExtractor={projectKeyExtractor}
/>
</List>
)}
<ModalView
placeholder="Please Enter a Project Name"
confirmLabel="Create Project"
isModalVisible={isModalVisible}
toggleModal={this.toggleModal}
handleSubmit={this.onProjectCreation}
/>
</View>
);
}

Below is the code for renderProject.

ProjectList.js
renderProject = ({ item }) => (
<SwipeDeleteable
key={item.projectId}
onPress={() => {
this.onProjectPress(item);
}}
onDeletion={() => {
this.onProjectDeletion(item);
}}
>
<ListItem
title={item.name}
badge={{
value: item.items.length
}}
hideChevron={true}
/>
</SwipeDeleteable>
);

SwipeDeleteable is another custom component we will add after the ProjectList.

Next we will add the following five methods:

ProjectList.js
onProjectCreation = projectName => {
const { user, realm } = this.props;
// Open a write transaction
realm.write(() => {
// Create a project
realm.create("Project", {
projectId: uuid(),
owner: user.identity,
name: projectName,
timestamp: new Date()
});
});
// Reset the state
this.setState({ isModalVisible: false });
};
onProjectPress = project => {
const { user, realm } = this.props;
Actions.items({ project, realm, user, title: project.name });
};
onProjectDeletion = project => {
const { realm } = this.props;
// Open a write transaction
realm.write(() => {
// Delete the project
realm.delete(project);
});
};
onSubscriptionChange = () => {
// Realm.Sync.SubscriptionState.Complete
// Realm.Sync.SubscriptionState.Error
};
toggleModal = () => {
this.setState({ isModalVisible: !this.state.isModalVisible });
}

onProjectCreation is the process of adding data to the realm. Once the LoginForm passes the realm object as a prop, we can open a write transaction to it and begin creating objects.

onProjectPressnavigates to the items associated with a project the user has selected.

onProjectDeletion opens a write transaction to delete data from the realm.

When the component is unmounted you will want to remove all listeners.

ProjectList.js
componentWillUnmount() {
const { projects } = this.state;
if (this.subscription) {
// Remove all listeners from the subscription
this.subscription.removeAllListeners();
}
if (projects) {
projects.removeAllListeners();
}
}

The last custom component we will add is the SwipeDeletable. Create a new SwipeDeletable.js file in the components folder and paste in the following code. More can be read about the panResponder here.

SwipeDeleteable.js
import PropTypes from "prop-types";
import React, { Component } from "react";
import { Animated, PanResponder } from "react-native";
export class SwipeDeleteable extends Component {
static propTypes = {
onDeletion: PropTypes.func.isRequired,
onPress: PropTypes.func
};
_panResponder = PanResponder.create({
onStartShouldSetPanResponder: () => true,
onPanResponderMove: (e, gestureState) => {
this._offset.setValue(gestureState.dx);
},
onPanResponderRelease: (e, gestureState) => {
if (gestureState.dx === 0 && this.props.onPress) {
// We consider it a press if the user didn't pan
this.props.onPress();
} else {
// Delete the item if the gesture is released 30% to either side
const isDeletion = Math.abs(gestureState.dx) > this._width / 3;
// If the item is deleted, complete the swipe in the correct direction
const toValue = isDeletion
? gestureState.dx > 0
? this._width
: -this._width
: 0;
// Apply a sprint to the offset
Animated.spring(this._offset, {
toValue,
speed: 48
}).start();
// Delete the item right away
if (isDeletion) {
this.props.onDeletion();
}
}
}
});
_offset = new Animated.Value(0);
render() {
const { children } = this.props;
return (
<Animated.View
style={{
transform: [
{
translateX: this._offset
}
]
}}
onLayout={this.onLayout}
{...this._panResponder.panHandlers}
>
{children}
</Animated.View>
);
}
onLayout = event => {
this._width = event.nativeEvent.layout.width;
};
}

Step 7: Adding the ItemList

Now we will add the ItemList. It shares several similarities with the ProjectList, but has a few key differences.

In our project model, each Project object contains a list property called items. The ItemList associates item objects with the parent Project. This is done in a write transaction at the creation of each Item object.

The code for the ItemList is as follows:

ItemList.js
import PropTypes from "prop-types";
import React, { Component } from "react";
import { View, FlatList, Text, StyleSheet } from "react-native";
import { Actions } from "react-native-router-flux";
import { List, ListItem } from "react-native-elements";
import { v4 as uuid } from "uuid";
const styles = StyleSheet.create({
placeholder: {
textAlign: "center",
padding: 10
}
});
const itemKeyExtractor = item => item.itemId;
import { ModalView } from "./ModalView";
import { SwipeDeleteable } from "./SwipeDeleteable";
const checkedIcon = {
name: "check-box",
color: "#555"
};
const uncheckedIcon = {
name: "check-box-outline-blank",
color: "#555"
};
export class ItemList extends Component {
static propTypes = {
user: PropTypes.object,
realm: PropTypes.object,
project: PropTypes.object
};
state = {
dataVersion: 0,
isModalVisible: false
};
componentDidMount() {
const { project } = this.props;
// Register an action to create an item
Actions.refresh({
title: project.name,
rightTitle: " Create",
onRight: () => {
this.toggleModal();
}
});
// Get a result containing all items
const items = project.items.sorted("timestamp");
// When the list of items changes, React won't know about it because the Result object itself will not change.
items.addListener(() => {
// Bump a data version counter that we'll pass to components that should update when the items change.
this.setState({ dataVersion: this.state.dataVersion + 1 });
});
// No need to create a subscription here:
// We assume another subscription is created for all the users projects already.
// Update the state with the items
this.setState({ items });
}
componentWillUnmount() {
const { items } = this.state;
// Remove all listeners from the subscription
if (this.subscription) {
this.subscription.removeAllListeners();
}
// Remove all listeners from the items
if (items) {
items.removeAllListeners();
}
}
render() {
const { dataVersion, isModalVisible, items } = this.state;
return (
<View>
{!items || items.length === 0 ? (
<Text style={styles.placeholder}>Create your first item</Text>
) : (
<List>
<FlatList
data={items}
extraData={dataVersion}
renderItem={this.renderItem}
keyExtractor={itemKeyExtractor}
/>
</List>
)}
<ModalView
placeholder="Please enter a description"
confirmLabel="Create Item"
isModalVisible={isModalVisible}
toggleModal={this.toggleModal}
handleSubmit={this.onItemCreation}
/>
</View>
);
}
renderItem = ({ item }) => (
<SwipeDeleteable
key={item.itemId}
onDeletion={() => {
this.onDeletion(item);
}}
>
<ListItem
title={item.body}
rightIcon={item.isDone ? checkedIcon : uncheckedIcon}
onPressRightIcon={() => {
this.onToggleDone(item);
}}
/>
</SwipeDeleteable>
);
onSubscriptionChange = () => {
// Realm.Sync.SubscriptionState.Complete
// Realm.Sync.SubscriptionState.Error
};
toggleModal = () => {
this.setState({ isModalVisible: !this.state.isModalVisible });
};
onItemCreation = body => {
const { realm, project } = this.props;
// Open a write transaction
realm.write(() => {
// Create a project
const item = realm.create("Item", {
itemId: uuid(),
body,
isDone: false,
timestamp: new Date()
});
// Add the item to the project
project.items.push(item);
});
// Reset the state
this.setState({ isModalVisible: false });
};
onToggleDone = item => {
const { realm } = this.props;
// Open a write transaction
realm.write(() => {
// Toggle the item isDone
item.isDone = !item.isDone;
});
};
onDeletion = item => {
const { realm } = this.props;
// Open a write transaction
realm.write(() => {
// Delete the item
realm.delete(item);
});
};
}

To see the entire app cycle, uncomment the Scene components in the App.js file and the SwipeDeletable import in ProjectList.js file.

Step 8: Implementing Permissions

Up to now nothing prevents a malicious user from seeing another user's projects by subscribing with a query that matches a broader set of Project instances.

To address this issue we will instead use Realm's permission system to limit access to each Project instance so only the user that created it can read, modify, or delete it.

A user's role

Permissions are assigned to a collection of zero or more users named a role. Since we want a given Project to be accessible only to a single user, we need to use a separate role for each user. We can then grant the current user's role the appropriate permissions when we create a new Project. This will have the effect of preventing all other users from seeing or modifying that Project instance.

By default, every logged-in user has a private role created for them. This role can be accessed at PermissionUser.role. We'll use this role when creating new projects. a

Control access to Project instances

Next we add a permissions attribute to the Project model. This tells Realm that we wish to configure the permissions of each Project instance independently, and serves as the means for doing so.

export const Project = {
name: "Project",
primaryKey: "projectId",
properties: {
//...
permissions: "__Permission[]"
}
};

The Permission class represents the set of privileges we want to grant and the role to which they should be granted. Since we want to limit each Project to being visible to only a single user, we will grant the current user's role the appropriate permissions when we create a new Project. This will have the result of preventing all other users from seeing or modifying the new Project instance.

Set the permissions of a new Project

Inside of the write transaction of ProjectList.js 's onObjectCreation() method we will edit the currently created project's permissions.

ProjectList.js
onProjectCreation = projectName => {
const { user, realm } = this.props;
// Open a write transaction
realm.write(() => {
// Create a project
const project = realm.create("Project", {
// Create properties
});
// Fetches the system user
const permissionUser = realm.objects("__User").filtered(`id == '${user.identity}'`)[0];
// Create and set permissions
const permission = realm.create(Realm.Permissions.Permission, {
role: permissionUser.role,
canRead: true,
canUpdate: true,
canDelete: true,
})
// Add permission to project
project.permissions.push(permission);
});

By stating that the current user's role can read, update, and delete the Project, we prevent any other user from having access to the Project.

Lower the default permissions

By default Realm Cloud creates a set of permissive default permissions and roles. This allows getting started on developing your application without first having to worry about permissions and roles. However, these permissive default permissions should be tightened before deploying your application.

By default, all users are added to an everyone role, and this role has access to all objects within the Realm. As part of lowering the default permissions, we will limit the access of this everyone role.

In this demo, we lower the permissions inside of a node script that must be run by an admin user. You will need to import the constants and schemas with an ES6 converter or paste them into the script.

Class level permissions

We use permissions to limit querying to only the Project type. This means that the only Item objects that can be synchronized are those associated with a Project that we have permissions to access. Additionally, we remove the ability for everyone to change the permissions of each of our model classes.

// Authenticate User
// `username` and `password` must be credentials for an Admin user
Realm.Sync.User.login(SERVER_URL, username, password)
.then(user => {
// Create a configuration to open the default Realm
const config = user.createConfiguration({
schema: [Project, Item]
})
// Open Realm
Realm.open(config)
.then(realm => {
realm.write(() => {
// This fetches the system `__Class` called `__Role` and prevents users from maliciously
// adding themselves to a role.
const rolePermission = realm.objects("__Class").filtered("name == '__Role'")[0].permissions[0];
rolePermission.canUpdate = false;
rolePermission.canCreate = false;
// This lowers the permissions on the 'everyone' role for `Project` objects
const projectPermissions = realm.objects("__Class").filtered("name == 'Project'")[0].permissions[0];
projectPermissions.canSetPermissions = false;
// This lowers the permissions on the 'everyone' role for `Item` objects
const itemPermissions = realm.objects("__Class").filtered("name == 'Item'")[0].permissions[0];
itemPermissions.canSetPermissions = false;
itemPermissions.canQuery = false;
// Lock the permissions and schema
const everyonePermission = realm.objects("__Realm")[0].permissions[0]
everyonePermission.canModifySchema = false;
everyonePermission.canSetPermissions = false;
})
})
})

Realm level permissions

Finally we use permissions to prevent any non-admin users from modifying our schema or Realm -level permissions.

const everyonePermission = realm.objects("__Realm")[0].permissions[0]
everyonePermission.canModifySchema = false;
everyonePermission.canSetPermissions = false;

Step 9: Review

You now have a simple implementation of the query-based sync process within react-native. You can read more about the process of how syncing data works through the query-based synchronization process, including information on subscriptions, queries, and notification tokens, by following this link.

To build on the access-control concepts mentioned in this tutorial, please see this article.

If you're experiencing errors or would like to compare to the original, the source for this project can be found here.