Handling sessions with Firestore

This tutorial shows how to handle sessions on App Engine.

Many apps need session handling for authentication and user preferences. The Gorilla Web Toolkit sessions package comes with a file system 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 in one instance might differ from other instances. The gorilla/sessions package also comes with a cookie-based implementation. But, this implementation requires encrypting cookies and storing the entire session on the client, rather than just a session ID, which may be too large for some apps.

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 Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud Console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

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

  4. Enable the Firestore API.

    Enable the API

  5. Install and initialize the Cloud SDK.
  6. Update gcloud components:
    gcloud components update
  7. Prepare your development environment.

Setting up the project

  1. In your terminal window, clone the sample app repository to your local machine:

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git
  2. Change to the directory that contains the sample code:

    cd golang-samples/getting-started/sessions

Understanding the web app

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

Multiple app windows displaying a greeting in different languages.

Before the app can store preferences for a user, you need a way to store information about the current user in a session. This sample app uses Firestore to store session data.

You can use firestoregorilla, a session store that's compatible with gorilla/sessions.

  1. The app starts by importing dependencies, defining an app type to hold a sessions.Store and an HTML template, and defining the list of greetings.

    
    // Command sessions starts an HTTP server that uses session state.
    package main
    
    import (
    	"context"
    	"fmt"
    	"html/template"
    	"log"
    	"math/rand"
    	"net/http"
    	"os"
    
    	"cloud.google.com/go/firestore"
    	firestoregorilla "github.com/GoogleCloudPlatform/firestore-gorilla-sessions"
    	"github.com/gorilla/sessions"
    )
    
    // app stores a sessions.Store. Create a new app with newApp.
    type app struct {
    	store sessions.Store
    	tmpl  *template.Template
    }
    
    // greetings are the random greetings that will be assigned to sessions.
    var greetings = []string{
    	"Hello World",
    	"Hallo Welt",
    	"Ciao Mondo",
    	"Salut le Monde",
    	"Hola Mundo",
    }
    
  2. Next, the app defines a main function, which creates a new app instance, registers the index handler, and starts the HTTP server. The newApp function creates the app instance by initializing a sessions.Store with the firestoregorilla.New function.

    
    func main() {
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    	}
    	projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
    	if projectID == "" {
    		log.Fatal("GOOGLE_CLOUD_PROJECT must be set")
    	}
    
    	a, err := newApp(projectID)
    	if err != nil {
    		log.Fatalf("newApp: %v", err)
    	}
    
    	http.HandleFunc("/", a.index)
    
    	log.Printf("Listening on port %s", port)
    	if err := http.ListenAndServe(":"+port, nil); err != nil {
    		log.Fatal(err)
    	}
    }
    
    // newApp creates a new app.
    func newApp(projectID string) (*app, error) {
    	ctx := context.Background()
    	client, err := firestore.NewClient(ctx, projectID)
    	if err != nil {
    		log.Fatalf("firestore.NewClient: %v", err)
    	}
    	store, err := firestoregorilla.New(ctx, client)
    	if err != nil {
    		log.Fatalf("firestoregorilla.New: %v", err)
    	}
    
    	tmpl, err := template.New("Index").Parse(`<body>{{.views}} {{if eq .views 1.0}}view{{else}}views{{end}} for "{{.greeting}}"</body>`)
    	if err != nil {
    		return nil, fmt.Errorf("template.New: %v", err)
    	}
    
    	return &app{
    		store: store,
    		tmpl:  tmpl,
    	}, nil
    }
    
  3. The index handler gets the user's session, creating one if needed. New sessions are assigned a random language and a view count of 0. Then, the view count is increased by one, the session is saved, and the HTML template writes the response.

    
    // index uses sessions to assign users a random greeting and keep track of
    // views.
    func (a *app) index(w http.ResponseWriter, r *http.Request) {
    	if r.RequestURI != "/" {
    		return
    	}
    
    	// name is a non-empty identifier for this app's sessions. Set it to
    	// something descriptive for your app. It is used as the Firestore
    	// collection name that stores the sessions.
    	name := "hello-views"
    	session, err := a.store.Get(r, name)
    	if err != nil {
    		// Could not get the session. Log an error and continue, saving a new
    		// session.
    		log.Printf("store.Get: %v", err)
    	}
    
    	if session.IsNew {
    		// firestoregorilla uses JSON, which unmarshals numbers as float64s.
    		session.Values["views"] = float64(0)
    		session.Values["greeting"] = greetings[rand.Intn(len(greetings))]
    	}
    	session.Values["views"] = session.Values["views"].(float64) + 1
    	if err := session.Save(r, w); err != nil {
    		log.Printf("Save: %v", err)
    		// Don't return early so the user still gets a response.
    	}
    
    	if err := a.tmpl.Execute(w, session.Values); err != nil {
    		log.Printf("Execute: %v", err)
    	}
    }
    

    The following diagram illustrates how Firestore handles sessions for the App Engine app.

    Diagram of architecture: user, App Engine, Firestore.

Deleting sessions

firestoregorilla doesn't delete old or expired sessions. You can delete session data in the Google Cloud Console or implement an automated deletion strategy. If you use storage solutions for sessions such as Memcache or Redis, expired sessions are automatically deleted.

Running locally

  1. In your terminal window, build the sessions binary:

    go build
    
  2. Start the HTTP server:

    ./sessions
    
  3. View the app in your web browser:

    Cloud Shell

    In the Cloud Shell toolbar, click Web preview Web preview and select Preview on port 8080.

    Local machine

    In your browser, go to http://localhost:8080

    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.

    Firestore sessions in Cloud Console.

  4. 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.

The app.yaml file contains the App Engine standard environment configuration:

runtime: go112
  1. Deploy the app on App Engine:

    gcloud app deploy
    
  2. In your browser, enter the following URL:

    https://PROJECT_ID.REGION_ID.r.appspot.com

    Replace the following:

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 Manage resources

  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 Versions

  2. Select the checkbox for the non-default app version that you want to delete.
  3. To delete the app version, click Delete.

What's next