Handling sessions with Firestore

Many apps need session handling for authentication and user preferences. Sinatra comes with a memory-based implementation to perform this function. However, this implementation is unsuitable for an app that can be served from multiple instances, because the session that is recorded on one instance might differ from other instances. This tutorial shows how to handle sessions on App Engine.

Objectives

  • Write the app.
  • Run the app locally.
  • Deploy the app on App Engine.

Costs

This tutorial uses the following billable components of Google Cloud:

To generate a cost estimate based on your projected usage, use the pricing calculator. New Google Cloud users might be eligible for a free trial.

When you finish this tutorial, you can avoid continued billing by deleting the resources you created. For more information, see Cleaning up.

Before you begin

  1. Sign in to your Google Account.

    If you don't already have one, sign up for a new account.

  2. In the Cloud Console, on the project selector page, select or create a Cloud project.

    Go to the project selector page

  3. Make sure that billing is enabled for your Google Cloud project. Learn how to confirm billing is enabled for your project.

  4. Enable the Firestore API.

    Enable the API

  5. Prepare your development environment.

Setting up the project

  1. In your terminal window, start in a directory of your choosing and create a new directory named sessions. All of the code for this tutorial is inside the sessions directory.

  2. Change into the sessions directory:

    cd sessions
    
  3. Initialize the Gemfile:

    bundle init
    
  4. Append the following to the resulting Gemfile:

    gem "google-cloud-firestore", "~> 1.2"
    gem "sinatra", "~> 2.0"

    The Gemfile lists any non-standard Ruby libraries your app needs App Engine to load for it:

    • google-cloud-firestore is the Ruby client for the Firestore API.

    • Sinatra is the Ruby web framework used for the app.

  5. Install the dependencies:

    bundle install
    

Writing the web app

This app displays greetings in different languages for every user. Returning users are always greeted in the same language.

  • With a text editor, create a file called app.rb in the sessions directory with the following content:

    require "sinatra"
    
    require_relative "firestore_session"
    
    use Rack::Session::FirestoreSession
    
    set :greetings, ["Hello World", "Hallo Welt", "Ciao Mondo", "Salut le Monde", "Hola Mundo"]
    
    get "/" do
      session[:greeting] ||= settings.greetings.sample
      session[:views] ||= 0
      session[:views] += 1
      "<h1>#{session[:views]} views for \"#{session[:greeting]}\"</h1>"
    end

Creating the session store

Before your app can store preferences for a user, you need a way to store information about the current user in a session. The following diagram illustrates how Firestore handles sessions for the App Engine app.

Diagram of architecture: user, App Engine, Firestore.

Sinatra has built-in support for saving session data to a cookie. To save to Firestore instead, you have to define your own Rack::Session object.

  • In the sessions directory, create a file called firestore_session.rb with the following content:

    require "google/cloud/firestore"
    require "rack/session/abstract/id"
    
    module Rack
      module Session
        class FirestoreSession < Abstract::Persisted
          def initialize app, options = {}
            super
    
            @firestore = Google::Cloud::Firestore.new
            @col = @firestore.col "sessions"
          end
    
          def find_session _req, session_id
            return [generate_sid, {}] if session_id.nil?
    
            doc = @col.doc session_id
            fields = doc.get.fields || {}
            [session_id, stringify_keys(fields)]
          end
    
          def write_session _req, session_id, new_session, _opts
            doc = @col.doc session_id
            doc.set new_session, merge: true
            session_id
          end
    
          def delete_session _req, session_id, _opts
            doc = @col.doc session_id
            doc.delete
            generate_sid
          end
    
          def stringify_keys hash
            new_hash = {}
            hash.each do |k, v|
              new_hash[k.to_s] =
                if v.is_a? Hash
                  stringify_keys v
                else
                  v
                end
            end
            new_hash
          end
        end
      end
    end

Deleting sessions

As written, this app doesn't delete old or expired sessions. You can delete session data in the Google Cloud Console, or you could implement an automated deletion strategy.

Running locally

  1. Start the HTTP server:

    bundle exec ruby app.rb
    
  2. View the app in your web browser.

    You see one of five greetings: "Hello World", "Hallo Welt", "Hola mundo", "Salut le Monde", or "Ciao Mondo." The language changes if you open the page in a different browser or in incognito mode. You can see and edit the session data in the Google Cloud Console.

  3. To stop the HTTP server, in your terminal window, press Control+C.

Deploying and running on App Engine

You can use the App Engine standard environment to build and deploy an app that runs reliably under heavy load and with large amounts of data.

This tutorial uses the App Engine standard environment to deploy the server.

  1. In your terminal window, create an app.yaml file and paste the following into the file:

    runtime: ruby25
    entrypoint: bundle exec ruby app.rb
  2. Deploy the app on App Engine:

    gcloud app deploy
    
  3. View the live app at the following URL, where PROJECT_ID is your Google Cloud project ID:

    https://PROJECT_ID.appspot.com

The greeting is now delivered by a web server running on an App Engine instance.

Debugging the app

If you cannot connect to your App Engine app, check the following:

  1. Check that the gcloud deploy commands successfully completed and didn't output any errors. If there were errors (for example, message=Build failed), fix them, and try deploying the App Engine app again.
  2. In the Cloud Console, go to the Logs Viewer page.

    Go to Logs Viewer page

    1. In the Recently selected resources drop-down list, click App Engine Application, and then click All module_id. You see a list of requests from when you visited your app. If you don't see a list of requests, confirm you selected All module_id from the drop-down list. If you see error messages printed to the Cloud Console, check that your app's code matches the code in the section about writing the web app.

    2. Make sure that the Firestore API is enabled.

Cleaning up

Delete the project

  1. In the Cloud Console, go to the Manage resources page.

    Go to the Manage resources page

  2. In the project list, select the project that you want to delete and then click Delete .
  3. In the dialog, type the project ID and then click Shut down to delete the project.

Delete the App Engine instance

  1. In the Cloud Console, go to the Versions page for App Engine.

    Go to the Versions page

  2. Select the checkbox for the non-default app version you want to delete.
  3. Click Delete to delete the app version.

What's next