Why and How Migrate From Firebase to Serverless Stack?

Sat, 19 Mar 2022

#serverless #sst #cdk #aws SST

SST Series

This article is part of a series around SST - Serverless Stack. I will try to let you discover some amazing aspects of this particular solution in the serverless world.

Firebase is a fantastic tool. It allows you to build mobile or web applications without having to manage a backend by yourself. But somehow, this comes with some drawbacks. In this article I will explain you why you may want to switch, and a practical guide to switch.

In a concrete example I will migrate a React application that is relying on both Firebase and a Serverless Framework backend to a single stack (with Serverless Stack)

Short Presentation of Each Solutions

Why You May Want to Migrate?

I was running my system to manage Montreal library cards since a few year based on Firebase. Because I was using the free version of Firebase, I wasn’t able to use Cloud Functions. But to query Montreal library system, it was needed to run some functions somewhere. Back in the days, I have selected Serverless Framework to operate this backend API on my own AWS account. But it was not ideal, because I was dealing with too much stacks. Focusing on Firebase, here is a list of items that can limit you:

Running the Migration in 6 Steps!

Step 1: Init your Serverless Stack application

Following the quick-start:

# Create a new SST app
npx create-serverless-stack@latest my-sst-app
cd my-sst-app

Take some time to explore the organisation of the folder. stacks/ contains your infrastructure setup, src/ will contains your Lambda function code.

Step 2: Migrate from Serverless Framework to the new application

In my specific case, I was migrating functions from Serverless Framework. The guys from SST have a decent documentation for this classic case: Migrating From Serverless Framework.

Basically I have reused directly the javascript files from the old project, and place them in the src/ folder of the new project. Then inside stacks/MyStack.ts, I have created my API routes:

// Create a HTTP API
const api = new sst.Api(this, "Api", {
  defaultAuthorizationType: sst.ApiAuthorizationType.AWS_IAM,
  cors: true,
  routes: {
    "GET /cards": "src/cards.list",
    "POST /cards": "src/cards.add",
    "DELETE /cards/{id}": "src/cards.remove",
    "GET /cards/{id}/books": "src/books.list",
		...
  },
});

The defaultAuthorizationType allow me to secure the API with an IAM authentication (see next step!).

Step 3: Replace the Firebase Authentication

Firebase is handy because it comes with an authentication layer built-in. Inside SST the best option is to use the Auth construct, that is relying behind the scene on AWS Cognito.

In stacks/MyStack.ts, I am adding:

// Create auth
const auth = new Auth(this, "Auth", {
  cognito: {
    userPoolClient: {
      supportedIdentityProviders: [UserPoolClientIdentityProvider.GOOGLE],
      oAuth: {
        callbackUrls: [
          scope.stage === "prod"
            ? `https://${prodDomainName}`
            : "http://localhost:3000",
        ],
        logoutUrls: [
          scope.stage === "prod"
            ? `https://${prodDomainName}`
            : "http://localhost:3000",
        ],
      },
    },
  },
});

if (
  !auth.cognitoUserPool ||
  !auth.cognitoUserPoolClient ||
  !process.env.GOOGLE_AUTH_CLIENT_ID ||
  !process.env.GOOGLE_AUTH_CLIENT_SECRET
) {
  throw new Error(
    "Please set GOOGLE_AUTH_CLIENT_ID and GOOGLE_AUTH_CLIENT_SECRET"
  );
}

const provider = new UserPoolIdentityProviderGoogle(this, "Google", {
  clientId: process.env.GOOGLE_AUTH_CLIENT_ID,
  clientSecret: process.env.GOOGLE_AUTH_CLIENT_SECRET,
  userPool: auth.cognitoUserPool,
  scopes: ["profile", "email", "openid"],
  attributeMapping: {
    email: ProviderAttribute.GOOGLE_EMAIL,
    givenName: ProviderAttribute.GOOGLE_GIVEN_NAME,
    familyName: ProviderAttribute.GOOGLE_FAMILY_NAME,
    phoneNumber: ProviderAttribute.GOOGLE_PHONE_NUMBERS,
  },
});

// make sure to create provider before client (https://github.com/aws/aws-cdk/issues/15692#issuecomment-884495678)
auth.cognitoUserPoolClient.node.addDependency(provider);

const domain = auth.cognitoUserPool.addDomain("AuthDomain", {
  cognitoDomain: {
    domainPrefix: `${scope.stage}-nelligan-plus`,
  },
});

// Allow authenticated users invoke API
auth.attachPermissionsForAuthUsers([api]);

This will allow me the use Google as my principal authentification system (inside Cognito User Pool). There is an alternate way to use Cognito Identity Pool with a simpler declaration:

new Auth(this, "Auth", {
  google: {
    clientId:
      "xxx.apps.googleusercontent.com",
  },
});

But it’s harder to manage in the React app so I prefer my initial version 😇.

Step 4: Replace the Firestore Database

The Firebase project rely on Firestore to store some data related to each user. On the new stack you must build a new system to store data. The equivalent structure in AWS world is a DynamoDB table, with a cost per usage. It fits well serverless deployments. There is useful Table construct available in SST:

// Table to store cards
  const table = new Table(this, "Cards", {
    fields: {
      cardId: TableFieldType.STRING,
      cardUser: TableFieldType.STRING,
      cardCode: TableFieldType.STRING,
      cardPin: TableFieldType.STRING,
    },
    primaryIndex: { partitionKey: "cardId" },
  });

Step 5: Replace the Firebase Hosting

Here there is multiple approach possible. I am suggesting the most integrated solution for an SST stack:

First add in MyStack.ts:

// Create frontend app
const reactApp = new ReactStaticSite(this, "ReactSite", {
  path: "react-app",
  buildCommand: "yarn && yarn build",
  environment: {
    REACT_APP_REGION: this.region,
    REACT_APP_API_URL: api.url,

    REACT_APP_GA_TRACKING_ID: "UA-151729273-1",
    REACT_APP_USER_POOL_ID: auth.cognitoUserPool.userPoolId,
    REACT_APP_USER_POOL_CLIENT_ID:
      auth.cognitoUserPoolClient.userPoolClientId,
    REACT_APP_IDENTITY_POOL_ID: auth.cognitoIdentityPoolId,
    REACT_APP_USER_UI_DOMAIN: domain.domainName,
    REACT_APP_DOMAIN:
      scope.stage === "prod"
        ? `https://${prodDomainName}`
        : "http://localhost:3000",
  },
  customDomain:
    scope.stage === "prod"
      ? {
          domainName: prodDomainName,
          hostedZone: "sidoine.org",
        }
      : undefined,
});

The environment props allow to pass environment variables to the React stack. The path is the relative path that contains your React app.

Step 6: Adapt your React Application

So following step 5, in the react-app/ folder I move my existing React application and start changing it to support my new stack content. Here is a general guidance follow:

For reference here is two examples of aws-amplify usage:

For reference, you can dig into the project before and after the migration:

Before the migration: branch sls_migration
After the migration: commit 7fcff53 on Feb 28

Conclusion

The switch have been a game-changer for me. And it’s not because of the cost or features, but more for the developer experience. Before the migration, I use to first build the backend function, test it, ship it. Then use this backend function in the frontend application after shipping the backend part. Then maybe I need to go back to the backend to adapt the contract or modify the code... You get it, it was a slow back-and-forth process, not very efficient.

Today I have a single stack:

The advantages:

Wrap up

Get a look at Serverless Stack (SST), or my example project (here on Github). It’s really worth the time to understand the main concepts, and then you will be more efficient building full stack serverless applications! Continue the discussion on twitter 😎