DEV Community

Cover image for Developing Geonotes — Adding authentication and connecting to GraphQL — Ep. 1
Emilio Schepis
Emilio Schepis

Posted on

Developing Geonotes — Adding authentication and connecting to GraphQL — Ep. 1

After writing Episode 0 I started working on Geonotes right away.

The goal was to setup a solid environment to serve as foundation for all future work.

🏠 Local by default

To increase the speed of iteration on all features and to have more control over all of the infrastructure I decided to go with a local-first approach.

Local Expo

This is the default way of working with Expo. Starting the project launches the React Native packager on the local machine and all of the devices / simulators connect directly to it.

Local Firebase

One of the features I love most about Firebase (and that I would really like to see more PaaS providers offering) is a suite of local emulators.

These emulators have support for basically all Firebase services. Local apps can connect to the authentication emulator to create accounts, sign in and generate tokens. There is no difference between connecting to an actual Firebase project and a local environment.*

Cloud functions are also emulated on the host machine, resulting in very fast execution times that are great for iterating on the different functionalities.

*: the JWT token provided by the Auth emulator is not signed, so Hasura does not accept it as a form of authentication. There is a GitHub issue on accepting the "none" algorithm, but for the moment I had to implement a local-only HS256 signing.

Local Hasura

Hasura works great with a local environment. You can download a Docker compose file directly from their repository and it will take care of setting up a local PostgreSQL database and the GraphQL engine.

Using the Hasura CLI I can launch a local instance of the console. Working from this console has the benefit of automatically generating migrations and metadata that can be later applied to the production environment. I'll talk more about this once I set up the various build pipelines.

🔐 Getting started with authentication

Authentication will be a crucial part of the application infrastructure, and ideally every access should be at least partially authenticated.

From the experience I've gained with several past apps, asking the user to create an account before being able to use the app once leads to a very low retention rate — and for good reason!

This time I decided to use an hybrid approach. I still don't want to allow unauthenticated requests to my backend, since it can be easily exploited by people trying to bring the platform down. While this should be heavily mitigated by hosting everything on massive public cloud providers (Google and Heroku, or AWS), I'd still like to have this extra layer of security.

For this reason, any user that opens the app for the first time automatically signs in with an anonymous account. This auth provider receives fewer grants (for example, they can only read notes and not create their own) and the user can then decide to create a "regular" account to access all of the features.

Client side auth

On the client, when no user is authenticated, an anonymous sign in is invoked. Source

firebase.auth().onAuthStateChanged(async (user) => {
  if (user === null) {
    return firebase.auth().signInAnonymously();
  }

  dispatch({
    type: "setAuthenticated",
    user: new User(user.uid, user.email ?? null, user.isAnonymous),
  });
});
Enter fullscreen mode Exit fullscreen mode

Server side auth

On the server, when a new user is created (either anonymously or with a provider), the appropriate claims are applied. Source

const isAnonymous = user.providerData.length === 0;

const customUserClaims = {
  "https://hasura.io/jwt/claims": {
    "x-hasura-default-role": isAnonymous ? "anonymous" : "user",
    "x-hasura-allowed-roles": isAnonymous ? ["anonymous"] : ["user"],
    "x-hasura-user-id": user.uid,
  },
};

await admin.auth().setCustomUserClaims(user.uid, customUserClaims);
Enter fullscreen mode Exit fullscreen mode

☂️ Type-safety first

As I anticipated in Episode 0, I want to make sure that as much code as possible is both type-safe and autogenerated from the database schema. This will result in the code being more reliable and standardized.

For this reason, both client- and server-side code have their GraphQL interface autogenerated by GraphQL Code Generator.

I decided to keep the generated files in version control. A lot of code will depend on the generated interface to work properly, and I prefer it to use a fixed version rather than running the codegen on-demand.

The generator's plugins allowed me to also create type-safe hooks for the app and a custom sdk for the cloud functions.

The code generation is launched only in the local environment, that downloads the current schema from the Hasura instance. To better leverage Hasura's access control system, the app receives only the schema that applies to users, while the cloud functions receive the schema that applies to the "backend" role.

schema:
  - "http://localhost:8080/v1/graphql":
      headers:
        "X-Hasura-Admin-Secret": ${HASURA_GRAPHQL_ADMIN_SECRET}
        "X-Hasura-Role": "backend"
documents:
  - src/graphql/**/queries.graphql
  - src/graphql/**/mutations.graphql
Enter fullscreen mode Exit fullscreen mode

🚧 Next steps

Next, I'll be working on adding a navigation scaffolding to the app, and on integrating the first empty map.

With everything else in place, I should be able to show custom markers on the app depending on the notes saved on the database.

🎙 How to follow the project

I'll be posting updates throughout the development process and as I learn new thing regarding development, design, and marketing.

If you'd like to have even more real-time updates you can

Discussion (0)