Build a Flask guestbook

A complete Flask app — a public guestbook where visitors sign, edit, and delete entries — backed by Python's built-in sqlite3 with no extra database to install. Full create/read/update/delete, verified end to end.

Skip the setup This guide has a one-click starter template that installs everything below onto your server.

A guestbook is the classic full-stack starter: a form, a list, and a database behind them. This build is a complete one in Flask, storing entries with Python's built-in sqlite3 module — so there's nothing extra to install beyond Flask itself, and no separate database server to run. You'll get all four data operations: create an entry, read the list, edit a message, delete one.

At a glance
You need A server running the Python application
Plan Any plan — free runs while your session timer has time left, premium runs 24/7
Time About twenty minutes
No server yet? Create your first app server

This builds on Build a Flask web app (routes and the SERVER_PORT rule) and Python on Falix (the app.py entry file and requirements.txt auto-install). Skim those if the basics are new.

Step 1 — The files

requirements.txt — just Flask; sqlite3 is part of Python's standard library:

flask

app.py:

import html
import os
import sqlite3

from flask import Flask, g, redirect, request

DB = "guestbook.db"
app = Flask(__name__)


def db():
    if "db" not in g:
        g.db = sqlite3.connect(DB)
        g.db.row_factory = sqlite3.Row
    return g.db


@app.teardown_appcontext
def close_db(exc):
    conn = g.pop("db", None)
    if conn is not None:
        conn.close()


def init_db():
    conn = sqlite3.connect(DB)
    conn.execute(
        """
        CREATE TABLE IF NOT EXISTS entries (
            id      INTEGER PRIMARY KEY AUTOINCREMENT,
            name    TEXT NOT NULL,
            message TEXT NOT NULL,
            created TEXT NOT NULL DEFAULT (datetime('now'))
        )
        """
    )
    conn.commit()
    conn.close()


@app.route("/")
def index():
    rows = db().execute("SELECT * FROM entries ORDER BY id DESC").fetchall()
    items = "\n".join(
        f"""<li><strong>{html.escape(r['name'])}</strong>
            <small>({r['created']})</small><br>{html.escape(r['message'])}
            <form method="post" action="/delete/{r['id']}" style="display:inline">
              <button type="submit">delete</button>
            </form></li>"""
        for r in rows
    )
    return f"""<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Guestbook</title></head>
<body style="font-family: system-ui, sans-serif; max-width: 40rem; margin: 3rem auto;">
  <h1>Guestbook</h1>
  <form method="post" action="/add">
    <p><input name="name" placeholder="Your name" maxlength="80" required style="width:100%"></p>
    <p><textarea name="message" placeholder="Sign the guestbook" maxlength="2000" rows="3" required style="width:100%"></textarea></p>
    <button type="submit">Sign</button>
  </form>
  <ul>{items or '<li>Be the first to sign.</li>'}</ul>
</body>
</html>"""


@app.route("/add", methods=["POST"])
def add():
    name = request.form.get("name", "").strip()
    message = request.form.get("message", "").strip()
    if name and message:
        conn = db()
        conn.execute(
            "INSERT INTO entries (name, message) VALUES (?, ?)",
            (name[:80], message[:2000]),
        )
        conn.commit()
    return redirect("/")


@app.route("/edit/<int:entry_id>", methods=["POST"])
def edit(entry_id):
    message = request.form.get("message", "").strip()
    if message:
        conn = db()
        conn.execute(
            "UPDATE entries SET message = ? WHERE id = ?",
            (message[:2000], entry_id),
        )
        conn.commit()
    return redirect("/")


@app.route("/delete/<int:entry_id>", methods=["POST"])
def delete(entry_id):
    conn = db()
    conn.execute("DELETE FROM entries WHERE id = ?", (entry_id,))
    conn.commit()
    return redirect("/")


if __name__ == "__main__":
    init_db()
    port = int(os.environ.get("SERVER_PORT", 8080))
    print(f"Listening on port {port}", flush=True)
    app.run(host="0.0.0.0", port=port)

Step 2 — How it works

This is a full CRUD app — four routes, one per operation:

Route Method Operation
/ GET Read — lists every entry, newest first
/add POST Create — inserts a new entry
/edit/<id> POST Update — changes a message
/delete/<id> POST Delete — removes an entry

A few details worth understanding:

  • The connection helper. db() opens one SQLite connection per request and stashes it on Flask's g object; the teardown_appcontext hook closes it when the request ends. That's the standard Flask pattern for sqlite3 — no connection left dangling.
  • init_db() runs once at startup, creating the table if it isn't there. IF NOT EXISTS means restarts are safe.
  • Every write uses a parameterized query — the ? placeholders with a values tuple. That keeps user input as data, so a name like '; DROP TABLE entries;-- is stored as harmless text, not executed.
  • html.escape(...) wraps every value shown on the page, so a message containing HTML displays as text instead of running in visitors' browsers.
  • Post-then-redirect. Each write ends in redirect("/"), so refreshing the page after submitting doesn't re-post the form.

💡 Tip: sqlite3 ships with Python — there's nothing to add to requirements.txt for it. The whole database is the single file guestbook.db, created next to your code on first run.

Step 3 — Start it and verify all four operations

Press Start. The console installs Flask, then prints Listening on port …. Open your server's address (from the Network page) and walk the full lifecycle:

  1. Create — sign the guestbook with a name and message. It appears in the list.
  2. Read — reload the page; your entry is still there (it's in the database now).
  3. Delete — press delete next to an entry; it's gone.
  4. Update — the /edit/<id> route is wired and ready. Add an edit form next to each entry (a <textarea> posting to /edit/{id}) to expose it in the UI, or test it directly by posting a new message.

Seeing an entry survive a page reload is the proof the database write worked — you're reading it back from disk, not from memory.

Where the data lives

Entries are stored in guestbook.db in the server's file system.

⚠️ Heads up: A reinstall — or switching the server to a different application — wipes the server's files, guestbook.db included, and it is not rebuilt for you. Take a backup before anything risky. If the guestbook needs to be durable, move storage to a managed database (which survives reinstalls) — Connect your app to a database shows the swap; the routes stay the same.

Troubleshooting

  • ModuleNotFoundError: No module named 'flask' — the install didn't run or finish. Confirm flask is in requirements.txt, or install it from the Packages page, then restart.
  • sqlite3.OperationalError: no such table: entriesinit_db() didn't run. It's called in the if __name__ == "__main__" block; make sure you started the app with python app.py (the default) and didn't remove that block.
  • A route 500s — read the console traceback; it names the file and line. print(..., flush=True) makes your own debug lines appear immediately.
  • Page won't load / connection refused — a port or bind issue. app.run(host="0.0.0.0", port=port) with port from SERVER_PORT; see I can't reach my app.

Templates, sessions, and forms beyond this are standard Flask — the official guide at flask.palletsprojects.com takes it from here.


Next steps

Was this guide helpful?