MongoDB Realm with Composer

This is a guest tutorial by MongoDB's Pavel Duchovny – please send feedback and improvement suggestions to pavel.duchovny@mongodb.com!

Introduction from Pavel

During my 15 years in the IT industry, starting as a devops engineer and moving on to Data engineering, I've done a fair share of coding. I am not going to lie, creating applications has both challenged and frustrated me.

However, the moment I get really excited is when I find new tools that make my life easy in combination with technologies I've already dealt with. What fascinates me even more is when I understand that this is exactly what the creators wanted me to feel.

When I came across AppGyver, the dots got connected pretty quickly and I saw the potential of integrating their no-code development platform with MongoDB's serverless platform MongoDB Realm & MongoDB Atlas.

Both platforms come to solve a common challenge in today's application development: to cut development cycle time, increase productivity using a minimum of developer hours and remove lots of unnecessary "boilerplate coding". This makes application development become a matter of hours/days and not weeks/months, which immediately possesses value to the user.

The integration point to MongoDB is also natural through AppGyver Data API's.

In this tutorial, we will be exploring how to use MongoDB's SaaS products MongoDB Realm and MongoDB Atlas with Composer Pro.

We will be covering the following topics:

The separation is clear, as AppGyver produces a cross-platform frontend application and MongoDB Realm handles any needed backend data and authentication logic, including scaling and other DevOps concerns.

After you read this tutorial, I hope you will see that you don't need a large team or 15 years of experience to build reliable and scalable applications.

Let's get going!

Development prerequisites

If you are new to either platform, I recommend watching the following tutorials before continuing:

Once we have our application and clusters defined we can start building our application.

On the backend side, I have implemented a movie search application based on MongoDB's "mflix" dataset which I loaded to my cluster via the sample data load button.

In Composer Pro, I have configured 3 initial pages:

  1. The login page with username/password login form, created by going:

    1. Auth global toolbar section

    2. Enable authentication

    3. Direct third party authentication

  2. The main movies feed list

  3. A movie details page, to be opened once we click on the movie and its comments

Authentication

Email/Password

On the AppGyver side I have placed my input fields (username, password) and attached page variables to those fields, so the variables will be populated upon user input.

Login form with page variables bound to the inputs

On the Realm Application I have enabled the Email/Password provider by going to Users > Provider > Toggling Email/Password to enable with "Automatically confirm users" and "Send a password reset email". Potentially you can use any of the providers; I will show case a custom-function provider later in the tutorial.

Email/Password provider set up in MongoDB Realm

On Composer side, I have defined a REST API direct integration data resource called auth where my Base URL is pointing to my current authentication provider:

https://realm.mongodb.com/api/client/v2.0/app/movies-abcd/auth/providers/local-userpass
  • movies-abcd is my Application ID from Realm

Base configuration

Since authentication requires a POST request I have used the "Create Record" section and pointed it to the /login path. I also added a Content-Type: application/json header.

If I wanted to implement a user registration flow, I would need to use the Administration API for a user creation. I created my test users through the UI.

Create record (POST) configuration

Next, I've set up my schema. The request schema needs to be configured manually, while the response schema can be auto-detected by using the Test tab with a correct username/password.

Request and response schemas

Now all there is left to do is build the login flow on our Login button.

We want to validate that our inputs are not empty, then send the inputted username/password to the backend, then store the response in an app variable. Finally, we want to show a spinner and handle errors.

Login button flow functions

We start with two If condition nodes to ensure both inputs are filled, using the formulas:

NOT(IS_EMPTY(pageVars.username))
NOT(IS_EMPTY(pageVars.password))

Then, we store our response with Set app variable – for now, we just store the access_token from the response to a new app variable AccessToken. Finally, we move on to the rest of the app with Dismiss initial view.

We simultaneously show a spinner with Show spinner when the login flow starts, and handle errors via Alert nodes.

This is where formulas are very helpful, as we can compose the text alert text programmatically, in-place where we call the Alert, without any boilerplate coding.

We are good to go with our username/password authentication, it's that easy!

Successful login in the MongoDB Realm Logs dashboard

SMS Authentication

However, modern applications require better authentication than plain Email/Password. I wanted to showcase an SMS authentication flow, with the use of MongoDB Realm custom-function authentication and a Twilio service defined on the Realm platform.

First, I amended the login view accordingly, having only a phone number input separated to a country code dropdown and a second field allowing only numbers.

The dropdown values can be constructed visually via the List of values binding type, or by inputting a formula.

Login view for SMS auth

Twilio webhook

First, we need to configure a webhook in Realm that generates an auth code, instructs Twilio to send it via SMS to the user, and stores it in the database for verification in step 2.

