nibula 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -0,0 +1,51 @@
1
+ # Changelog
2
+
3
+ All notable changes to Nibula are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.1.0] - 2026-07-21
9
+
10
+ ### Added
11
+ - **Node.js backend** as an alternative to PHP, living side by side in
12
+ `src/backend/`: `_core/index.js` front controller (which is also the HTTP
13
+ server), `Response` and `RateLimiter` modules, a `mysql2` pool singleton
14
+ `Database.js`, `example.config.js`, and a `package.json` for the backend's
15
+ own dependencies. Same REST API as PHP: routing, `X-Api-Key` auth, CORS and
16
+ file-based rate limiting are identical.
17
+ - **Backend choice at scaffold time**: `nib new` now asks whether to use Node.js
18
+ or PHP, in addition to language and CSS framework.
19
+ - `backend/backend-node.service.example` — a ready-to-adapt **systemd** unit for
20
+ keeping the Node backend running on a server.
21
+ - `config.js` is generated from `example.config.js` at scaffold time (mirroring
22
+ the existing `config.php` generation).
23
+ - Deployment docs now cover running the Node backend with **`screen`** (quick /
24
+ small setups) and **systemd** (production), plus environment variables.
25
+
26
+ ### Changed
27
+ - **Composer is now required only when the PHP backend is chosen.** If you pick
28
+ Node, `composer install` is skipped entirely and the backend's npm
29
+ dependencies (`mysql2`) are installed instead.
30
+ - `.htaccess`, `web.config` and `nginx.conf` now cover **both** backends. On
31
+ nginx, `/api` goes to Node and automatically falls back to the PHP front
32
+ controller when Node is unreachable. On Apache/IIS the active backend is
33
+ selected by configuration (documented), since per-directory configs can't
34
+ health-check an upstream.
35
+ - `.gitignore` now also ignores `src/backend/config.js`,
36
+ `src/backend/node_modules/` and `src/backend/cache/`.
37
+ - Documentation (`README.md`, `docs/Backend.md`, `docs/Deploy.md`) updated:
38
+ backend examples now use JavaScript/Node as the reference, with the equivalent
39
+ PHP shown alongside and a note that PHP behaves identically.
40
+
41
+ ### Notes
42
+ - Both backends are always scaffolded, so you can switch later without
43
+ re-creating the project. The PHP front controller only serves `.php` endpoint
44
+ files and the Node one only `.js`, so they coexist without conflict.
45
+
46
+ ## [1.0.2]
47
+
48
+ - Previous release (PHP backend only).
49
+
50
+ [1.1.0]: https://github.com/Rhaastrake/Nibula/releases/tag/v1.1.0
51
+ [1.0.2]: https://github.com/Rhaastrake/Nibula/releases/tag/v1.0.2
package/README.md CHANGED
@@ -16,11 +16,12 @@ Building a website from scratch involves a lot of moving parts: templating, buil
16
16
  - ðŸ–ąïļ **A helpful CLI** — create a page with one command instead of hand-writing ten separate files
17
17
  - ⚙ïļ **Server configs handled for you** — `.htaccess` and `web.config` are generated automatically, and an `nginx.conf` is provided so that anyone comfortable with nginx already has what they need to run the site outside of shared hosting
18
18
  - ðŸŽĻ **Pick your CSS framework** — choose from 4 pre-installed options (or none), and switch later in a few guided steps
19
+ - 🔌 **Pick your backend** — Node.js or PHP, chosen at creation; **Composer is only needed for PHP**
19
20
  - ðŸ§Đ **Your own modules** — add your own CSS and JS/TS modules freely and easily
20
21
  - ðŸŠķ **Lightweight by default** — SCSS frameworks can be filtered so you ship only what you actually use
21
22
  - 🌍 **Open source** — free to use, free to modify, free to share
22
23
 
