A lightweight, multi-user web notes application built with PHP 8.2, Slim 4, Twig, HTMX, and Alpine.js. Designed for deployment on Virtualmin/LAMP servers.
- User authentication (registration, login, password reset)
- Folder management with drag-and-drop reordering
- Rich-text note editing with formatting support
- Full-text search across notes
- Responsive UI with Bootstrap 5
- RESTful API with HTMX for seamless updates
- Automatic trash purging (30 days)
- Backend: PHP 8.2+, Slim 4, Twig, PDO
- Frontend: HTMX, Alpine.js, Bootstrap 5
- Database: MariaDB 10.6+ / MySQL 8.0+ with full-text search
- Security: Argon2id password hashing, CSRF protection, sessions, rate limiting
- Email: PHPMailer with SMTP or sendmail support
- PHP 8.2+ with extensions:
pdo_mysql,mbstring,intl - MariaDB 10.6+ or MySQL 8.0+
- Composer for dependency management
- Virtualmin control panel (recommended for deployment)
- Apache/Nginx web server with URL rewriting enabled
-
Clone the repository
git clone <repository-url> cd hamnotes
-
Install dependencies
composer install
-
Configure environment
cp .env.example config/env.php # Edit config/env.php with your database and SMTP settings -
Set up database
# Create database and user, then run migration php migrations/migrate.php -
Start development server
# Point your web server document root to the public/ directory # Or use PHP's built-in server for testing: cd public php -S localhost:8000
-
Create a new virtual server in Virtualmin:
- Domain:
notes.yourdomain.com - Enable SSL (Let's Encrypt recommended)
- PHP version: 8.2 or higher
- Domain:
-
Configure PHP settings:
- Memory limit: 256MB minimum
- Max execution time: 300 seconds
- Upload max filesize: 10MB (for future attachments)
- Post max size: 10MB
-
Create database:
- Database name:
hamnotes - Database user:
hamnotes_user - Grant all privileges on
hamnotes.*tohamnotes_user
- Database name:
-
Upload files to your Virtualmin server:
# Upload the entire project to /home/yourdomain/public_html/ # Or create a subdirectory like /home/yourdomain/notes/
-
Install dependencies:
cd /home/yourdomain/public_html/ composer install --no-dev --optimize-autoloader -
Configure environment:
cp .env.example config/env.php # Edit config/env.php with production values: # - Database credentials from Virtualmin # - SMTP settings (use localhost for sendmail, or configure external SMTP) # - App URL: https://notes.yourdomain.com # - Environment: 'production'
-
Run database migration:
php migrations/migrate.php
-
Set proper permissions:
chown -R yourdomain:yourdomain /home/yourdomain/public_html/ chmod -R 755 /home/yourdomain/public_html/ chmod 600 config/env.php # Secure config file
The included .htaccess file handles URL rewriting. Ensure:
- Apache:
mod_rewriteis enabled - Document root: Points to
public/directory - AllowOverride: Set to
Allfor.htaccesssupport
For password reset functionality:
Option A: Sendmail (recommended for Virtualmin)
// In config/env.php
'smtp' => [
'host' => 'localhost',
'port' => 587,
'user' => '', // Leave empty
'pass' => '', // Leave empty
'from' => 'noreply@yourdomain.com',
],Option B: External SMTP
'smtp' => [
'host' => 'smtp.gmail.com', // Or your SMTP provider
'port' => 587,
'user' => 'your-email@gmail.com',
'pass' => 'your-app-password',
'from' => 'noreply@yourdomain.com',
],Set up daily cron job to purge trashed notes older than 30 days:
- Via Virtualmin: Go to
Server Configuration > Scheduled Cron Jobs - Add cron job:
Command: cd /home/yourdomain/public_html && php cli/purge_trash.php When to execute: Daily (0 2 * * *) - runs at 2 AM daily
Or via command line:
crontab -e
# Add: 0 2 * * * cd /home/yourdomain/public_html && php cli/purge_trash.php-
SSL/TLS: Ensure SSL is enabled and forced (Virtualmin handles this)
-
File permissions: Keep sensitive files secure:
chmod 600 config/env.php chmod 600 .env.example
-
Rate limiting: Built-in rate limiting protects against brute force attacks
-
CSRF protection: Enabled by default for all forms
-
Session security: Configure secure session settings in PHP:
session.cookie_secure = 1 session.cookie_httponly = 1 session.cookie_samesite = Lax
-
Clone and install as described in Quick Start
-
Enable error reporting in
config/env.php:'app' => [ 'env' => 'development', // ... ],
-
Run tests:
vendor/bin/phpunit
-
Use CLI scripts:
# Disable a user php cli/disable_user.php user@example.com # Purge trash manually php cli/purge_trash.php
Database connection fails
- Verify database credentials in
config/env.php - Ensure database user has proper permissions
- Check if MySQL/MariaDB service is running
Emails not sending
- For sendmail: Check mail logs in Virtualmin
- For SMTP: Verify SMTP credentials and server settings
- Test with a simple PHP mail script
404 errors on routes
- Ensure
.htaccessis working (check Apache config) - Verify document root points to
public/directory - Clear browser cache
Permission errors
- Run:
chown -R domainuser:domainuser /home/domainuser/public_html/ - Ensure PHP can write to necessary directories
Composer install fails
- Ensure PHP 8.2+ is available
- Check memory limit:
php -r "echo ini_get('memory_limit');"
- PHP errors: Check Virtualmin's error logs
- Application logs: Add error logging to your PHP code if needed
- Database queries: Enable query logging in MySQL for debugging
- Database: Ensure proper indexes (included in schema)
- PHP: Use OPcache in production
- Static assets: Consider CDN for Bootstrap/HTMX files in production
The application uses four main tables:
users: User accounts with Argon2id hashed passwordsfolders: User folders with drag-drop orderingnotes: Notes with full-text search on title/bodypassword_resets: Secure password reset tokens
Run php migrations/migrate.php to set up the database schema.
The app uses HTMX for dynamic updates. Key endpoints:
GET/POST /login,/register,/logoutGET /- DashboardPOST /folders- CRUD operationsGET/POST /notes/{id}- Note operationsGET /search?q=...- Full-text search
- Fork the repository
- Create a feature branch
- Make changes with tests
- Submit a pull request
MIT License
For issues and questions, please check the troubleshooting section or create an issue in the repository.