Storing Data Remotely in a React Web App with Unbounded

Introduction

In this article, I’m going to show you how to build a note-taking app written entirely in Javascript on the frontend, which stores its data in a remote database.

Our goal will be to allow users to log in to our app, enter notes, and have those notes appear again when they log in, even on another device.

Just to make things clear, we’re not going to cheat by deploying a database instance and writing cloud functions to access it. All of our app’s code will live as a “static” site in the browser, while we utilize existing SaaS tools to do our remote work. In other words, we’re not going to do this:

But rather this:

Rationale

Writing full-fledged apps that live entirely in the browser, sometimes referred to as JAMstack, is a relatively new and exciting development paradigm. Part of what makes it possible, the “A” in “JAM,” are SaaS APIs that take the place of elements of an application which would traditionally run on a server.

In this case, we’re going to replace the server-side database portion of our app with Unbounded, which is a NoSQL SaaS database. We’re also going to use a third-party service, in this case Google Sign-In, to handle our user authentication.

Why use a third party authentication provider instead of building a user login system directly on top of our database?

Authentication technology and requirements have grown more complex in recent years. Beyond just checking passwords, app developers now need to worry about things like federated/social logins, two-factor auth, password reset email deliverability, etc. etc. And all of this needs to be handled according to modern best practices, in a completely secure manner. Therefore, unless you have a very good reason why you need to handle your user database in-house, adding a bespoke authentication system is akin to planting a time bomb of technical debt in your app.

For this reason, Unbounded requires a third-party authentication provider to work from the frontend. The auth provider will give your app a signed JWT token when a user logs in, which you then provide to Unbounded in lieu of a username and password. Unbounded then contacts the auth provider and retrieves a public key which it can use to verify the signature on the token, and thus the user’s identity, which it can use for authentication and permissions purposes.

Thus, to be more complete, we’ll amend the above diagram to something like the following:

The Code

Let’s start by building a basic note-taking app using React, which stores data in Unbounded. We’ll start by using create-react-app to build a scaffold, and installing the Unbounded client module:

$ npx create-react-app notes
Creating a new React app in /home/will/notes.

.... (many, many things happen) ...

$ cd notes
$ npm install --save @unbounded/unbounded

Now edit src/App.js and set it to the following:

(If you want a more detailed explanation of the Unbounded portion of this code, you may want to check out our Intro to Unbounded article series.)

While this application is purposely bare bones — it doesn’t catch errors or use separate components, for example — it does contain the logic we need to read and write user notes. But although you could run this application with npm start , it’s not going to work, yet. For one thing, you will need to replace the user@domain string representing your Unbounded username with your own email address:

let client = new Unbounded('aws-us-east-2', 'YOUR EMAIL HERE');

More importantly, though, this app is designed to store data on behalf of a specific end user, and we have no concept of individual users yet (beyond our placeholder “UNIQUE USER ID” string).

So let’s add a way for users to log in. First, sign in to the Google Developer Console. Create a project for this app, then select “Credentials” on the left panel:

Create a new credential and select “ OAuth client ID” as the type, then select “Web application.” Under “Authorized JavaScript origins” and “ Authorized redirect URIs,” enter “http://localhost:3000” to allow our default local dev server.

If all is well, you’ll get a pop-up informing you of the client ID and client secret for your new credential. Make note of the client ID (we won’t need the secret), and let’s return to our application code to add our login functionality.

We’ll use the react-google-login package to add a Google login button to our app:

npm install --save react-google-login

With this code in place, we now have buttons we can use to sign in and out of our app with any Google account. Importantly, when the login process finishes, the Google SDK gives us both the unique user ID we needed earlier, and an ID token, which is a signed token that Unbounded can use to verify the user’s identity. We then tell our Unbounded client our token string and type (in this case, “google”) so it can use that token to authenticate our subsequent requests.

const onSignIn = r => {
  // tokenId is our verification token signed by Google
  let idtoken = r.tokenId;

  // tell Unbounded to use this token as our user's identity
  client.setSubToken(idtoken, 'google');

Before we can run our app, though, we need to tell Unbounded which Google client ID we’d like to accept user tokens for — if Unbounded didn’t require this, then users could authenticate using tokens gained from logging in to another app, which is a decidedly insecure situation.

To configure our client ID in Unbounded, we use its API, which we can do from a tool like Postman or curl:

$ curl -X PUT -u [user@domain.com](mailto:user@domain.com):YourUnboundedPass [https://aws-us-east-2.unbounded.cloud/databases/notes](https://aws-us-east-2.unbounded.cloud/databases/notes) -d '{
  "users": {
    "auth": {
      "google": {
        "clientid": "xxxxxxxxxx-xxxxxxxxxxxxxxxx.apps.googleusercontent.com"
      }
    },
    "values": {
      "userID": "(subid)"
    },
    "match": {
      "userID": "(subid)"
    }
  }
}'

As usual, replace user@domain.com with your e-mail address, and YourUnboundedPass with your Unbounded password. If you don’t have an Unbounded account yet, running this command will create one for you, as well as creating a new notes database if one doesn’t exist.

Once you’ve run the above command, our “notes” app should run and work. In only a few minutes, we’ve created a multi-user, database-backed application, without writing any server-side code at all. Awesome!

Hey, I warned you it was bare bones, didn’t I?Hey, I warned you it was bare bones, didn’t I?

But wait…what about those “values” and “match” properties in the command above? Are those necessary?

They are, but the reason is a little more subtle than the reason why we had to specify our client ID to Unbounded. In the code above, we’ve been making queries using the match method, keyed to a userID property in our note documents:

let notes = await db.match({userID: userID});

While this is what we want to do, the problem is that since our code is running on the end user’s machine, there’s no way to force them to add this parameter to their queries, or to include their own userID when they call insert. Therefore, without some mechanism on the server side to enforce that these properties are set, any user with a valid login token could potentially read and write the data for any other user, just by editing our code before its run.

The “values” and “match” parameters in our database PUT, then, is exactly this mechanism. By specifying the special token “(subid)”, we tell Unbounded to set the value of the userID property to the user’s unique Google ID whenever data is read or written, no matter what they try to put in their place. In fact, though we included it for clarity, we don’t even need to specify our userID when making database calls, since Unbounded will do it for us:

let notes = await db.match({}); // don't need a parameter here

...

let {inserted: [newid]} = await db.add({
  // likewise, no user ID necessary; Unbounded sets it implicitly
  note: newNote
});

There are more security-related parameters you can set on a database — the Unbounded Frontend Access Guide has details — but at least for this basic example, we’ve covered enough bases to get started.

Likewise, although using Google Sign-In is a good starting solution, you may want to consider integrating with a full-fledged authentication provider like Auth0 if you’re looking to grow. Unlike using a social provider directly, using Auth0 gives you the ability to manually manage your user list, integrate with multiple social providers at once, and much more. Best of all, it’s free to start and isn’t much more work to set up with Unbounded; we’ll cover the details in a future article.

In the meantime, try experimenting with Unbounded as part of your JAMstack, and let us know what you think!