nibula 1.2.4 → 1.2.6

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.
Files changed (81) hide show
  1. package/.eleventy.js +81 -81
  2. package/.eleventyignore +3 -3
  3. package/.github/workflows/publish.yml +37 -0
  4. package/CHANGELOG.md +179 -148
  5. package/LICENSE +169 -169
  6. package/NOTICE +4 -4
  7. package/README.md +120 -123
  8. package/bin/create.js +507 -507
  9. package/bin/nibula.js +317 -305
  10. package/docs/Assistant CLI.md +65 -65
  11. package/docs/Backend.md +295 -295
  12. package/docs/Components.md +84 -84
  13. package/docs/Creating pages.md +64 -64
  14. package/docs/Deploy.md +189 -189
  15. package/docs/Head and SEO.md +138 -138
  16. package/docs/Javascript.md +50 -50
  17. package/docs/Styling with SCSS.md +141 -141
  18. package/nginx.conf +75 -75
  19. package/package.json +1 -1
  20. package/src/backend/.htaccess +6 -6
  21. package/src/backend/_core/composer.json +5 -5
  22. package/src/backend/_core/composer.lock +492 -492
  23. package/src/backend/_core/index.js +267 -267
  24. package/src/backend/_core/index.php +147 -147
  25. package/src/backend/_core/init.js +52 -52
  26. package/src/backend/_core/init.php +33 -33
  27. package/src/backend/_core/modules/RateLimiter.js +58 -58
  28. package/src/backend/_core/modules/RateLimiter.php +30 -30
  29. package/src/backend/_core/modules/Response.js +59 -59
  30. package/src/backend/_core/modules/Response.php +48 -48
  31. package/src/backend/api/protected/example-protected.js +23 -23
  32. package/src/backend/api/protected/example-protected.php +16 -16
  33. package/src/backend/api/public/example-public.js +24 -24
  34. package/src/backend/api/public/example-public.php +16 -16
  35. package/src/backend/backend-node.service.example +30 -30
  36. package/src/backend/database/Database.js +46 -46
  37. package/src/backend/database/Database.php +23 -23
  38. package/src/backend/example.config.js +37 -37
  39. package/src/backend/example.config.php +27 -27
  40. package/src/backend/package.json +18 -18
  41. package/src/backend/web.config +16 -16
  42. package/src/frontend/.htaccess +51 -51
  43. package/src/frontend/404.njk +17 -17
  44. package/src/frontend/assets/brand/favicon.svg +54 -54
  45. package/src/frontend/assets/brand/logo.svg +54 -54
  46. package/src/frontend/components/global/footer.njk +25 -25
  47. package/src/frontend/components/global/header.njk +6 -6
  48. package/src/frontend/components/welcome.njk +114 -114
  49. package/src/frontend/data/site.json +48 -48
  50. package/src/frontend/index.njk +8 -8
  51. package/src/frontend/js/modules/exampleModule.js +2 -2
  52. package/src/frontend/js/pages/404.js +6 -6
  53. package/src/frontend/js/pages/homepage.js +6 -6
  54. package/src/frontend/layouts/base.njk +132 -132
  55. package/src/frontend/layouts/page-components.njk +13 -13
  56. package/src/frontend/llms.njk +17 -17
  57. package/src/frontend/robots.njk +7 -7
  58. package/src/frontend/scss/modules/_animations.scss +24 -24
  59. package/src/frontend/scss/modules/_footer.scss +27 -27
  60. package/src/frontend/scss/modules/_global.scss +47 -47
  61. package/src/frontend/scss/modules/_header.scss +27 -27
  62. package/src/frontend/scss/modules/_mobile.scss +29 -29
  63. package/src/frontend/scss/modules/_root.scss +34 -34
  64. package/src/frontend/scss/modules/_typography.scss +14 -14
  65. package/src/frontend/scss/modules/frameworks/_bootstrap.scss +109 -109
  66. package/src/frontend/scss/modules/frameworks/_bulma.scss +108 -108
  67. package/src/frontend/scss/modules/frameworks/_foundation.scss +138 -139
  68. package/src/frontend/scss/modules/frameworks/_uikit.scss +109 -109
  69. package/src/frontend/scss/pages/404.scss +27 -27
  70. package/src/frontend/scss/pages/homepage.scss +22 -22
  71. package/src/frontend/sitemap.njk +17 -17
  72. package/src/frontend/ts/modules/exampleModule.ts +2 -2
  73. package/src/frontend/ts/pages/404.ts +6 -6
  74. package/src/frontend/ts/pages/homepage.ts +6 -6
  75. package/src/frontend/web.config +55 -55
  76. package/tools/config/messages.json +3 -1
  77. package/tools/res/templates/template.js +6 -6
  78. package/tools/res/templates/template.njk +8 -8
  79. package/tools/res/templates/template.scss +22 -22
  80. package/tools/res/templates/template.ts +6 -6
  81. package/tsconfig.json +24 -24
