Leanplate is a levelsio-style PHP micro-stack template: plain functions, SQLite, no framework, no build step. One-person SaaS in a handful of files.
cp src/config/config.example.php src/config/config.php
php -S 127.0.0.1:8000 -t public scripts/router.phpPHP built-in server serves from public/; scripts/router.php mimics
nginx clean URLs so extensionless links work locally. Sources (src/,
data/, logs/) are never directly reachable.
- Run
phpstan analysebefore committing; keep it clean at level 5 (config inphpstan.neon).
- Plain functions, no classes. No autoloader.
snake_casefor functions and DB columns.- Every page's first line is the bootstrap require, relative to its depth: root pages (
public/index.php) userequire __DIR__ . '/../src/bootstrap.php';; grouped pages (public/auth/login.php) userequire __DIR__ . '/../../src/bootstrap.php';. - Pages are grouped by concern:
public/auth/,public/app/,public/billing/, andpublic/webhooks/. Folders are URL segments; nginx drops the.php(clean URLs). - Authenticated product pages live under
/app; machine callbacks live under/webhooks. - Prepare statements with
?placeholders. Never interpolate user input into SQL. - All dynamic output escaped via
htmlspecialchars(). No exceptions. - POST redirect pattern: validate, write via prepared statement,
header('Location: ...'),exit. - Keep interactive state (search/filter/sort) in the query string so URLs are shareable; read it from
$_GETand render links/inputs pre-filled.
| Layer | Files |
|---|---|
| Config | src/config/app.php (committed identity), src/config/config.php (gitignored environment/secrets), src/config/config.example.php |
| Bootstrap | src/bootstrap.php - composition root; loads config and modules, then initializes the request |
| Runtime | src/lib/runtime.php - directories, error policy, hardened session, fatal alerts |
| DB | src/lib/db.php - shared PDO and migration runner; numbered functions live in src/db/migrations/ and use PRAGMA user_version |
| HTTP | src/lib/http.php - CSRF fields/checks and query-string status flags |
src/lib/mail.php - pluggable transport (log writes to logs/mail.log) |
|
| Layout | src/lib/layout.php - shared HTML chrome (layout_header()/layout_footer()) |
| Auth | src/app/auth.php - magic links, Google OAuth, find_or_create_user(), |
login_user(), require_login() |
|
| Account | src/app/account.php - profile updates and coordinated account deletion |
| Feedback | src/app/feedback.php - persistence, notifications, and shared widget |
| Payments | src/app/stripe.php - Checkout, portal, subscription cancellation, webhooks |
| Pages | public/* - thin endpoints grouped under auth/, app/, billing/, webhooks/ |
| PWA | public/manifest.php, public/service-worker.js, public/assets/js/pwa.js, public/assets/icons/ |
src/config/app.php is the single committed customization surface for shared
product identity. Environment config degrades gracefully when keys are blank
(mail goes to log, integration buttons hide). Google OAuth endpoints are
constants in src/app/auth.php.
- Add the next numbered file under
src/db/migrations/, require it fromsrc/lib/db.php, and append its function to the$migrationsmap. Never edit, delete, or renumber a migration that may have shipped. - Add function(s) to the relevant file - reusable infra in
src/lib/, app domain insrc/app/- and require it frombootstrap.phpif needed. - Add/edit a page in
public/, starting with the bootstrap require. - Validate input, use prepared statements, escape all output, redirect after POST.
Product identity keys in src/config/app.php: name, short_name,
description, tagline, theme_color, background_color, and shared
links.
| Key | Effect when blank |
|---|---|
mail_transport (or Resend key) |
Writes mail to logs/mail.log |
stripe_secret_key / stripe_price_id |
Hides upgrade button |
google_client_id / google_client_secret |
Hides Google button |