Catching 200w: Scaling Shiny Apps with Google’s Firebase

Umair Durrani

About me

Hi, I am Umair Durrani

  • Data Scientist at Preseage Group

    • Human factors and analytics
  • Part-time Instructor at St. Clair College

  • PhD in Civil Engineering

Confession

I am not a developer

First project at the current job

You are going to develop a robust application that will contain confidential data

… It will be used by thousands of people

I am a developer?

Shiny to the rescue!

What is shiny?

An R package that lets you create web applications with no knowledge* of HTML / CSS / Javascript

 

Web applications? Software that you access and use in a web browser

   

* mostly

What about python?

Let’s take a look at an example app

Live app link

Getting Started

Business Logic

Create a model to predict chances of survival in the Hunger Games:

make_prediction <- function(sex, age, career, rating_rand){

  if (is.null(sex) | is.null(age) | is.null(career) | is.null(rating_rand)){
    return(NULL)
  }
 predict(
    "SAVED MODEL",
    tibble::tibble(
      sex = sex, age = age, career = career, rating_rand = rating_rand
    ),
    type = "survival",
    eval_time = c(1L, 10L, 20L)
  ) |>
    tidyr::unnest(col = .pred)
}

Business Logic

Use the function:

make_prediction(1L, 18L, 0L, 13)
# # A tibble: 3 × 2
#   .eval_time .pred_survival
#        <dbl>          <dbl>
# 1          1          0.991
# 2         10          0.971
# 3         20          0.932

Structure of a shiny app

library(shiny)

# User Interface
ui <- fluidPage(
  
)

# Server
server <- function(input, output, session) {
  
}

shinyApp(ui, server)

Structure of a shiny app

library(shiny)

# User Interface
ui <- fluidPage(
  # Display inputs and outputs
)

# Server
server <- function(input, output, session) {
  # Calculate chances of survival from the user inputs
}

shinyApp(ui, server)

Scaling the app via Firebase

  • Authentication
  • Creating, Reading, Writing, Updating, and Deleting user data (Firestore)

Create Project

Give your project a name

Enable Google Analytics (Optional)

Create an app

Select web app

Give your app a name

Now you can add authentication to your app

Authentication

Choose sign-in methods

Gmail

You may also enable sign-in via email

Add users manually

Using Firebase in {shiny}

Authentication

polished::polished_config(
    app_name = "APP_NAME",
    api_key = "POLISHED_API_KEY",

    firebase_config = list(
      apiKey = "FIREBASE_API_KEY",
      authDomain = "AUTH_DOMAIN",
      projectId = "PROJECT_ID"
    ),
    sign_in_providers = c("google", "email")
  )

Firestore Database

Programmatically add users and sign in

Define a function:

sign.in <- function(email, password, api_key) {
  r <- httr::POST(paste0("https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=", api_key),
    httr::add_headers("Content-Type" = "application/json"),
    body = jsonlite::toJSON(
      list(
        email = email,
        password = password,
        returnSecureToken = TRUE
      ),
      auto_unbox = TRUE
    )
  )
  return(httr::content(r))
}

Use the function:

user <- sign.in("user_email",  "user_password", api_key)
accessToken <- user$idToken
email <- user$email

Sign out

User Interface (UI):

actionButton(inputId = "sign_out", label = "Sign Out")

Server:

  observeEvent(input$sign_out, {
    polished::sign_out_from_shiny()
    session$reload()
  })

Getting inputs from user

UI:

tagList(
# sex
  radioGroupButtons(
    inputId = "sex",
    label = "Are you a girl or a boy?",
    choices = list("Girl" = 1L, "Boy" = 0L),
    status = "primary",
    selected = character(0)
  ),
  
  # age
  sliderInput(
    inputId = "age",
    label = "What is your age?",
    min = 12,
    max = 18,
    value = 15,
    step = 1
  ),
  
  # career
  radioGroupButtons(
    inputId = "career",
    label = "I am a career tribute.",
    choices = list("Yes" = 1L, "No" = 0L),
    status = "primary",
    selected = character(0)
  ),
  
  # rating
  sliderInput(
    inputId = "rating",
    label = "How would you rate your survival skills?",
    min = 3,
    max = 11,
    value = 5,
    step = 1
  )
)

Getting inputs from user

UI:

tagList(
  # sex
  radioGroupButtons(
    inputId = "sex",
    label = "Are you a girl or a boy?",
    choices = list("Girl" = 1L, "Boy" = 0L),
    status = "primary",
    selected = character(0)
  ),
  
  # age
  sliderInput(
    inputId = "age",
    label = "What is your age?",
    min = 12,
    max = 18,
    value = 15,
    step = 1
  ),
  
  # career
  radioGroupButtons(
    inputId = "career",
    label = "I am a career tribute.",
    choices = list("Yes" = 1L, "No" = 0L),
    status = "primary",
    selected = character(0)
  ),
  
  # rating
  sliderInput(
    inputId = "rating",
    label = "How would you rate your survival skills?",
    min = 3,
    max = 11,
    value = 5,
    step = 1
  )
)

Server:

# Form inputs in one list
form_inputs <- reactive({
  list(
   sex    = input$sex,
   age    = input$age,
   career = input$career,
   rating = input$rating
  )
})

# Send data to firestore when 'Submit' is clicked
observeEvent(
      input$form_submit,
      {
      ## Convert data to json structure
        form_data_list <- toJSON(list(
          fields = list(
            uid = list("stringValue" = email()),
            Sex = list("integerValue" = form_inputs()[["sex"]]),
            Age = list("integerValue" = form_inputs()[["age"]]),
            Career = list("integerValue" = form_inputs()[["career"]]),
            Rating = list("integerValue" = form_inputs()[["rating"]])
          )
        ), auto_unbox = TRUE)

        ## writing data
        endpoint_quiz <- "projects/gdg-demo-b8928/databases/(default)/documents/Quiz"
        
        
        write.db <- function(db_endpoint, data, auth_token) {
  r <- httr::POST(sprintf("https://firestore.googleapis.com/v1beta1/%s", db_endpoint),
    httr::add_headers(
      "Content-Type" = "application/json",
      "Authorization" = paste("Bearer", auth_token)
    ),
    body = data
  )
  return(r)
}
        
        write_request_quiz <- write.db(
          db_endpoint = paste0(endpoint_quiz, "?documentId=", email()),
          data = form_data_list,
          auth_token = accessToken()
        )
    }
)

On Firestore

Display Prediction

User Interface (UI):

uiOutput("display_pred")

Server:

  output$display_pred <- renderUI({
      # requires user input
      req(form_list())
    
    # Predictions
    pred_text <- reactive({
      req(form_list)
      user_inputs <- form_list()$in_form

      user_preds <- make_prediction(user_inputs$sex,
                                    user_inputs$age,
                                    user_inputs$career,
                                    user_inputs$rating)
      if(is.null(user_preds)){
        return(NULL)
      }

      # PRETTY PRINTING OF PREDICTIONS
    })
  })

Thank you

Live app link

Github Repo