Build a Team Standup Board on GCP
Deploy a full-stack standup board app on Google Cloud Run with Firestore.
Introduction
30 Second Summary
Every engineering team needs a quick way to share what they're working on each day. Without a central place to post updates, status gets lost in chat threads and nobody knows who's blocked.
In this project, you will build a team standup board and deploy it live on Google Cloud. You'll create a web app where teammates submit daily updates, backed by a database that remembers every entry.
What You'll Build
Open a live URL powered by Google Cloud Run, type your name and today's plan, and watch your update appear on a shared board backed by Firestore.
By the end of this project, you'll have:
- Submit standup updates with your name, yesterday's work, today's plan, and any blockers, and watch each entry appear on the board instantly.
- See the whole team's status at a glance in a styled card layout that displays all of today's standups grouped by person.
- Share a live public URL that anyone on your team can open in a browser, with standup data that persists across redeployments and page refreshes.
- Secret Mission: Add date navigation to browse standups from any previous day.
Are there any prerequisites?
None. This guide walks you through setting up every tool you need, including VS Code, the Google Cloud CLI, and your Google Cloud project.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Your Google Cloud Environment
Your standup board will live on Google Cloud, so you need the right tools to build, test, and deploy code to Google's infrastructure. The Google Cloud CLI is the bridge between your machine and Google Cloud services.
In this step, you'll install VS Code as your code editor, set up the Google Cloud CLI, and create the project folder where your standup board app will live.
In this step, get ready to:
- Install and open VS Code.
- Install and configure the Google Cloud CLI.
- Create the standup-board project folder and confirm Python is available.
Install VS Code
VS Code is a free code editor where you'll write all of the Python and HTML for this project.
- Open your browser and go to https://code.visualstudio.com/.
- Click the download button for your operating system (macOS or Windows).
- Run the installer and follow the on-screen prompts to complete the installation.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
You should see the VS Code Welcome tab appear.
VS Code won't open?
Make sure you ran the installer to completion before searching for the app. On macOS, if you downloaded the .zip file, drag the VS Code app into your Applications folder first.
help me get VS Code installed and running
Install and configure the Google Cloud CLI
The Google Cloud CLI gives you the gcloud command, which you'll use to deploy your app and manage cloud resources from the terminal.
- In VS Code, open the integrated terminal by clicking Terminal in the top menu bar.
- Click New Terminal.
- Check if the Google Cloud CLI is already installed by running this command:
gcloud --version
✔️ I see a version number
The Google Cloud CLI is already installed on your machine. You can skip ahead to the gcloud init step below.
ⓧ Command not found
You need to install the Google Cloud CLI.
- Open your browser and go to https://cloud.google.com/sdk/docs/install-sdk.
- Download the installer for your operating system.
- Run the installer and follow the prompts to complete the installation.
What happens during installation?
The installer may ask if you want to add the gcloud CLI to your PATH and enable command completion. Accept both of these options.
On Windows, the installer bundles Python 3 automatically, so you do not need a separate Python install.
- Close the VS Code terminal by clicking the trash icon on the terminal panel.
- Open a fresh terminal by clicking Terminal then New Terminal again. This ensures your terminal picks up the new PATH.
- Confirm the installation by running this command:
gcloud --version
You should now see a version number and a list of installed components.
Now you need to connect the CLI to your Google account and set up a project. The gcloud init command is interactive. It will open a browser for login, ask you to select a project, and optionally set a default region.
- Initialize the Google Cloud CLI by running this command:
gcloud init
- When asked to log in, enter Y and press Enter.
- A browser window opens. Sign in with your Google account and click Allow to grant access.
- Back in the terminal, you are asked to pick a cloud project. If you have an existing project, select it. If not, choose the option to create a new project.
- When asked if you want to configure a default Compute Engine region and zone, enter Y.
- Find us-central1 in the list and enter its number.
What is a Google Cloud project?
A Google Cloud project is a container that holds all of the cloud resources for your app. Every service you use (databases, deployments, APIs) lives inside a project.
If you are new to Google Cloud, you may be prompted to set up a billing account. Google Cloud offers a free trial with $300 in credit for 90 days, and the services in this project fall within the free tier.
Next, enable the three APIs your app needs. These APIs let Google Cloud accept your deploy commands, build your container, and store data.
- Enable the required APIs by running this command:
gcloud services enable run.googleapis.com cloudbuild.googleapis.com firestore.googleapis.com
This command takes a moment to complete. When it finishes, you should see a confirmation message for each API.
What are these APIs?
- run.googleapis.com enables Cloud Run, which hosts and runs your app on Google's servers.
- cloudbuild.googleapis.com enables Cloud Build, which packages your code into a container image for deployment.
- firestore.googleapis.com enables Firestore, the NoSQL database where your standup entries will be stored.
Seeing a billing or permission error?
Enabling APIs requires a billing account linked to your project. Go to https://console.cloud.google.com/billing to link a billing account, then re-run the command above.
Google Cloud offers a free trial with $300 credit, so you will not be charged for this project.
help me fix a billing or permission error when enabling Google Cloud APIs
Create your project folder and confirm Python
You need a dedicated folder on your machine where all of your app's files will live.
- In your VS Code terminal, move to your Desktop by running this command:
cd ~/Desktop
- Create the project folder and move into it by running these commands:
mkdir standup-board
cd standup-board
- Now open this folder in VS Code. Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the standup-board folder, and click Open.
Trust the authors prompt
VS Code may ask if you trust the authors of the files in this folder. Click Yes, I trust the authors to continue.
- Open a fresh integrated terminal by clicking Terminal then New Terminal.
The terminal should now show you are inside the standup-board folder. The Google Cloud CLI bundles Python, so you should already have it available without installing it separately.
Before you run the next command, what do you think will happen? Do you expect Python to be available, or will you get an error?
- Confirm Python is available by running this command:
python --version
You should see a Python version number printed (3.10 or higher).
Command not found or wrong version?
On some systems, the command is python3 --version instead of python --version. Try that first.
If neither works, the Google Cloud CLI may not have added its bundled Python to your PATH. Close VS Code entirely, reopen it, and try again in a fresh terminal.
help me get Python working from my terminal
Your environment is ready to go. Next up, you'll write your first Flask app and see it running live in your browser.
Build and Run a Basic Flask App Locally
Your Google Cloud environment is set up and ready to go. Before jumping into cloud services, you need a working web server running on your own machine.
A local server gives you a fast feedback loop. Change code, refresh the browser, and see results instantly. You'll use Flask, a lightweight Python web framework, to build it.
In this step, get ready to:
- Create the project files (requirements.txt and main.py).
- Set up a Python virtual environment and install dependencies.
- Run the Flask app and verify it works at localhost:8080.
Create the dependency file
Every Python project lists the packages it depends on in a file called requirements.txt. This tells pip exactly which packages to install.
- In VS Code's Explorer sidebar, hover over the STANDUP-BOARD folder name and click the New File icon.
- Name the file requirements.txt and press Enter.
- Paste the following into requirements.txt:
Flask==3.1.3
gunicorn==26.0.0
What are these packages?
Flask is the web framework that handles routing and serving your app.
gunicorn is a production web server that Google Cloud Run uses to run your app when you deploy it later. You won't use it locally, but it needs to be listed here so Cloud Run can find it.
Create the Flask app
Now you need the actual Python file that runs your web server. This file defines what happens when someone visits your app.
- Create another new file in the STANDUP-BOARD folder by clicking the New File icon again.
- Name this file main.py and press Enter.
- Paste the following code into main.py:
import os
from flask import Flask
# Create the Flask application
app = Flask(__name__)
# Define the home page route
@app.route("/")
def index():
return "Standup Board - Coming Soon!"
# Run the app when this file is executed directly
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
What does this code do?
- app = Flask(__name__) creates a new Flask application.
- @app.route("/") tells Flask what to do when someone visits the root URL of your site. The index() function returns a simple text message.
- The if __name__ == "__main__" block runs the server when you execute the file directly with python main.py.
- os.environ.get("PORT", 8080) reads the port from an environment variable, defaulting to 8080. Google Cloud Run sets this variable automatically when your app is deployed.
- Save both files by pressing Ctrl+S (Windows) or Cmd+S (macOS).
- Confirm the files exist by running this in your integrated terminal:
ls
You should see main.py and requirements.txt listed in the output.
Don't see your files?
- Make sure you created the files inside the standup-board folder, not in a subfolder or outside it.
- Check the filenames match exactly. requirements.txt must be spelled with no extra spaces or capital letters.
- If you're on Windows and ls doesn't work, try dir instead.
- Help me find my project files in VS Code
✔️ Awesome, I've got everything!
Great. Make sure both files are saved before moving on.
ⓧ I'd like to double check the full code
Here is what requirements.txt should contain:
Flask==3.1.3
gunicorn==26.0.0
Here is what main.py should contain:
import os
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Standup Board - Coming Soon!"
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
Set up the virtual environment and install dependencies
A virtual environment keeps your project's packages separate from the rest of your system. This prevents version conflicts between different Python projects on your machine.
- Create and activate a virtual environment by running these commands in your terminal:
macOS
python3 -m venv venv
- Activate the virtual environment:
source venv/bin/activate
Windows
python -m venv venv
- Activate the virtual environment:
.\venv\Scripts\activate
You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.
- Install the project dependencies by running:
pip install -r requirements.txt
You should see output showing Flask and gunicorn being downloaded and installed, ending with a Successfully installed message.
Having trouble with the virtual environment?
- On Windows, if you see an error about running scripts being disabled, open PowerShell as Administrator and run Set-ExecutionPolicy RemoteSigned. Then try activating again.
- If pip install fails with a permissions error, make sure the virtual environment is activated. Look for (venv) at the start of your terminal prompt.
- On macOS, if python3 is not found, install it from python.org or use the Google Cloud Shell instead.
- Help me set up my Python virtual environment
Run the app and check it in the browser
- Start the Flask development server by running:
python main.py
You should see output in the terminal that includes a line starting with * Running on followed by a URL. This means the server is running.
Before you check the browser, what do you think you'll see at http://localhost:8080?
- Open your browser and go to http://localhost:8080.
You should see the text Standup Board - Coming Soon! displayed on the page. That is your Flask app serving its first response.
- Return to VS Code and stop the server by pressing Ctrl+C in the terminal.
Don't see the page in your browser?
- Make sure the terminal shows the server is running. Look for a * Running on line in the output.
- Check that you're visiting http://localhost:8080, not https://. The development server does not use HTTPS.
- If you see an "address already in use" error in the terminal, another process is using port 8080. Close other terminal windows or restart VS Code.
- Help me get my Flask app running at localhost
Your Flask app is running on your machine and serving a real web page. Next up, you'll deploy it to Google Cloud Run so anyone on the internet can see it.
Deploy to Google Cloud Run
In the last step, you built a Flask app and confirmed it runs on your own machine. That's a solid start, but nobody else can reach localhost.
Google Cloud Run takes your source code, packages it into a container behind the scenes, and serves it on a live HTTPS URL. In this step, you'll deploy with a single command and watch your local project become a real cloud application.
In this step, get ready to:
- Deploy your Flask app to Google Cloud Run.
- Confirm the app is live at a public URL on the internet.
Deploy your app to Cloud Run
Cloud Run's source deploy uploads your code to Google Cloud, builds it into a container automatically, and starts serving it. You don't need to install Docker or write any container configuration.
- In your VS Code terminal, make sure you're in the standup-board folder (not inside the venv folder).
- Deploy your app to Cloud Run by running this command:
gcloud run deploy standup-board --source . --region us-central1 --allow-unauthenticated
What does this command do?
- standup-board names your Cloud Run service.
- --source . tells Cloud Run to build directly from your source code instead of requiring a pre-built container image.
- --region us-central1 deploys to the US Central region you selected during setup.
- --allow-unauthenticated makes the service publicly accessible so anyone with the URL can visit it.
The build and deploy process takes a few minutes. You'll see progress updates as Google Cloud uploads your source, builds the container, creates a revision, and routes traffic.
Watch for prompts
The command may ask you to enable the Artifact Registry API. Type y and press Enter if prompted. You only need to do this once per project.
- When the deploy finishes, the terminal prints a Service URL (something like https://standup-board-xxxxx-uc.a.run.app). Copy this URL.
Deploy failing?
- If you see a permissions error, make sure you ran gcloud init in Step 1 and selected the correct project.
- If the command can't find your files, confirm your terminal is in the standup-board folder. Run cd to check your current directory.
- If you see a billing error, set up a billing account at console.cloud.google.com/billing. Google Cloud offers a free trial with $300 in credits for new accounts.
Help me debug my Cloud Run deployment error.
Visit your live URL
Before you open the URL, what do you think you'll see? Will it look different from the local version you tested in Step 2?
- Paste the Service URL from your terminal output into your browser's address bar.
- Press Enter to open the page.
You should see the same "Standup Board - Coming Soon!" text from your local server. This time, it's running on Google's infrastructure with a public HTTPS URL that anyone on the internet can visit.
What just happened behind the scenes?
When you ran the deploy command, Google Cloud did four things for you. It uploaded your source code to Cloud Build. It detected your requirements.txt and built a container image using buildpacks. It stored that image in an Artifact Registry repository (created automatically). Then it started serving your app on Cloud Run with a public URL.
You didn't need to install Docker, write a Dockerfile, or configure any infrastructure. The --source . flag handled everything.
Your app is live on the internet. Next up, you'll connect a Firestore database so your standup board can actually store and retrieve entries.
Connect Firestore and Build the Standup Submission API
Your standup board is live on the internet. Anyone with the URL can see it. But right now, the app has no memory.
If someone submits a standup update, there is nowhere to store it. In this step, you'll create a Firestore database to give your app persistent storage, then build a REST API that can save and retrieve standup entries.
In this step, get ready to:
- Create a Firestore database in the Google Cloud Console.
- Build POST and GET API endpoints for standup entries.
- Test the API locally to confirm data flows correctly.
Create a Firestore database
Your app needs a place to store standup entries that survives page refreshes and redeployments. Firestore is Google Cloud's NoSQL document database. You store data as JSON-like documents inside collections, with zero schema setup required.
- Open the Firestore page in the Google Cloud Console by visiting:
- https://console.cloud.google.com/firestore
- Make sure your project is selected in the project dropdown at the top of the page.
- Click Create a Firestore database.
- For the database ID, leave it as the default.
- Under Edition, select Standard.
- For Data access mode, select Firestore in Native mode.
- Under Security rules, select Restrictive. Your Python server will access Firestore directly, so you do not need mobile or web client access.
- For Location, choose nam5 (United States).
- Click Create Database.
Wait a moment for the database to finish creating. You should see the Firestore data viewer load with an empty database.
Why Firestore over a traditional SQL database?
Firestore stores data as documents inside collections. You write Python dictionaries directly to a collection with no table definitions, no migrations, and no SQL.
Firestore also has a generous free tier: 50,000 reads per day, 20,000 writes per day, and 1 GiB of storage. A portfolio standup board stays well inside those limits.
Add the Firestore dependency and build the GET endpoint
With the database ready, you need to connect your Flask app to Firestore. That starts with installing the Python client library and setting up authentication for local development.
- In VS Code, open requirements.txt.
- Find this line:
gunicorn==26.0.0
- Add a new line directly below it so the file now looks like this:
Flask==3.1.3
gunicorn==26.0.0
google-cloud-firestore==2.28.1
- Save requirements.txt.
- Install the updated dependencies by running this command in your terminal:
pip install -r requirements.txt
You should see the Firestore library downloading and installing along with its sub-dependencies.
Seeing an install error?
Make sure your virtual environment is still active. You should see (venv) at the start of your terminal prompt. If not, activate it again by running .\venv\Scripts\activate and then retry the install command.
Help me fix my pip install error.
Next, you need to set up authentication so the Firestore client can connect to your database from your local machine. On Cloud Run this happens automatically, but locally you need to provide credentials.
- Set up Application Default Credentials by running:
gcloud auth application-default login
A browser window opens asking you to sign in with your Google account. Sign in and grant access. Your terminal confirms the credentials are saved.
What are Application Default Credentials?
Application Default Credentials (ADC) let Google Cloud client libraries authenticate without hardcoding any keys. Locally, ADC uses the token you just created. On Cloud Run, the service automatically uses its built-in service account. Your code stays the same in both environments.
Now update your Flask app to connect to Firestore and add the first API endpoint.
- In main.py, find the imports at the top of the file:
import os
from flask import Flask
app = Flask(__name__)
- Replace those lines with the following code, which adds the Firestore imports and initializes a database client:
import os
from datetime import datetime, timezone
from flask import Flask, render_template, request, jsonify
from google.cloud import firestore
app = Flask(__name__)
# Connect to your Firestore database
db = firestore.Client()
What does this code do?
- datetime and timezone let you tag each standup entry with today's date in UTC.
- request reads incoming JSON data from POST requests. jsonify turns Python dictionaries into JSON responses.
- firestore.Client() creates a connection to your Firestore database using the credentials you just set up.
Now add the GET endpoint that retrieves today's standup entries.
- In main.py, find the index route that returns the Coming Soon message. Add the following code directly below it, above the if __name__ block:
@app.route("/api/standups", methods=["GET"])
def get_standups():
# Get today's date as a string for filtering
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
standups_ref = db.collection("standups")
# Only return standups from today, sorted by time
query = standups_ref.where("date", "==", today).order_by("timestamp")
results = []
for doc in query.stream():
data = doc.to_dict()
data["id"] = doc.id
# Convert timestamp to a JSON-friendly string
if "timestamp" in data and data["timestamp"]:
data["timestamp"] = data["timestamp"].isoformat()
results.append(data)
return jsonify(results)
What does this endpoint do?
- It calculates today's date in UTC and queries the standups collection for documents matching that date.
- Results are sorted by timestamp so the earliest standup appears first.
- Each document's Firestore timestamp is converted to an ISO string so it can be sent as JSON.
- Save main.py.
Before you run this, what do you think the API will return when there are no standup entries in the database yet?
- Start the app by running:
python main.py
- Open your browser and visit http://localhost:8080/api/standups.
You should see an empty JSON list: []. That confirms your Firestore connection is working and the GET endpoint is live.
Seeing an error about indexes?
Firestore may need a composite index for queries that filter on one field and sort by another. If the error message includes a link to create the index, click it, confirm the details, and click Create. Wait a minute for the index to build, then restart the app and try again.
Help me fix a Firestore index error.
Add the POST endpoint and test the full API
Your GET endpoint works, but there is no way to create standup entries yet. The POST endpoint will accept JSON data from the frontend form and save it to Firestore.
- Stop the running server by pressing Ctrl+C in the terminal.
- In main.py, add the following code below the get_standups function you just wrote, above the if __name__ block:
@app.route("/api/standups", methods=["POST"])
def create_standup():
data = request.get_json()
# Require at least a name to submit
if not data or not data.get("name"):
return jsonify({"error": "Name is required"}), 400
# Tag each entry with today's date for filtering
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
standup = {
"name": data.get("name", ""),
"yesterday": data.get("yesterday", ""),
"today": data.get("today", ""),
"blockers": data.get("blockers", ""),
"date": today,
"timestamp": firestore.SERVER_TIMESTAMP,
}
# Save the standup entry to Firestore
db.collection("standups").add(standup)
return jsonify({"status": "ok"}), 201
What does this endpoint do?
- It reads JSON from the request body and checks that a name field is present.
- It tags the entry with today's date string so the GET endpoint can filter by day.
- firestore.SERVER_TIMESTAMP tells Firestore to stamp the exact time on its server, avoiding timezone issues from the client machine.
- db.collection("standups").add(standup) saves the dictionary as a new document in the standups collection with an auto-generated ID.
- Save main.py and restart the server by running:
python main.py
Now test the full flow. You will submit a standup entry via a POST request and then check that the GET endpoint returns it.
- Open a second terminal in VS Code by clicking Terminal in the top menu, then New Terminal.
- Submit a test standup entry by running:
curl -X POST http://localhost:8080/api/standups -H "Content-Type: application/json" -d "{\"name\": \"Alex\", \"yesterday\": \"Set up the project\", \"today\": \"Building the API\", \"blockers\": \"None\"}"
You should see {"status":"ok"} printed in the terminal. That means the entry was saved to Firestore.
- Switch to your browser and refresh http://localhost:8080/api/standups.
The page now shows a JSON array with your test entry, including the name, yesterday, today, and blockers fields. Your API can store and retrieve standup data.
Still seeing an empty list?
Double check that the curl command returned {"status":"ok"} and not an error. If you see a 400 error, make sure the JSON includes a name field.
You can also verify data was saved by going back to the Firestore console and checking if a standups collection appeared with your test document.
Help me debug my Firestore standup API.
- Stop the server with Ctrl+C when you are done testing.
✔️ Awesome, I've got everything!
Great. Make sure you have saved both requirements.txt and main.py.
ⓧ I'd like to double check the full code
Here is the complete requirements.txt:
Flask==3.1.3
gunicorn==26.0.0
google-cloud-firestore==2.28.1
For main.py, scroll up and confirm the following sections are present in order:
- The updated imports (os, datetime, timezone, Flask, render_template, request, jsonify, firestore).
- The app and db initialization lines.
- The / route returning the Coming Soon message.
- The /api/standups GET route (get_standups function).
- The /api/standups POST route (create_standup function).
- The if __name__ block at the bottom.
Your app now has a working database and API. Next up, you'll build a proper frontend so users can submit standups through a form instead of raw JSON commands.
Build the Frontend and Redeploy
Your API can store and retrieve standup entries, but right now the only way to use it is by sending raw JSON requests. That is not something you would ask your teammates to do.
In this step, you will build a proper HTML frontend with a submission form, styled cards, and JavaScript that ties everything to your API. Then you will redeploy to Cloud Run so the finished product goes live.
In this step, get ready to:
- Create an HTML template with a styled standup submission form.
- Wire the form to your Flask API with JavaScript.
- Redeploy the complete app to Cloud Run.
Create the HTML template with the standup form
Flask looks for HTML pages in a folder called templates by default. You will create this folder and build the standup form page inside it.
- Create the templates folder by running this command:
mkdir templates
- In VS Code, right-click the templates folder in the Explorer sidebar.
- Select New File.
- Name the file index.html.
- Paste the following code into the file to start the page structure and layout styling:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Team Standup Board</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f0f2f5;
color: #1a1a2e;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 24px;
color: #1a73e8;
}
.form-card, .standup-card {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
What does this CSS do?
The reset rule (*) removes default browser spacing so every element starts from a clean slate. The body rule centers the page at 800px wide with a light gray background.
The .form-card and .standup-card classes give containers a white background, rounded corners, and a subtle shadow so they look like floating cards.
- Add the following CSS directly below in the same file to style the form inputs, buttons, and standup cards:
label {
display: block;
font-weight: 600;
margin-bottom: 4px;
margin-top: 12px;
}
input[type="text"], textarea {
width: 100%;
padding: 10px;
border: 1px solid #dadce0;
border-radius: 8px;
font-size: 14px;
font-family: inherit;
}
textarea { resize: vertical; min-height: 60px; }
button {
margin-top: 16px;
padding: 12px 24px;
background-color: #1a73e8;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
width: 100%;
}
button:hover { background-color: #1557b0; }
.standup-card h3 { color: #1a73e8; margin-bottom: 8px; }
.standup-card p { margin: 4px 0; line-height: 1.5; }
.standup-card .label { font-weight: 600; }
What do these styles do?
Labels are bold and spaced above each input field. Text inputs and textareas span the full width with rounded borders. The submit button is a wide blue bar that darkens on hover.
The .standup-card rules style how submitted entries will look. Names appear in blue, and each field gets comfortable line spacing.
- Save index.html.
- Add the remaining CSS, the HTML body, and the standup form below in the same file:
.blocker { color: #d93025; }
#standup-list { margin-top: 24px; }
.empty-state {
text-align: center;
color: #5f6368;
padding: 40px;
}
</style>
</head>
<body>
<h1>Team Standup Board</h1>
<div class="form-card">
<form id="standup-form">
<label for="name">Your Name</label>
<input type="text" id="name" name="name" required placeholder="e.g. Alex">
<label for="yesterday">What did you do yesterday?</label>
<textarea id="yesterday" name="yesterday" placeholder="Finished the login page..."></textarea>
<label for="today">What are you doing today?</label>
<textarea id="today" name="today" placeholder="Working on the dashboard..."></textarea>
<label for="blockers">Any blockers?</label>
<textarea id="blockers" name="blockers" placeholder="None / Waiting on API access..."></textarea>
<button type="submit">Post Update</button>
</form>
</div>
What does this form contain?
The .blocker class colors any blockers in red so they stand out. The .empty-state class styles a centered message for when no standups exist yet.
The form has four fields: your name, yesterday's work, today's plan, and any blockers. The required attribute on the name input prevents empty submissions.
- Save index.html.
Don't see index.html in the Explorer?
Make sure you created the file inside the templates folder, not in the root standup-board folder. The file must be at templates/index.html for Flask to find it.
help me find where my index.html file ended up
Wire the template to Flask and add JavaScript
Your HTML template is ready, but main.py still returns plain text on the root route. You need to switch it to serve your new template, then add JavaScript to make the form actually work.
- In main.py, find the index function and locate this line:
return "Standup Board - Coming Soon!"
- Replace that line with this:
return render_template("index.html")
- Save main.py.
- Restart the server by pressing Ctrl+C in the terminal, then running:
python main.py
Before opening the browser, what do you think you will see at http://localhost:8080?
- Open http://localhost:8080 in your browser.
You should see the styled standup form with fields for your name, yesterday's work, today's plan, and blockers. The form does not submit yet because there is no JavaScript to handle it.
Still seeing 'Coming Soon'?
Make sure you saved main.py after changing the return line. Try a hard refresh in your browser with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows).
If you see a template-not-found error, check that index.html is inside a folder called templates (not template without the s).
help me debug why Flask cannot find my template
The form looks right but does not do anything yet. You need JavaScript to send form data to your API and display the results.
- In index.html, find the closing </div> tag at the bottom of the file.
- Add the following code below it to load and display standup entries:
<div id="standup-list"></div>
<script>
async function loadStandups() {
const res = await fetch("/api/standups");
const standups = await res.json();
const list = document.getElementById("standup-list");
if (standups.length === 0) {
list.innerHTML = '<div class="empty-state">No standups posted today yet. Be the first!</div>';
return;
}
list.innerHTML = standups.map(s => `
<div class="standup-card">
<h3>${escapeHtml(s.name)}</h3>
<p><span class="label">Yesterday:</span> ${escapeHtml(s.yesterday)}</p>
<p><span class="label">Today:</span> ${escapeHtml(s.today)}</p>
<p class="${s.blockers ? 'blocker' : ''}"><span class="label">Blockers:</span> ${escapeHtml(s.blockers || "None")}</p>
</div>
`).join("");
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
What do these functions do?
loadStandups() fetches today's entries from your GET /api/standups endpoint and renders each one as a styled card. If no entries exist yet, it shows a friendly empty-state message.
escapeHtml() prevents cross-site scripting by converting user-submitted text into safe HTML. It creates a temporary text node and reads back the escaped content.
- Add the following code directly below the escapeHtml function to handle form submissions:
document.getElementById("standup-form").addEventListener("submit", async (e) => {
e.preventDefault();
const body = {
name: document.getElementById("name").value,
yesterday: document.getElementById("yesterday").value,
today: document.getElementById("today").value,
blockers: document.getElementById("blockers").value,
};
await fetch("/api/standups", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
document.getElementById("standup-form").reset();
loadStandups();
});
loadStandups();
</script>
</body>
</html>
What does the submit handler do?
e.preventDefault() stops the browser from refreshing the page when the form is submitted. Instead, the handler collects all four field values and sends them as JSON to your POST /api/standups endpoint using fetch.
After the POST succeeds, reset() clears the form and loadStandups() re-fetches the full list so the new entry appears instantly. The final loadStandups() call at the bottom runs when the page first loads to show any existing entries.
✔️ Awesome, I've got everything!
Make sure you have saved index.html with all five code sections pasted in order.
ⓧ I'd like to double check the full code
Since index.html is a long file, scroll through it in VS Code and confirm these sections appear in order:
- Page structure and layout CSS (DOCTYPE, head, style tag, body/h1/card reset rules).
- Form input and button CSS (label, input, textarea, button, standup-card display rules).
- Remaining CSS and form HTML (.blocker, .empty-state, closing style/head tags, body with the four-field form).
- loadStandups and escapeHtml functions (standup-list div, script tag, fetch logic, HTML escaping).
- Form submit handler (addEventListener, POST fetch, form reset, closing script/body/html tags).
- Save index.html.
- Restart the server by pressing Ctrl+C, then running:
python main.py
What do you think will happen when you fill in the form and click Post Update?
- Open http://localhost:8080 in your browser.
- Fill in all four fields with test data.
- Click Post Update.
Your standup entry should appear below the form instantly, without a page refresh. The form clears itself and is ready for the next entry.
Form submits but nothing appears?
Open your browser's developer console (F12 or Cmd+Option+I on macOS) and check the Console tab for errors. A red error mentioning /api/standups means the API route has an issue.
Make sure your main.py still has both the GET and POST routes from the previous step. Also verify that Application Default Credentials are still active from when you ran gcloud auth application-default login earlier.
help me debug why my standup form is not saving entries
Redeploy the complete app to Cloud Run
Your standup board works locally with a real form and live data. Time to push the final version to Cloud Run so anyone can access it.
- Make sure your terminal is in the standup-board directory, not inside templates or venv.
- Redeploy to Cloud Run by running:
gcloud run deploy standup-board --source . --region us-central1 --allow-unauthenticated
What happens during redeployment?
This is the same deploy command you used in Step 3. Cloud Build detects your new templates/index.html file and updated main.py, builds a fresh container image, and replaces the running service with the new version.
Before you open the live URL, what do you expect the standup board to look like compared to your local version?
- Wait for the deployment to finish and copy the service URL from the terminal output.
- Open the URL in your browser.
- Fill in the standup form with a test entry and click Post Update.
Your entry should appear on the board instantly. You now have a working standup board live on the internet, backed by Firestore, that anyone with the URL can use.
Live URL shows an error?
Check the terminal output for build errors. A common issue is running the deploy command from inside the templates or venv folder instead of the root standup-board folder.
If the page loads but the form does not work, open the browser console to check for errors. The Cloud Run logs in the Google Cloud Console can also show server-side issues.
help me debug my Cloud Run deployment error
Secret mission
Add Date Navigation
Your standup board only shows today's updates. What about yesterday? Last Friday? A real standup tool lets you look back in time. In this secret mission, you will add a date picker that filters standups by any date you choose.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project uses Google Cloud's free tier for both Cloud Run and Firestore, so there are no ongoing costs for normal portfolio-level usage.
Resources you used:
- Google Cloud Run service: standup-board in us-central1 (free tier covers up to 2 million requests/month).
- Firestore database in Native mode, location nam5 (free tier covers 1 GiB storage, 50,000 reads/day, 20,000 writes/day).
- Artifact Registry repository (created automatically during Cloud Run deployment to store container images).
- Local standup-board project folder on your Desktop.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep your standup board live as a portfolio piece.
- Your Cloud Run service stays live at its public URL. The free tier covers 2 million requests per month, which is more than enough for a demo project.
- Your Firestore database keeps storing standup entries. The free tier covers 1 GiB of storage and 50,000 reads per day.
- The Google Cloud CLI remains installed on your machine. You can use it for future Google Cloud projects.
Pause - I'll come back to this later
Remove the live Cloud Run service so it stops serving requests, but keep your Firestore data and local files intact for when you return.
- Delete the Cloud Run service by running this command in your terminal:
gcloud run services delete standup-board --region us-central1
- When prompted, confirm by typing Y.
- Your Firestore data and local project files stay untouched. When you're ready to come back, redeploy with the same gcloud run deploy command from Step 3.
Delete - I don't want to use this again
Remove all cloud resources and local files to start completely fresh.
The fastest way to remove all cloud resources at once is to delete the entire Google Cloud project. This removes the Cloud Run service, Firestore database, and Artifact Registry repository in one action.
- Open the Google Cloud project settings page at:
https://console.cloud.google.com/iam-admin/settings
- Make sure your standup board project is selected in the project dropdown at the top of the page.
- Click Shut down.
- Enter the project ID when prompted, then click Shut down again to confirm.
What happens when I shut down a project?
The project enters a 30-day pending deletion state. During that window, you can restore it if you change your mind. After 30 days, the project and all its resources are permanently removed.
To delete your local project files, run this command in your terminal:
macOS
rm -rf ~/Desktop/standup-board
Windows
rmdir /s /q %USERPROFILE%\Desktop\standup-board
- This removes the standup-board folder, including your virtual environment and all project files.
Nice Work!
Nice Work!
You just built a full-stack team standup board and deployed it live on Google Cloud. This is a real tool that engineering teams use every single day, and you built it from scratch.
You've learned how to:
- Build a Python Flask web application from scratch with HTML templates and a REST API.
- Deploy a live, publicly accessible app to Google Cloud Run using source-based deployment.
- Store and retrieve data from a Firestore NoSQL database using the Python client library.
- Secret Mission: Added date navigation to browse historical standup entries.
Ready to quiz yourself?