Alpha ZealPHP is early-stage and under active development. APIs may change between minor versions until v1.0. Feedback and bug reports welcome on GitHub.

User Accounts

SQLite, password hashing, and an auth guard. Real accounts in 50 lines.

You will learn

  • Store user data in SQLite with PDO
  • Hash passwords properly (never store plaintext)
  • Build register and login forms
  • Guard pages so only logged-in users can access them

The problem

Sessions remember this browser, but they don't know who is using it. Anyone who opens your app can see anyone else's data. You need real user accounts: a username, a password, and a way to prove "I am who I say I am."

Step 1: A database

You need a place to store users. SQLite is perfect for this — it's a database in a single file. No server to install, no credentials to configure. PHP includes PDO (PHP Data Objects) for talking to databases.

// src/Learn/DB.php — open the database and create tables
$pdo = new \PDO('sqlite:' . __DIR__ . '/../../storage/learn.db');
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$pdo->query('PRAGMA journal_mode = WAL');
$pdo->query('PRAGMA foreign_keys = ON');

$pdo->query("CREATE TABLE IF NOT EXISTS users (
    id            INTEGER PRIMARY KEY AUTOINCREMENT,
    username      TEXT UNIQUE NOT NULL,
    password_hash TEXT NOT NULL,
    created_at    INTEGER NOT NULL
)");

WAL mode lets multiple coroutines read the database simultaneously (important for ZealPHP). foreign_keys enforces data integrity when we add notes in the next lesson.

Step 2: Register a user

The key insight: never store passwords as plaintext. PHP's password_hash() generates a one-way hash that can't be reversed. Even if someone steals your database, they can't read the passwords.

// src/Learn/Auth.php — register
public static function register(\PDO $db, string $username, string $password): ?int
{
    $hash = password_hash($password, PASSWORD_DEFAULT);
    $stmt = $db->prepare(
        'INSERT INTO users (username, password_hash, created_at) VALUES (?, ?, ?)'
    );
    $stmt->execute([$username, $hash, time()]);
    return (int) $db->lastInsertId();
}

Think of password_hash() like a safe with a one-way lock. You put the password in, the safe locks, and even you can't open it to see what's inside. But you can check whether a new password matches the one inside — that's password_verify().

Step 3: Log in

// src/Learn/Auth.php — login
public static function login(\PDO $db, string $username, string $password): ?int
{
    $stmt = $db->prepare('SELECT id, password_hash FROM users WHERE username = ?');
    $stmt->execute([$username]);
    $user = $stmt->fetch();
    if (!$user || !password_verify($password, $user['password_hash'])) {
        return null;
    }
    return (int) $user['id'];
}

password_verify() checks whether the password matches the hash without ever decrypting it. If it matches, store the user ID in the session — now the server knows who you are on every request.

Step 4: The auth guard

Any page that needs a logged-in user checks the session at the top:

$user = Auth::currentUser();
if (!$user) {
    // Show login form
} else {
    // Show the protected content
}

Auth::currentUser() reads $g->session['user_id'], looks up the user in SQLite, and returns the user row or null. If the session has a stale user_id (e.g., after a database reset), it returns null too.

Architecture: proper OOP

Notice how the auth logic lives in src/Learn/Auth.php — a proper class, autoloaded via Composer. The API endpoint (api/learn/register.php) is a thin wrapper:

// api/learn/register.php — thin endpoint
$register = function () {
    $creds = Auth::readCredentials($this);
    $userId = Auth::register(DB::open(), $creds['username'], $creds['password']);
    // ... set session, redirect
};

Business logic in src/, endpoints in api/. The endpoint delegates; the class does the work. This pattern scales — your API files stay under 20 lines each.

Register now

Pick a username and password. This creates a real account stored in SQLite. You'll use it in the next three lessons to save notes and chat with the AI.

Already have an account?

Challenge: change password

Build a "change password" feature. You'll need: a form with old password and new password fields, an endpoint that verifies the old password with password_verify(), then updates the hash with password_hash().

Hint 1

Use a prepared UPDATE statement: UPDATE users SET password_hash = ? WHERE id = ?

Hint 2

Always verify the old password first — never trust the client to send only valid requests

Key Takeaways

  • SQLite + PDO gives you a full database in a single file — no server setup
  • password_hash() and password_verify() handle passwords safely
  • Store the user ID in $g->session after login — the session cookie handles the rest
  • Business logic in src/ classes, thin endpoint wrappers in api/