Stack: PHP 8 β’ MySQL β’ Vanilla JS β’ IndexedDB β’ Service Worker β’ OKLCH Design Tokens Live Demo: https://lab.minischetti.org/markly/
A login-protected, offline-capable Markdown notebook powered by pure PHP and the web platform. No frameworks. No build pipeline. Deployable anywhere - shared hosting, VPS, or containers.
- Project type: Offline-first Markdown workspace with full CRUD, tagging, search, backlinks, and public note sharing
- Frontend: Vanilla JS modules with live preview, metadata panel, keyboard shortcuts
- Data layer: IndexedDB (offline cache + outbox queue)
- Backend: PHP 8, PDO, secure sessions, CSRF, optimistic locking
- Deployment: Drop-in
/htdocsstructure; works from FTP hosting to Kubernetes - Design: OKLCH colors, responsive split views, smooth micro-interactions
- Lines of code: ~2.8k
- Dependencies: None (optional CDN: Marked.js + DOMPurify)
- DB tables:
users,notes - APIs:
/api/notes.php,/api/auth.php,/api/publish.php - Storage: IndexedDB for offline notes + request outbox
- Search: MySQL FULLTEXT with LIKE fallback
- Tests: PHPUnit for repositories, utilities, and CSRF lifecycle
- Service worker precaches the shell
- IndexedDB stores notes, tags, and queued mutations
- On reconnect: queued requests replay with optimistic locking + conflict detection
- Status pills: Queued β Saving β Saved
- Notes carry a
versionfield - Client sends
If-MatchETag - API only updates when versions match
- Conflicts return
409 Conflictwith server copy for merge flow
- Session hardening + SameSite cookies
- CSRF rotation across login/logout
- Prepared statements everywhere
- DOMPurify for preview sanitization
- Public pages fully escaped with
htmlspecialchars
- Pure ES modules
- Static markup + API endpoints
- Works on shared hosting with no Node/Composer
-
π Solid Authentication Email/password login, CSRF, ID regeneration, hardened sessions.
-
π Markdown Workspace Live preview, tags, backlinks, autosave, keyboard shortcuts, metadata drawer.
-
π Smart Organisation FULLTEXT search, tag filtering, backlinks discovery.
-
πΆ Offline-Ready Shell cache + IndexedDB cache + request outbox + graceful sync toasts.
-
π Beautiful UI OKLCH-based light/dark themes, polished transitions, friendly on both mobile and desktop.
-
βοΈ Deploy Anywhere No Composer, no build tools - just PHP and MySQL.
- CSRF tokens on all state-changing requests
- Secure session cookies (HttpOnly + SameSite)
- Session ID regeneration
- DOMPurify for safe HTML preview
- Prepared statements with PDO
- Public note view fully sanitized
| Layer | Details |
|---|---|
| Backend | Pure PHP 8, PDO, no frameworks |
| Database | MySQL (utf8mb4), FULLTEXT index on title/body |
| Domain | /src classes: Auth, NotesRepo, LinksRepo, Csrf, etc. |
| SPA | Vanilla JS: app.js, api.js, editor.js, db.js |
| Offline | Service worker + IndexedDB caches + outbox queue |
Replayed changes with version checks and graceful recovery using ETags and 409 responses.
Queued operations deduplicated by type, note ID, and payload hash.
Tokens rotate on login/logout, with the API holding a short pool to allow concurrent requests.
All functionality implemented without Composer, Node, or frameworks.
Proper SW scope for installs in /htdocs or subdirectories - avoids cache poisoning and routing edge cases.
index.phpboots sessions and loads the SPA./api/*.phpendpoints handle CRUD, tags, backlinks, and publish toggles.- Strict JSON output, early exits, no body rendering.
- JS modules control sidebar, editor, sync state, keyboard shortcuts, and live preview.
- ETags from server map to optimistic locking version numbers.
- IndexedDB stores notes + pending changes.
- On reload: notes come from DB immediately; API refreshes them when online.
- Outbox replays sequentially with conflict resolution.
- Service worker lifecycle, scoping, and cache versioning
- IndexedDB patterns: object stores, cursors, atomic transactions
- Practical optimistic UI design
- Secure PHP session and CSRF handling
- PWA considerations (manifest, icons, offline shell)
- Designing a cohesive visual system with OKLCH
- PHP β₯ 8.0
- MySQL 5.7+ / MariaDB 10.3+
- Apache or Nginx
- HTTPS (required for service worker)
- Clone the repository
git clone https://github.com/yourusername/markly.git
cd markly- Create the database
CREATE DATABASE markly CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;- Import schema (and seed)
mysql -u <user> -p markly < htdocs/sql/schema.sql
mysql -u <user> -p markly < htdocs/sql/seed.sql- Configure the app
cp htdocs/config/config.example.php htdocs/config/config.phpSet up credentials
'db' => [
'dsn' => 'mysql:host=localhost;dbname=markly;charset=utf8mb4',
'user' => 'your_db_user',
'pass' => 'your_db_password',
],-
Deploy to web server
- Upload
htdocs/contents to your web root - Ensure
.htaccessis present (Apache) or configure equivalent for Nginx - Set proper file permissions (755 for directories, 644 for files)
- Upload
-
Access the application
- Navigate to your domain:
https://yourdomain.com/ - Login with demo credentials:
admin@example.com/admin123
- Navigate to your domain:
Using PHP's built-in server:
cd htdocs
php -S localhost:8000Visit http://localhost:8000/login.php
Note: Service Worker requires HTTPS in production. For local development, localhost is exempt.
- Upload the
htdocs/directory to your hosting account's web root via FTP or file manager. - Create the database using the hosting control panel and import
schema.sql+seed.sql. - Configure environment variables or
config.phpwith production credentials. - Confirm
.htaccessrules are active for clean URLs, service worker scope, and manifest MIME types.
- Provision PHP 8, MySQL, and a web server (Apache or Nginx + PHP-FPM).
- Clone the repository or deploy via CI/CD.
- Configure vhost to serve
htdocs/as the document root and enable HTTPS (Let's Encrypt recommended). - Set environment variables in your process manager (systemd, supervisord) or web server config.
- Harden permissions: non-root user,
chmod644/755, and restrict write access to necessary directories.
- Use the provided PHP/Apache or PHP-FPM base image.
- Copy
htdocs/into the container and mount persistent storage for logs/uploads if required. - Run migrations via
mysqlclient container or use docker-compose service for MySQL. - Expose port 80/443 through your orchestrator and configure HTTPS termination.
- Email:
admin@example.com - Password:
admin123
Feel free to change the password or add additional users directly in the users table.
- Notes opened while online are cached in IndexedDB for quick access offline.
- Edits while offline queue into the outbox; when connectivity returns, creates/updates/deletes/publish toggles replay sequentially with conflict detection.
- Toasts announce
Syncingβ¦andSyncedstates, while the status pill reflectsQueued,Savingβ¦, orSaved. - The service worker precaches the shell and keeps IndexedDB + the API in sync using a network-first strategy with cache fallback.
- Database created and both
htdocs/sql/schema.sql+htdocs/sql/seed.sqlimported. -
htdocs/config/config.phpupdated with live credentials. -
/htdocs(including.htaccess,manifest.webmanifest, andsw.js) uploaded to the server root. - Service worker registered successfully (check browser DevTools β Application β Service Workers).
- Login with the demo account works and sessions persist across reloads.
- Create, edit, delete, and publish notes online and offline; queued changes sync after reconnecting.
- Responsive layout verified on desktop (split view), tablet (sticky tabs), and mobile (drawer sidebar, full-width editor).
- Public permalink (
/index.php?p=<slug>) renders the note for unauthenticated visitors.
# Install PHPUnit (if not already installed)
composer require --dev phpunit/phpunit ^10.0
# Run test suite
./vendor/bin/phpunitTest Coverage: Core domain logic (NotesRepo, TextUtil, Csrf) covered with fast in-memory databases.
MIT License - see LICENSE.