The Twilio service can be defined in Realm via 3rd Party Services > Twilio service > Input Twilio credentials.

Next, we configure an HTTP service webhook (called startLogin), which we can define to use a "SYSTEM" auth and method POST.

The webhook function will look something like the following (see comments for detailed code explanation):

// Generate a random number function
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
// This function is the webhook's request handler.
exports = async function(payload, response) {
// Data can be extracted from the request as follows:
// Query params phone
const {phone} = payload.query;
// Initiate the auth data collection
const users = context.services
.get("mongodb-atlas")
.db("app")
.collection("smsAuthLogin");
// Get an up to 4 digits random code.
const authCode = getRandomInt(9999);
// Send the code via twilio, make sure to place your admin twilio number instead of +1234567
const twilio = context.services.get("authSMS");
twilio.send({
to: phone,
from: "+1234567",
body: `Please authenticate to AppGyver with: ${authCode}`
});
// Upsert the code for existing user document with the specified phone
const user = await users.updateOne({phone},{ phone, "authCode" : authCode.toString(), lastModified : new Date()}, {upsert : true});
};

Then, we create a new REST API integration smsAuth which calls our startLogin webhook URL via the Create record (POST) method, with query parameter phone (we could also modify the code above to expect phone as part of the request body).

Data can be passed as a query parameter too

Let's construct the login button logic.

To get our full phone number, we need to combine the country code dropdown and the phone number input. First, we bind the dropdown value to a prefixCode app variable and the number input to a phoneNumber app variable. Then, we combine both to a fullPhone app variable by executing Set app variable with a formula:

appVars.prefixCode + appVars.phoneNumber

(In this case, these all could be page variables too, since all our logic happens on this single page.)

Then, we send the full phone number to the webhook API by calling Create record for our smsAuth resource, passing in the phone parameter.

If all goes well, you should get an SMS to the inputted number after pressing Login.

A cool feature in AppGyver platform is that you can make some components appear or vanish based on a true/false expression. In this case, we create an isSmsVisible page variable of the type true/false and set it to have a false initial value. By binding our input field's Visible property to our page variable, it'll be initially hidden.

Once we get a 200 response from our startLogin API (i.e. our Create record success output triggers), we will switch the variable to true with Set page variable.

The input field is conditionally visible

Authentication function

On the MongoDB Realm Application I have enabled custom-function authentication, with the following small function to authenticate a valid SMS token:

exports = async function(loginPayload) {
// Get a handle for the app.smsAuthLogin collection
const users = context.services
.get("mongodb-atlas")
.db("app")
.collection("smsAuthLogin");
// Parse out custom data from the FunctionCredential
const { phone, authCode } = loginPayload;
console.log (JSON.stringify(authCode));
// Query for an existing user document with the specified phone
const user = await users.findOne({ "phone" : phone , "authCode" : authCode});
if (user) {
// If the user document exists, return its unique ID
return user._id.toString();
} else {
throw "Auth failed";
}
};

The function expects phone and authCode in the request body, matching them with the values stored in the previous step. If a match is found, the function returns the user's _id to the auth provider, thereby authenticating the user and e.g. returning a valid access token.

We can change our auth integration to point to the new URL:

https://realm.mongodb.com/api/client/v2.0/app/movies-abcd/auth/providers/custom-function

We also need to change the request body to have phone and authCode instead of email and password.

Now, when we call Create record for auth with the phone number and auth code inputted by the user, if the code is the one we sent to the user's phone, the authentication will return a 200 status code, the success output triggers we can login our user.

If the authentication fails we can prompt a confirmation dialog to resend the authCode, (and empty the input by setting the authCode variable to an empty value) looping the logic all the way back to the initial part where we send the SMS.

That's the beauty of No-Code, you get your "flow" already broken to modular components and can jump back and forward as much as you want.

Finally, we add some conditional logic to make the login button call the SMS sending API on the first go, then call the auth API after the SMS API has succeeded. We do this by binding the button label to a buttonLabel page variable with initial value "Send SMS", and change it to "Login" once the SMS auth is done. We then use If condition with formula

IF(pageVars.buttonLabel == "Login", true, false)

(The IF is technically not needed, since pageVars.buttonLabel == "Login" already evaluates to true or false.)

Alternatively, and more correctly from an architectural perspective, I could simply use the existing isSmsVisible variable and bind the button label to a formula:

IF(pageVars.isSmsVisible, "Login", "Send SMS")

The full logic flow can be seen below (instead of Open page and Navigate back, you can use Dismiss initial view like in the first part).

Full logic flow

The access token we get at the end of the flow will be tested against rules such as "Read your own data"/"Write your own Data" on the MongoDB side when using the data access methods in the following section – read here for more information.

