Build a PHP guestbook with MySQL

A complete PHP guestbook served by the PHP Web Server application and backed by a managed MySQL database — connected with PDO, prepared statements throughout, verified against a real MySQL server. Your data survives reinstalls.

This build is a classic: a guestbook where visitors leave a message and everyone sees the list. It runs on the PHP Web Server application and stores entries in a managed MySQL database, connected with PDO — PHP's standard database layer — using prepared statements everywhere. Because the database is managed and separate from your server, this guestbook's data survives reinstalls, unlike a file-based store.

At a glance
You need A server running the PHP Web Server application, and a managed MySQL database
Plan Any plan — free runs while your session timer has time left, premium runs 24/7
Time About thirty minutes
No server yet? Create your first app server

This build assumes you've read PHP on Falix (the three PHP applications and why the Web Server one needs its config) and Host a PHP website. PDO is built into PHP, so there's no Composer step here.

Step 1 — Get the web server serving

⚠️ Start from the template. A fresh PHP Web Server doesn't fill in your port by default, which leaves nginx crash-looping. Deploy the PHP Website template first (from the PHP website guide or your server's Templates page) — it ships the corrected web-server config plus a working public/ folder. Everything below then replaces the sample page.

With the template deployed, your site serves files from the Web Root (the public folder by default) on your public port automatically — no port code needed in PHP.

Step 2 — Create the database

Open the Databases page from your server menu and create a MySQL database (Add a database walks through it). You'll get:

  • a database name shaped like s{serverId}_guestbook,
  • a username shaped like u{serverId}_random,
  • a generated password, and a host and port.

Keep that page open — you'll paste those five values in a moment. Leave Remote Access at % so your app can connect.

Step 3 — The files

Create two files. First, db.php in the server's root folder — outside public, so it's never served to the web — holding your credentials and the connection:

<?php
// db.php — connection details from your server's Databases page.
// Keep this file OUT of your web root and out of Git.
$DB_HOST = 'your-db-host.falixnodes.net';
$DB_PORT = '3306';
$DB_NAME = 's123_guestbook';   // shaped s{serverId}_name
$DB_USER = 'u123_ab12cd34';    // shaped u{serverId}_random
$DB_PASS = 'PASTE_YOUR_PASSWORD_HERE';

$dsn = "mysql:host=$DB_HOST;port=$DB_PORT;dbname=$DB_NAME;charset=utf8mb4";
$pdo = new PDO($dsn, $DB_USER, $DB_PASS, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
]);

$pdo->exec(
    "CREATE TABLE IF NOT EXISTS entries (
        id      INT AUTO_INCREMENT PRIMARY KEY,
        name    VARCHAR(80) NOT NULL,
        message TEXT NOT NULL,
        created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )"
);

return $pdo;

Fill the five values in from the Databases page. Then public/index.php — the page itself:

<?php
$pdo = require __DIR__ . '/../db.php';

// A submitted form (POST): validate, insert with a prepared statement,
// then redirect so a refresh doesn't post twice.
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name    = trim($_POST['name'] ?? '');
    $message = trim($_POST['message'] ?? '');
    if ($name !== '' && $message !== '') {
        $stmt = $pdo->prepare('INSERT INTO entries (name, message) VALUES (?, ?)');
        $stmt->execute([mb_substr($name, 0, 80), mb_substr($message, 0, 2000)]);
    }
    header('Location: /');
    exit;
}

$entries = $pdo
    ->query('SELECT name, message, created FROM entries ORDER BY id DESC LIMIT 100')
    ->fetchAll();

function h($s) { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); }
?>
<!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">
    <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>
    <?php foreach ($entries as $e): ?>
      <li><strong><?= h($e['name']) ?></strong>
          <small>(<?= h($e['created']) ?>)</small><br>
          <?= h($e['message']) ?></li>
    <?php endforeach; ?>
    <?php if (!$entries): ?><li>Be the first to sign.</li><?php endif; ?>
  </ul>
</body>
</html>

Step 4 — How it works

  • PDO with a DSN. new PDO("mysql:host=...;dbname=...", $user, $pass) opens the connection. The options turn on exceptions (so a failure is loud, not silent), set a sensible default fetch mode, and — importantly — set ATTR_EMULATE_PREPARES => false so real server-side prepared statements are used.
  • The table is created once with CREATE TABLE IF NOT EXISTS, so the app is safe to load repeatedly.
  • Every insert is a prepared statement. $pdo->prepare('... VALUES (?, ?)') then execute([...]) sends your values separately from the SQL, so a message like '; DROP TABLE entries;-- is stored as literal text, never executed. This is the single most important habit in database code.
  • htmlspecialchars on output (the h() helper) means a message containing HTML shows as text instead of running in visitors' browsers.
  • Post-then-redirect (header('Location: /')) stops a page refresh from re-submitting the form.

🎯 Good to know: db.php lives outside public, so even though it holds your password, the web server never serves it — visitors can only reach files under the Web Root. public/index.php pulls it in with require __DIR__ . '/../db.php'.

Step 5 — Verify it works

Restart the server and open your site's address (from the Network page). Sign the guestbook — your entry appears at the top of the list. Reload the page: it's still there, because it came from MySQL, not memory. For a second opinion, open phpMyAdmin from the Databases page and you'll see your rows in the entries table.

Why the database placement matters

The managed database is the payoff here:

🎯 It survives. Your server's files get wiped by a reinstall or an application switch — but a managed database lives on a separate always-on host and is untouched. On the free plan, it even stays up when your session timer stops the server. Your guestbook entries outlive the server they're shown from. (Keep db.php safe and re-uploadable — that file is on the server — but the data itself is safe in MySQL.)

If you ever rotate the database password on the Databases page, paste the new one into db.php and restart — nothing else changes.

Troubleshooting

  • nginx keeps restarting (host not found in "SERVER_PORT") — the web config never got your port. Deploy the PHP Website template, which ships the corrected config (Step 1).
  • SQLSTATE[HY000] [1045] Access denied — the username or password in db.php is wrong or stale. Copy them fresh from the Databases page; rotate the password there if unsure.
  • SQLSTATE[HY000] [2002] / connection refused or timeout — the host is wrong. Use the host shown on the database page, not your server's own address, and keep Remote Access at %.
  • Blank page / raw PHP shown — your files aren't in the Web Root, or the entry file isn't index.php. Check the Web Root variable and that index.php is inside public.
  • Can't reach the site at all — a port or address issue: I can't reach my app.

PDO's full API — transactions, named parameters, fetching styles — is documented at php.net/pdo. For wiring connection strings across languages, see Connect your app to a database.


Next steps

Was this guide helpful?