23
- ![Version](https://img.shields.io/badge/version-1.0.2-blue)
24
+ ![Version](https://img.shields.io/badge/version-1.1.1-blue)
24
25
  ![License](https://img.shields.io/badge/license-Apache--2.0-blue)
25
26
  ![Eleventy](https://img.shields.io/badge/11ty-v3.1.2-black)
26
27
 
@@ -35,7 +36,7 @@ Either way, the project structure stays the same, so you can start easy and leve
35
36
 
36
37
  ## Backend included
37
38
 
38
- Essential server-side functionality comes built in — no extra setup required. Backend support will soon let you **choose between PHP, Node, and Python**, so the project can grow with you.
39
+ Essential server-side functionality comes built in — no extra setup required. At project creation you **choose your backend: Node.js or PHP** (Python is planned). Both expose the **same REST API** — same routing, `X-Api-Key` auth, CORS and rate limiting — so you can even switch later without rewriting your endpoints. See [docs/Backend.md](docs/Backend.md) for details.
39
40
 
40
41
  ## Customizable, but with sensible defaults
41
42
 
@@ -43,8 +44,8 @@ Nibula ships with a clean, opinionated layout so beginners are never lost. But i
43
44
 
44
45
  ## Prerequisites
45
46
 
46
- * **Node.js**: v18.0.0 or higher
47
- * **Composer**: latest stable version
47
+ * **Node.js**: v18.0.0 or higher — **always required** (the Nibula CLI, the build, and the optional Node backend all run on Node)
48
+ * **Composer**: **only required if you choose the PHP backend**, to install its PHP dependencies. If you pick the **Node** backend, Composer is never used — you can skip installing it entirely.
48
49
  * *Optional:* the **Better Nunjucks** VS Code extension by Ed Heltzel
49
50
 
50
51
  ## Installation
@@ -65,7 +66,7 @@ From the folder where you keep your websites, run:
65
66
  nib new your-project
66
67
  ```
67
68
 
68
- The scaffolder is interactive: you choose the language (JavaScript/TypeScript) and the CSS framework, and all dependencies are installed automatically.
69
+ The scaffolder is interactive: you choose the **language** (JavaScript/TypeScript), the **CSS framework**, and the **backend** (Node.js or PHP). All dependencies are installed automatically — and if you pick **Node**, the PHP/Composer step is skipped, so you don't need Composer at all.
69
70
 
70
71
  Then start the dev server and visit `localhost:8080`:
71
72
 
@@ -99,7 +100,20 @@ nib cli
99
100
 
100
101
  See [docs/Assistant CLI.md](docs/Assistant%20CLI.md) for details.
101
102
 
103
+ ## Deploying
104
+
105
+ Nibula builds a static site into your `out` folder, which you upload to a web
106
+ server. Which backend you chose affects deployment:
107
+
108
+ - **PHP** runs on ordinary shared hosting (Apache/IIS) or a VPS with PHP-FPM — no process to keep alive.
109
+ - **Node** runs as a long-running service on a VPS; the web server reverse-proxies `/api` to it.
110
+
111
+ The shipped `.htaccess`, `web.config` and `nginx.conf` cover **both** backends.
112
+ See [docs/Deploy.md](docs/Deploy.md) for the full guide, including how to start
113
+ the Node service on your server.
114
+
102
115
  ## Roadmap
103
116
 
104
117
  * [ ] Add support for multiple themes
105
- * [ ] Backend integration choice — switch between PHP, Python, or Node
118
+ * [x] Backend integration choice — pick **PHP or Node.js** at creation (Python planned)
119
+ * [ ] Extend documentation with advanced usage examples
package/bin/create.js CHANGED
@@ -28,10 +28,15 @@ const FRAMEWORK = Object.freeze({
28
28
  NONE: 'none',
29
29
  });
30
30
 
31
+ const BACKEND = Object.freeze({
32
+ NODE: 'node',
33
+ PHP: 'php',
34
+ });
35
+
31
36
  // ── CHOICES ──────────────────────────────────────────────────────────────────
32
37
 
33
38
  const LANGUAGE_CHOICES = [
34
- { label: 'JavaScript (recomened)', value: LANGUAGE.JAVASCRIPT },
39
+ { label: 'JavaScript (recomended)', value: LANGUAGE.JAVASCRIPT },
35
40
  { label: 'TypeScript', value: LANGUAGE.TYPESCRIPT },
36
41
  ];
37
42
 
@@ -43,6 +48,17 @@ const FRAMEWORK_CHOICES = [
43
48
  { label: 'None', value: FRAMEWORK.NONE },
44
49
  ];
45
50
 
51
+ const BACKEND_CHOICES = [
52
+ { label: 'Node.js (index.js — no PHP, no Composer)', value: BACKEND.NODE },
53
+ { label: 'PHP (index.php — Composer dependencies)', value: BACKEND.PHP },
54
+ ];
55
+
56
+ // Runtime dependency added to the ROOT package.json when the Node backend is
57
+ // chosen, so it lands in the root node_modules (never in src/backend).
58
+ const NODE_BACKEND_DEPENDENCIES = {
59
+ mysql2: '^3.11.0',
60
+ };
61
+
46
62
  // ── COPY CONFIG ───────────────────────────────────────────────────────────────
47
63
 
48
64
  const MANDATORY_COPY = [
@@ -62,6 +78,14 @@ const CREATE_DIRS = [
62
78
  'src/frontend/_routes',
63
79
  ];
64
80
 
81
+ // Backend files that belong to exactly one backend, matched by basename.
82
+ // Everything else (migrations, .htaccess, web.config, README, ...) is shared.
83
+ const NODE_ONLY_FILES = new Set(['package.json', 'backend-node.service.example']);
84
+ const PHP_ONLY_FILES = new Set(['composer.json', 'composer.lock']);
85
+ const PHP_ONLY_DIRS = new Set(['vendor']);
86
+ // Runtime artifacts that must never be copied from the template, either way.
87
+ const BACKEND_SKIP_DIRS = new Set(['node_modules', 'cache', '.git']);
88
+
65
89
  // ── FRAMEWORK CONFIG ──────────────────────────────────────────────────────────
66
90
 
67
91
  const ALL_FRAMEWORKS = Object.values(FRAMEWORK).filter(f => f !== FRAMEWORK.NONE);
@@ -117,6 +141,8 @@ node_modules/
117
141
  src/backend/_core/vendor/
118
142
  out/
119
143
  src/backend/config.php
144
+ src/backend/config.js
145
+ src/backend/cache/
120
146
  `;
121
147
 
122
148
 
@@ -185,6 +211,48 @@ function copyRecursive(src, dest, exclude = []) {
185
211
  }
186
212
  }
187
213
 
214
+ /**
215
+ * Decide whether a backend entry belongs to the chosen backend.
216
+ * Returns false for entries that must be SKIPPED.
217
+ */
218
+ function backendEntryKept(basename, isDir, backend) {
219
+ // Never copy runtime artifacts from the template.
220
+ if (isDir && BACKEND_SKIP_DIRS.has(basename)) return false;
221
+
222
+ if (backend === BACKEND.NODE) {
223
+ // Node project: drop every PHP artifact.
224
+ if (basename.endsWith('.php')) return false;
225
+ if (PHP_ONLY_FILES.has(basename)) return false;
226
+ if (isDir && PHP_ONLY_DIRS.has(basename)) return false;
227
+ return true;
228
+ }
229
+
230
+ // PHP project: drop every Node artifact.
231
+ if (basename.endsWith('.js')) return false;
232
+ if (NODE_ONLY_FILES.has(basename)) return false;
233
+ return true;
234
+ }
235
+
236
+ /**
237
+ * Copy src/backend into the project keeping only the chosen backend's files.
238
+ * Shared files (SQL migrations, .htaccess, web.config, README, ...) are kept
239
+ * for both. Empty directories left behind by filtering are not created.
240
+ */
241
+ function copyBackend(src, dest, backend) {
242
+ const stat = fs.statSync(src);
243
+ if (stat.isDirectory()) {
244
+ for (const child of fs.readdirSync(src)) {
245
+ const childSrc = path.join(src, child);
246
+ const isDir = fs.statSync(childSrc).isDirectory();
247
+ if (!backendEntryKept(child, isDir, backend)) continue;
248
+ copyBackend(childSrc, path.join(dest, child), backend);
249
+ }
250
+ } else {
251
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
252
+ fs.copyFileSync(src, dest);
253
+ }
254
+ }
255
+
188
256
  function slashComment(content, line) {
189
257
  content = content.replace(new RegExp(`^([ \\t]*)// (${escapeRegex(line)})$`, 'gm'), '$1$2');
190
258
  return content.replace(new RegExp(`^([ \\t]*)(${escapeRegex(line)})$`, 'gm'), '$1// $2');
@@ -203,7 +271,7 @@ function njkUncomment(content, line) {
203
271
  return content.split(`{# ${line} #}`).join(line);
204
272
  }
205
273
 
206
- function installDependencies() {
274
+ function installDependencies(backend) {
207
275
  const backendCore = path.join(targetDir, 'src', 'backend', '_core');
208
276
 
209
277
  log(`${color.blue}\n>> Installing Node modules...${color.reset}`);
@@ -220,6 +288,14 @@ function installDependencies() {
220
288
  return false;
221
289
  }
222
290
 
291
+ // Node backend: its runtime deps were added to the ROOT package.json and
292
+ // installed above into the root node_modules — there is no separate install
293
+ // inside src/backend, and Composer is never run.
294
+ if (backend === BACKEND.NODE) {
295
+ return true;
296
+ }
297
+
298
+ // --- PHP backend: install Composer dependencies (if present) ---
223
299
  if (!fs.existsSync(path.join(backendCore, 'composer.json'))) {
224
300
  return true;
225
301
  }
@@ -353,6 +429,7 @@ async function init() {
353
429
 
354
430
  const language = await askChoice('Select a language', LANGUAGE_CHOICES);
355
431
  const framework = await askChoice('Select a CSS framework', FRAMEWORK_CHOICES);
432
+ const backend = await askChoice('Select a backend', BACKEND_CHOICES);
356
433
 
357
434
  log('');
358
435
 
@@ -360,11 +437,19 @@ async function init() {
360
437
  const src = path.join(templateDir, target);
361
438
  const dest = path.join(targetDir, target);
362
439
  if (!fs.existsSync(src)) continue;
363
- const exclude = target === 'src/frontend' ? FRONTEND_EXCLUDE[language] : [];
364
- copyRecursive(src, dest, exclude);
440
+
441
+ if (target === 'src/backend') {
442
+ // Copy only the chosen backend's files (the other backend is omitted).
443
+ copyBackend(src, dest, backend);
444
+ } else {
445
+ const exclude = target === 'src/frontend' ? FRONTEND_EXCLUDE[language] : [];
446
+ copyRecursive(src, dest, exclude);
447
+ }
365
448
  logAdd(target);
366
449
  }
367
450
 
451
+ // Generate the local config only for the chosen backend. The other backend's
452
+ // example file was not copied, so its guard below is simply skipped.
368
453
  const configDest = path.join(targetDir, 'src/backend/config.php');
369
454
  const configExample = path.join(targetDir, 'src/backend/example.config.php');
370
455
  if (!fs.existsSync(configDest) && fs.existsSync(configExample)) {
@@ -372,7 +457,24 @@ async function init() {
372
457
  logAdd('src/backend/config.php');
373
458
  }
374
459
 
460
+ const configJsDest = path.join(targetDir, 'src/backend/config.js');
461
+ const configJsExample = path.join(targetDir, 'src/backend/example.config.js');
462
+ if (!fs.existsSync(configJsDest) && fs.existsSync(configJsExample)) {
463
+ fs.copyFileSync(configJsExample, configJsDest);
464
+ logAdd('src/backend/config.js');
465
+ }
466
+
467
+ // Build the project package.json. Clone the shared dependency maps so we
468
+ // never mutate the PROJECT_PACKAGE constant.
375
469
  const pkg = { ...PROJECT_PACKAGE };
470
+ pkg.dependencies = { ...PROJECT_PACKAGE.dependencies };
471
+ pkg.devDependencies = { ...PROJECT_PACKAGE.devDependencies };
472
+
473
+ // Node backend deps live in the ROOT node_modules — add them to root deps
474
+ // so `npm install` installs them there (never in src/backend).
475
+ if (backend === BACKEND.NODE) {
476
+ pkg.dependencies = { ...pkg.dependencies, ...NODE_BACKEND_DEPENDENCIES };
477
+ }
376
478
 
377
479
  if (language === LANGUAGE.TYPESCRIPT) {
378
480
  const tsSrc = path.join(templateDir, 'tsconfig.json');
@@ -395,7 +497,7 @@ async function init() {
395
497
  applyFramework(framework);
396
498
  applyLanguage(language);
397
499
 
398
- installDependencies();
500
+ installDependencies(backend);
399
501
 
400
502
  log(`\n${color.green}>> Done!${color.reset}`);
401
503
  log(`${color.yellow}\nNow run:\n${color.reset}`);
package/docs/Backend.md CHANGED
@@ -1,46 +1,98 @@
1
1
  # Backend
2
2
 
3
- > **PHP requirement** — the backend runs only on servers with PHP support:
4
- > - **Apache** (shared hosting, VPS) ✅
5
- > - **Nginx** + PHP-FPM (VPS) ✅
6
- > - **IIS** + FastCGI (Windows Server) ✅
7
- > - **Static hosting** (Netlify, Vercel, GitHub Pages, Cloudflare Pages) ❌
8
-
9
- The backend is a PHP REST API located in `src/backend/`, copied to the output directory automatically at build time.
3
+ Nibula ships **two interchangeable backends** in `src/backend/` — a **PHP** one
4
+ and a **Node.js** one. You pick which to use when you scaffold a project
5
+ (`nib new`). Both provide the same REST API: same routing, same `Response`
6
+ envelope, same `X-Api-Key` auth, same CORS model, same file-based rate limiter.
7
+ Only the language differs.
8
+
9
+ > **Server requirement** — where each backend can run:
10
+ >
11
+ > | Host | PHP | Node.js |
12
+ > |---|---|---|
13
+ > | Apache (shared hosting, VPS) | ✅ | ⚠ïļ needs a persistent process (VPS only) |
14
+ > | Nginx + PHP-FPM (VPS) | ✅ | ✅ (Nginx reverse-proxies to Node) |
15
+ > | IIS (Windows Server) | ✅ FastCGI | ✅ via ARR reverse proxy |
16
+ > | Static hosting (Netlify, Vercel, GitHub Pages, Cloudflare Pages) | ❌ | ❌ |
17
+ >
18
+ > Typical shared hosting can run **PHP** but not a long-running Node process, so
19
+ > choose PHP there. On a VPS you control, either works.
20
+
21
+ The backend lives in `src/backend/` and is copied to the output directory
22
+ automatically at build time.
23
+
24
+ ## Choosing a backend
25
+
26
+ When you run `nib new`, the scaffolder asks three questions: language, CSS
27
+ framework, and **backend**. Pick Node.js or PHP.
28
+
29
+ - **If you pick Node**, Composer is never run — you get an npm-only setup, and
30
+ the backend's dependency (`mysql2`, used only if you touch the database) is
31
+ added to the **root** `package.json` and installed into the root `node_modules`.
32
+ - **If you pick PHP**, Composer dependencies are installed as before, and no Node
33
+ backend files are added.
34
+
35
+ **Only the backend you choose is copied into the project.** A Node project
36
+ contains no PHP files; a PHP project contains no `.js` backend files. Shared,
37
+ backend-agnostic files (SQL migrations, `.htaccess`, `web.config`) are always
38
+ included.
10
39
 
11
40
  ## Structure
12
41
 
42
+ The structure below shows both backends for reference; **your project will have
43
+ only one side of each pair** (plus the shared files).
44
+
13
45
  ```
14
46
  src/backend/
15
47
  ├── api/
16
- │ └── protected/ # Endpoints requiring X-Api-Key header
17
48
  │ ├── public/ # Endpoints accessible without an API key
49
+ │ └── protected/ # Endpoints requiring the X-Api-Key header
18
50
  ├── _core/ # Framework internals (routing, modules) — do not edit
19
- │ └── modules/ # Response, RateLimiter, ...
51
+ │ ├── index.php # PHP front controller (PHP project)
52
+ │ ├── index.js # Node front controller + server (Node project)
53
+ │ ├── init.php / init.js
54
+ │ └── modules/ # Response, RateLimiter
20
55
  ├── database/
21
- │ ├── Database.php
22
- │ └── migrations/
23
- ├── example.config.php # Template — versioned, safe to commit
24
- └── config.php # Your local config, generated on setup — never commit this
56
+ │ ├── Database.php # PDO singleton (PHP project)
57
+ │ ├── Database.js # mysql2 pool singleton (Node project)
58
+ │ └── migrations/ # shared
59
+ ├── package.json # Node project only — backend deps + start script
60
+ ├── example.config.php # PHP project only — versioned template
61
+ ├── example.config.js # Node project only — versioned template
62
+ ├── config.php # PHP project only — generated on setup, never commit
63
+ └── config.js # Node project only — generated on setup, never commit
25
64
  ```
26
65
 
27
- > A scaffolded project contains **both** files. `config.php` is generated automatically by `nib new` (a copy of the example) and is git-ignored, so your secrets stay local and are never pushed. `example.config.php` is kept and versioned: it's the file that ends up on GitHub, acting as a secret-free reference so anyone cloning the project knows which keys to set. If `config.php` is ever missing (e.g. after a fresh clone), just copy `example.config.php` to `config.php`.
66
+ > `config.php` / `config.js` are generated automatically by `nib new` (a copy of
67
+ > the matching `example.config.*`) and are git-ignored, so your secrets stay
68
+ > local. The `example.config.*` file is versioned as a secret-free reference. If
69
+ > `config.*` is ever missing (e.g. after a fresh clone), copy the example to it.
28
70
 
29
- > Note: in the boilerplate repo itself, only `example.config.php` exists — `config.php` is created at scaffold time, not shipped.
71
+ > **Where do the Node backend's dependencies live?** In the **root**
72
+ > `node_modules`, not in `src/backend` — so your source tree stays clean (no
73
+ > `src/backend/node_modules` locally). On the server you install them into
74
+ > `out/backend` instead; see `docs/Deploy.md`.
30
75
 
31
- ## Configuration
76
+ ## Key difference: how each backend runs
32
77
 
33
- `config.php` works like a `.env` file — it holds secrets and environment settings that stay local and out of version control.
78
+ - **PHP** is executed **per request** by the web server through PHP-FPM (or
79
+ FastCGI/mod_php). There is no process to keep alive.
80
+ - **Node** is a **long-running process** that *is* the server, listening on
81
+ `127.0.0.1:3000`. The web server (Nginx/Apache/IIS) **reverse-proxies** `/api`
82
+ to it. You start it once and keep it alive (systemd/pm2). See `docs/Deploy.md`.
34
83
 
35
- Fill in your values:
84
+ ## Configuration
85
+
86
+ `config.php` / `config.js` work like a `.env` file — they hold secrets and
87
+ environment settings that stay local and out of version control. Same keys in
88
+ both, just PHP-array vs JS-object syntax.
36
89
 
37
90
  ### config.php <small>(`src/backend/`)</small>
38
91
  ```php
39
- // Default key for protected endpoints that don't have a specific key in CUSTOM_ENDPOINT_KEYS
92
+ // Default key for protected endpoints without a specific key in CUSTOM_ENDPOINT_KEYS
40
93
  'GENERAL_API_KEY' => 'DEFAULT_KEY',
41
94
 
42
- // If you want restrict access to protected endpoints to specific clients, you can define custom keys for each endpoint
43
- // For subfolder endpoints, use the relative path ('subfolder/endpoint')
95
+ // Per-endpoint keys. For subfolder endpoints use the relative path ('subfolder/endpoint')
44
96
  'CUSTOM_ENDPOINT_KEYS' => [
45
97
  'subfolder/endpoint' => 'custom-key',
46
98
  ],
@@ -61,39 +113,100 @@ Fill in your values:
61
113
  'DB_PASS' => '',
62
114
  ```
63
115
 
116
+ ### config.js <small>(`src/backend/`)</small>
117
+ ```js
118
+ module.exports = {
119
+ GENERAL_API_KEY: 'DEFAULT_KEY',
120
+ CUSTOM_ENDPOINT_KEYS: {
121
+ 'subfolder/endpoint': 'custom-key',
122
+ },
123
+ GENERAL_ALLOWED_ORIGINS: ['*' /*, 'https://example.com' */],
124
+ CUSTOM_ENDPOINT_ORIGINS: {
125
+ 'subfolder/endpoint': ['https://app.example.com'],
126
+ },
127
+ DB_HOST: '127.0.0.1',
128
+ DB_NAME: 'example_db',
129
+ DB_USER: 'root',
130
+ DB_PASS: '',
131
+ APP_ENV: 'production', // anything other than 'production' = verbose errors
132
+ };
133
+ ```
134
+
64
135
  ### API keys
65
136
 
66
- `GENERAL_API_KEY` is the fallback key for all protected endpoints. Use `CUSTOM_ENDPOINT_KEYS` to assign a different key to a specific endpoint — for subfolder endpoints, use the relative path as the key.
137
+ `GENERAL_API_KEY` is the fallback key for all protected endpoints. Use
138
+ `CUSTOM_ENDPOINT_KEYS` to assign a different key to a specific endpoint — for
139
+ subfolder endpoints, use the relative path as the key.
67
140
 
68
- > ⚠ïļ The API key travels in the `X-Api-Key` header on every request. Use it only for server-to-server calls over HTTPS. Never embed it in frontend code, where it would be publicly visible.
141
+ > ⚠ïļ The API key travels in the `X-Api-Key` header on every request. Use it only
142
+ > for server-to-server calls over HTTPS. Never embed it in frontend code, where
143
+ > it would be publicly visible.
69
144
 
70
145
  ### CORS (allowed origins)
71
146
 
72
147
  Cross-origin access mirrors the API-key model:
73
148
 
74
- - `GENERAL_ALLOWED_ORIGINS` — the default list of origins allowed to call any endpoint from a browser.
75
- - `CUSTOM_ENDPOINT_ORIGINS` — overrides the default for a specific endpoint, keyed by the same relative path used in `CUSTOM_ENDPOINT_KEYS`.
149
+ - `GENERAL_ALLOWED_ORIGINS` — the default list of origins allowed to call any
150
+ endpoint from a browser.
151
+ - `CUSTOM_ENDPOINT_ORIGINS` — overrides the default for a specific endpoint,
152
+ keyed by the same relative path used in `CUSTOM_ENDPOINT_KEYS`.
76
153
 
77
- Origins must be exact (scheme + host, no trailing slash), e.g. `https://example.com`. When a request's `Origin` is in the resolved list, it is reflected back in `Access-Control-Allow-Origin` (with `Vary: Origin`). An empty list sends no CORS header — the most restrictive setting; same-origin requests still work. Use `'*'` as the only element to allow any origin (not recommended for protected endpoints).
154
+ Origins must be exact (scheme + host, no trailing slash), e.g.
155
+ `https://example.com`. When a request's `Origin` is in the resolved list, it is
156
+ reflected back in `Access-Control-Allow-Origin` (with `Vary: Origin`). An empty
157
+ list sends no CORS header — the most restrictive setting; same-origin requests
158
+ still work. Use `'*'` as the only element to allow any origin (not recommended
159
+ for protected endpoints).
78
160
 
79
- Resolution order for a given endpoint: `CUSTOM_ENDPOINT_ORIGINS[path]` if present, otherwise `GENERAL_ALLOWED_ORIGINS`.
161
+ Resolution order for a given endpoint: `CUSTOM_ENDPOINT_ORIGINS[path]` if
162
+ present, otherwise `GENERAL_ALLOWED_ORIGINS`.
80
163
 
81
164
  ## How routing works
82
165
 
83
- The file path inside `api/` maps directly to the URL. Extra URL segments become route parameters available as `$requestParams[]`.
166
+ The file path inside `api/` maps directly to the URL. The `public`/`protected`
167
+ folder does **not** appear in the URL — it only decides whether the `X-Api-Key`
168
+ check applies. So `api/public/example-public` and `api/protected/example-protected`
169
+ are both reached at `/api/<endpoint>`:
170
+
171
+ - `api/public/example-public` → `/api/example-public` (no key)
172
+ - `api/protected/example-protected` → `/api/example-protected` (requires key)
84
173
 
85
- Every endpoint file has access to:
174
+ Subfolders **do** appear: `api/public/users/list` → `/api/users/list`. Extra URL
175
+ segments become route parameters. Every endpoint has access to:
86
176
 
87
- | Variable | Description |
177
+ | Variable / field | Description |
88
178
  |---|---|
89
- | `$method` | HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) |
90
- | `$requestParams` | Extra URL segments (e.g. `/api/posts/42` → `['42']`) |
179
+ | `method` | HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) |
180
+ | `requestParams` | Extra URL segments (e.g. `/api/posts/42` → `['42']`) |
181
+
182
+ > If two files share the same path — one in `public/`, one in `protected/` — the
183
+ > public one wins (it is checked first), so the endpoint ends up **without**
184
+ > authentication. Don't duplicate names across the two folders.
185
+
186
+ > **About the examples below** — the code samples use **JavaScript (the Node
187
+ > backend)** as the reference. That's just the example language: if you chose the
188
+ > **PHP** backend it works exactly the same way, with the same variables, the
189
+ > same `Response` helper and the same behaviour — only the syntax differs. Each
190
+ > section shows the JS version first, with the equivalent PHP right after.
91
191
 
92
192
  ## Creating a public endpoint
93
193
 
94
- Create a `.php` file anywhere inside `api/public/`
194
+ Create a file anywhere inside `api/public/`.
195
+
196
+ ### `api/public/example.js` (Node — reference)
197
+ ```js
198
+ module.exports = ({ method, requestParams, Response }) => {
199
+ if (method !== 'GET') Response.error('Method not allowed', 405);
200
+ const id = requestParams[0] ? parseInt(requestParams[0], 10) : null;
201
+ Response.success({ id });
202
+ };
203
+ ```
204
+
205
+ The Node handler receives a context object: `method`, `requestParams`,
206
+ `Response` (`.success` / `.error` / `.noContent`), plus `query`, `body` (parsed
207
+ JSON), `rawBody`, `headers`, `config`, `req`, `res`.
95
208
 
96
- ### api/public/example.php
209
+ The same endpoint in PHP — identical logic, PHP syntax:
97
210
  ```php
98
211
  <?php
99
212
  declare(strict_types=1);
@@ -111,9 +224,18 @@ Response::success(['id' => $id]);
111
224
 
112
225
  ## Creating a protected endpoint
113
226
 
114
- Create a `.php` file inside `api/protected/`. The API key check happens automatically before your file runs.
227
+ Create a file inside `api/protected/`. The API-key check happens automatically
228
+ before your file runs.
229
+
230
+ ### `api/protected/example.js` (Node — reference)
231
+ ```js
232
+ module.exports = ({ method, Response }) => {
233
+ if (method !== 'GET') Response.error('Method not allowed', 405);
234
+ Response.success({ visits: 1024 });
235
+ };
236
+ ```
115
237
 
116
- ### api/protected/example.php
238
+ The same endpoint in PHP:
117
239
  ```php
118
240
  <?php
119
241
  declare(strict_types=1);
@@ -127,25 +249,48 @@ if ($method !== 'GET') {
127
249
  Response::success(['visits' => 1024]);
128
250
  ```
129
251
 
130
- To assign a dedicated key, add it to `config.php`:
252
+ To assign a dedicated key, add it to your config:
131
253
 
254
+ ```js
255
+ CUSTOM_ENDPOINT_KEYS: { 'endpoint': 'custom-key' } // config.js
256
+ ```
132
257
  ```php
133
- 'CUSTOM_ENDPOINT_KEYS' => [
134
- 'endpoint' => 'custom-key',
135
- ],
258
+ 'CUSTOM_ENDPOINT_KEYS' => [ 'endpoint' => 'custom-key' ], // config.php
136
259
  ```
137
260
 
138
261
  ## The Response helper
139
262
 
263
+ Identical envelope in both backends — JS shown as reference, PHP works the same:
264
+
265
+ ```js
266
+ Response.success(data, code); // default 200
267
+ Response.error(message, code, details); // default 400
268
+ Response.noContent(); // 204
269
+ ```
140
270
  ```php
141
271
  Response::success($data, $code); // default 200
142
272
  Response::error($message, $code, $details); // default 400
143
- Response::noContent(); // 204
273
+ Response::noContent(); // 204
144
274
  ```
145
275
 
276
+ Response shapes:
277
+ - success → `{ "status": "success", "data": ... }`
278
+ - error → `{ "status": "error", "message": "...", "code": ... }` (+ optional `details`)
279
+
280
+ ## Database
281
+
282
+ Both backends expose a connection singleton reading the same config.
283
+
284
+ - **Node** (reference) — `database/Database.js`, a `mysql2` pool singleton:
285
+ `const pool = Database.getInstance(); const [rows] = await pool.execute(sql, params);`
286
+ The `mysql2` driver is installed at scaffold time when you choose Node (it is
287
+ loaded lazily, so the backend also boots fine without a database).
288
+ - **PHP** — `database/Database.php`, a PDO singleton:
289
+ `$pdo = Database::getInstance();`
290
+
146
291
  ## Pre-built endpoints
147
292
 
148
293
  | Route | Method | Description |
149
294
  |---|---|---|
150
- | `/api/public/example-public` | `GET` | Example endpoint that doesn't require any key |
151
- | `/api/protected/example-protected` | `GET` | Example endpoint that requires X-API-KEY |
295
+ | `/api/example-public` | `GET` | Example endpoint that doesn't require any key |
296
+ | `/api/example-protected` | `GET` | Example endpoint that requires `X-Api-Key` |