package/docs/Backend.md CHANGED
@@ -1,296 +1,296 @@
1
- # Backend
2
-
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.
39
-
40
- ## Structure
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
-
45
- ```
46
- src/backend/
47
- ├── api/
48
- │ ├── public/ # Endpoints accessible without an API key
49
- │ └── protected/ # Endpoints requiring the X-Api-Key header
50
- ├── _core/ # Framework internals (routing, modules) — do not edit
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
55
- ├── database/
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
64
- ```
65
-
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.
70
-
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`.
75
-
76
- ## Key difference: how each backend runs
77
-
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`.
83
-
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.
89
-
90
- ### config.php <small>(`src/backend/`)</small>
91
- ```php
92
- // Default key for protected endpoints without a specific key in CUSTOM_ENDPOINT_KEYS
93
- 'GENERAL_API_KEY' => 'DEFAULT_KEY',
94
-
95
- // Per-endpoint keys. For subfolder endpoints use the relative path ('subfolder/endpoint')
96
- 'CUSTOM_ENDPOINT_KEYS' => [
97
- 'subfolder/endpoint' => 'custom-key',
98
- ],
99
-
100
- 'GENERAL_ALLOWED_ORIGINS' => [
101
- '*',
102
- // 'https://example.com',
103
- ],
104
-
105
- 'CUSTOM_ENDPOINT_ORIGINS' => [
106
- 'subfolder/endpoint' => ['https://app.example.com'],
107
- ],
108
-
109
- // Database configuration
110
- 'DB_HOST' => '127.0.0.1',
111
- 'DB_NAME' => 'example_db',
112
- 'DB_USER' => 'root',
113
- 'DB_PASS' => '',
114
- ```
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
-
135
- ### API keys
136
-
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.
140
-
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.
144
-
145
- ### CORS (allowed origins)
146
-
147
- Cross-origin access mirrors the API-key model:
148
-
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`.
153
-
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).
160
-
161
- Resolution order for a given endpoint: `CUSTOM_ENDPOINT_ORIGINS[path]` if
162
- present, otherwise `GENERAL_ALLOWED_ORIGINS`.
163
-
164
- ## How routing works
165
-
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)
173
-
174
- Subfolders **do** appear: `api/public/users/list` → `/api/users/list`. Extra URL
175
- segments become route parameters. Every endpoint has access to:
176
-
177
- | Variable / field | Description |
178
- |---|---|
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.
191
-
192
- ## Creating a public endpoint
193
-
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`.
208
-
209
- The same endpoint in PHP — identical logic, PHP syntax:
210
- ```php
211
- <?php
212
- declare(strict_types=1);
213
-
214
- require_once CORE_PATH . '/modules/Response.php';
215
-
216
- if ($method !== 'GET') {
217
- Response::error('Method not allowed', 405);
218
- }
219
-
220
- $id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
221
-
222
- Response::success(['id' => $id]);
223
- ```
224
-
225
- ## Creating a protected endpoint
226
-
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
- ```
237
-
238
- The same endpoint in PHP:
239
- ```php
240
- <?php
241
- declare(strict_types=1);
242
-
243
- require_once CORE_PATH . '/modules/Response.php';
244
-
245
- if ($method !== 'GET') {
246
- Response::error('Method not allowed', 405);
247
- }
248
-
249
- Response::success(['visits' => 1024]);
250
- ```
251
-
252
- To assign a dedicated key, add it to your config:
253
-
254
- ```js
255
- CUSTOM_ENDPOINT_KEYS: { 'endpoint': 'custom-key' } // config.js
256
- ```
257
- ```php
258
- 'CUSTOM_ENDPOINT_KEYS' => [ 'endpoint' => 'custom-key' ], // config.php
259
- ```
260
-
261
- ## The Response helper
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
- ```
270
- ```php
271
- Response::success($data, $code); // default 200
272
- Response::error($message, $code, $details); // default 400
273
- Response::noContent(); // 204
274
- ```
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
-
291
- ## Pre-built endpoints
292
-
293
- | Route | Method | Description |
294
- |---|---|---|
295
- | `/api/example-public` | `GET` | Example endpoint that doesn't require any key |
1
+ # Backend
2
+
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.
39
+
40
+ ## Structure
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
+
45
+ ```
46
+ src/backend/
47
+ ├── api/
48
+ │ ├── public/ # Endpoints accessible without an API key
49
+ │ └── protected/ # Endpoints requiring the X-Api-Key header
50
+ ├── _core/ # Framework internals (routing, modules) — do not edit
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
55
+ ├── database/
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
64
+ ```
65
+
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.
70
+
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`.
75
+
76
+ ## Key difference: how each backend runs
77
+
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`.
83
+
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.
89
+
90
+ ### config.php <small>(`src/backend/`)</small>
91
+ ```php
92
+ // Default key for protected endpoints without a specific key in CUSTOM_ENDPOINT_KEYS
93
+ 'GENERAL_API_KEY' => 'DEFAULT_KEY',
94
+
95
+ // Per-endpoint keys. For subfolder endpoints use the relative path ('subfolder/endpoint')
96
+ 'CUSTOM_ENDPOINT_KEYS' => [
97
+ 'subfolder/endpoint' => 'custom-key',
98
+ ],
99
+
100
+ 'GENERAL_ALLOWED_ORIGINS' => [
101
+ '*',
102
+ // 'https://example.com',
103
+ ],
104
+
105
+ 'CUSTOM_ENDPOINT_ORIGINS' => [
106
+ 'subfolder/endpoint' => ['https://app.example.com'],
107
+ ],
108
+
109
+ // Database configuration
110
+ 'DB_HOST' => '127.0.0.1',
111
+ 'DB_NAME' => 'example_db',
112
+ 'DB_USER' => 'root',
113
+ 'DB_PASS' => '',
114
+ ```
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
+
135
+ ### API keys
136
+
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.
140
+
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.
144
+
145
+ ### CORS (allowed origins)
146
+
147
+ Cross-origin access mirrors the API-key model:
148
+
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`.
153
+
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).
160
+
161
+ Resolution order for a given endpoint: `CUSTOM_ENDPOINT_ORIGINS[path]` if
162
+ present, otherwise `GENERAL_ALLOWED_ORIGINS`.
163
+
164
+ ## How routing works
165
+
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)
173
+
174
+ Subfolders **do** appear: `api/public/users/list` → `/api/users/list`. Extra URL
175
+ segments become route parameters. Every endpoint has access to:
176
+
177
+ | Variable / field | Description |
178
+ |---|---|
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.
191
+
192
+ ## Creating a public endpoint
193
+
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`.
208
+
209
+ The same endpoint in PHP — identical logic, PHP syntax:
210
+ ```php
211
+ <?php
212
+ declare(strict_types=1);
213
+
214
+ require_once CORE_PATH . '/modules/Response.php';
215
+
216
+ if ($method !== 'GET') {
217
+ Response::error('Method not allowed', 405);
218
+ }
219
+
220
+ $id = isset($requestParams[0]) ? (int)$requestParams[0] : null;
221
+
222
+ Response::success(['id' => $id]);
223
+ ```
224
+
225
+ ## Creating a protected endpoint
226
+
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
+ ```
237
+
238
+ The same endpoint in PHP:
239
+ ```php
240
+ <?php
241
+ declare(strict_types=1);
242
+
243
+ require_once CORE_PATH . '/modules/Response.php';
244
+
245
+ if ($method !== 'GET') {
246
+ Response::error('Method not allowed', 405);
247
+ }
248
+
249
+ Response::success(['visits' => 1024]);
250
+ ```
251
+
252
+ To assign a dedicated key, add it to your config:
253
+
254
+ ```js
255
+ CUSTOM_ENDPOINT_KEYS: { 'endpoint': 'custom-key' } // config.js
256
+ ```
257
+ ```php
258
+ 'CUSTOM_ENDPOINT_KEYS' => [ 'endpoint' => 'custom-key' ], // config.php
259
+ ```
260
+
261
+ ## The Response helper
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
+ ```
270
+ ```php
271
+ Response::success($data, $code); // default 200
272
+ Response::error($message, $code, $details); // default 400
273
+ Response::noContent(); // 204
274
+ ```
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
+
291
+ ## Pre-built endpoints
292
+
293
+ | Route | Method | Description |
294
+ |---|---|---|
295
+ | `/api/example-public` | `GET` | Example endpoint that doesn't require any key |
296
296
  | `/api/example-protected` | `GET` | Example endpoint that requires `X-Api-Key` |