Data access from MongoDB Realm

MongoDB has always been known for its stunningly easy and robust way of querying, updating and aggregating data with the flexible schema approach and bulilt-in redundancy and scalability mechanics.

With MongoDB Realm, we get a fully scalable and managed backend that utilizes the familiar MongoDB technology. Combining this with the state-of-the-art frontend produced by Composer brings about one heck of a powerful stack.

There are two main ways we can currently quickly consume data for our AppGyver application from MongoDB Realm.

Using HTTP Service webhooks to retrieve data

With HTTP Service webhooks we can define little microservices, which recieve query parameters/headers and body data to perform reading or writing of data. Since we can quickly access any of the provided services like our mongodb-atlas instance, we can use any of the availble CRUD commands, including the aggregation framework. Then we can respond with this array of documents or a single document back to our application.

However, since Atlas offers some additional capabilities like the Atlas Full Text Search service we can e.g. dramatically boost our search capabilities, which is what I've done in my Movies application when searching for my movies.

First, I've defined an HTTP service called MoviesAPI and an incoming webhook on method GET. Since our API is public for all our users I have used a SYSTEM authentication and toggled the "Response with Result" on.

GetMovies webhook

The code I've use for my Movie search engine is the following:

/*
This API searches movies from db("sample_mflix").collection("movies") using Atlas Search aggregation
*/
exports = async function(payload, response) {
// Extrat the search query param
const {Search} = payload.query;
var filter;
// Get the collection object
const movies = await context.services.get("mongodb-atlas").db("sample_mflix").collection("movies");
var doc = [];
console.log(`ID ${Search}`);
// If Search term is not empty use atlas search $search operator
if (Search !== "")
{
filter = [{
'$search': {
'text': {
'path': ['title', 'plot','fullplot','generes'],
'query': Search,
'fuzzy': {}
}
}
},{"$limit" : 100}];
doc = await movies.aggregate(filter).toArray();
}
else
{
// If no Search term provided return first found 100 results
doc = await movies.aggregate([{"$limit" : 100}]).toArray();
}
// Return the found documents through the response object
response.setBody(JSON.stringify(doc));
response.setStatusCode(200);
};

Now we integrate this webhook the same way we integrated the previous API's, by defining a new REST API data resource and using the Get collection (GET) method.

MovieSearch data resource config

We can then define a data variable MovieSearchApp on our movies list page. By default, the logic fetches new data every 5 seconds, executing the API call with whatever the input arguments currently are – you can see and modify this logic by opening the logic canvas for the data variable from the bottom of the screen. We define a SearchTerm page variable and bind it to the data variable's Search input and a search field on our page. Now, whatever the user inputs in the search field gets stored in the variable, and when the data variable refresh loop triggers again, a new API call gets made with the updated search term.

This logic could be updated to react directly to user input via events, so that the search would respond faster, but that's the topic for another tutorial.

To get the list of movies to show based on the data, I set the rating card component I created to repeat based on the data variable, binding each property to a field in my data (title, plot, rating etc). For some properties, I used a formula to mangle the data into the correct format.

Formulas are used to mangle the data into the correct format in the UI

Now we have a full search engine connected to a MongoDB Atlas database through Realm webhooks, with beautiful, automatically reactive UI – of which I can even see a WYSIWYG preview of while building the application.

Using HTTP service webhooks to store data

The same way we can define our GET collection we can define a "Create Record" API with a POST webhook service. In my application I have defined a CommentsAPI integration, where in the POST section I have placed the URL for a webhook called UpsertComment, defined in MongoDB Realm.

CommentsAPI configuration

The request body under the Schema tab is set to an object with keys id, movieId and commentText.

The webhook function performs an upsert by the provided comment ID, updating the record if it exists and creating a new one if nothing is found with the ID.

// This function is storing comments
exports = async function(payload, response) {
// Extract JSON object from the body
const body = JSON.parse(payload.body.text());
const updateObj = body;
// Set comment time from string to date
updateObj.commentTime = new Date(`${updateObj.commentTime}`);
// Upsert the comment for insert or update
const comments = await context.services.get("mongodb-atlas").db("sample_mflix").collection("comments");
const upsert_res = await comments.updateOne({"id" : updateObj.id}, updateObj, {"upsert" : true});
return upsert_res;
};

By wiring this integration up to our UI, we can now create new comments and update existing ones by simply calling the Create record function.

Conclusion

With these few chapters, we created two different auth flows, a movie search engine and an integration to create/modify data in the backend. There's a lot more that MongoDB Realm can offer when used with Composer Pro, so dive in and let us know what you've built!