chadstart 1.0.0

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 (115) hide show
  1. package/.dockerignore +10 -0
  2. package/.env.example +46 -0
  3. package/.github/workflows/browser-test.yml +34 -0
  4. package/.github/workflows/docker-publish.yml +54 -0
  5. package/.github/workflows/docs.yml +31 -0
  6. package/.github/workflows/npm-chadstart.yml +27 -0
  7. package/.github/workflows/npm-sdk.yml +38 -0
  8. package/.github/workflows/test.yml +85 -0
  9. package/.weblate +9 -0
  10. package/Dockerfile +23 -0
  11. package/README.md +348 -0
  12. package/admin/index.html +2802 -0
  13. package/admin/login.html +207 -0
  14. package/chadstart.example.yml +416 -0
  15. package/chadstart.schema.json +367 -0
  16. package/chadstart.yaml +53 -0
  17. package/cli/cli.js +295 -0
  18. package/core/api-generator.js +606 -0
  19. package/core/auth.js +298 -0
  20. package/core/db.js +384 -0
  21. package/core/entity-engine.js +166 -0
  22. package/core/error-reporter.js +132 -0
  23. package/core/file-storage.js +97 -0
  24. package/core/functions-engine.js +353 -0
  25. package/core/openapi.js +171 -0
  26. package/core/plugin-loader.js +92 -0
  27. package/core/realtime.js +93 -0
  28. package/core/schema-validator.js +50 -0
  29. package/core/seeder.js +231 -0
  30. package/core/telemetry.js +119 -0
  31. package/core/upload.js +372 -0
  32. package/core/workers/php_worker.php +19 -0
  33. package/core/workers/python_worker.py +33 -0
  34. package/core/workers/ruby_worker.rb +21 -0
  35. package/core/yaml-loader.js +64 -0
  36. package/demo/chadstart.yaml +178 -0
  37. package/demo/docker-compose.yml +31 -0
  38. package/demo/functions/greet.go +39 -0
  39. package/demo/functions/hello.cpp +18 -0
  40. package/demo/functions/hello.py +13 -0
  41. package/demo/functions/hello.rb +10 -0
  42. package/demo/functions/onTodoCreated.js +13 -0
  43. package/demo/functions/ping.sh +13 -0
  44. package/demo/functions/stats.js +22 -0
  45. package/demo/public/index.html +522 -0
  46. package/docker-compose.yml +17 -0
  47. package/docs/access-policies.md +155 -0
  48. package/docs/admin-ui.md +29 -0
  49. package/docs/angular.md +69 -0
  50. package/docs/astro.md +71 -0
  51. package/docs/auth.md +160 -0
  52. package/docs/cli.md +56 -0
  53. package/docs/config.md +127 -0
  54. package/docs/crud.md +627 -0
  55. package/docs/deploy.md +113 -0
  56. package/docs/docker.md +59 -0
  57. package/docs/entities.md +385 -0
  58. package/docs/functions.md +196 -0
  59. package/docs/getting-started.md +79 -0
  60. package/docs/groups.md +85 -0
  61. package/docs/index.md +5 -0
  62. package/docs/llm-rules.md +81 -0
  63. package/docs/middlewares.md +78 -0
  64. package/docs/overrides/home.html +350 -0
  65. package/docs/plugins.md +59 -0
  66. package/docs/react.md +75 -0
  67. package/docs/realtime.md +43 -0
  68. package/docs/s3-storage.md +40 -0
  69. package/docs/security.md +23 -0
  70. package/docs/stylesheets/extra.css +375 -0
  71. package/docs/svelte.md +71 -0
  72. package/docs/telemetry.md +97 -0
  73. package/docs/upload.md +168 -0
  74. package/docs/validation.md +115 -0
  75. package/docs/vue.md +86 -0
  76. package/docs/webhooks.md +87 -0
  77. package/index.js +11 -0
  78. package/locales/en/admin.json +169 -0
  79. package/mkdocs.yml +82 -0
  80. package/package.json +65 -0
  81. package/playwright.config.js +24 -0
  82. package/public/.gitkeep +0 -0
  83. package/sdk/README.md +284 -0
  84. package/sdk/package.json +39 -0
  85. package/sdk/scripts/build.js +58 -0
  86. package/sdk/src/index.js +368 -0
  87. package/sdk/test/sdk.test.cjs +340 -0
  88. package/sdk/types/index.d.ts +217 -0
  89. package/server/express-server.js +734 -0
  90. package/test/access-policies.test.js +96 -0
  91. package/test/ai.test.js +81 -0
  92. package/test/api-keys.test.js +361 -0
  93. package/test/auth.test.js +122 -0
  94. package/test/browser/admin-ui.spec.js +127 -0
  95. package/test/browser/global-setup.js +71 -0
  96. package/test/browser/global-teardown.js +11 -0
  97. package/test/db.test.js +227 -0
  98. package/test/entity-engine.test.js +193 -0
  99. package/test/error-reporter.test.js +140 -0
  100. package/test/functions-engine.test.js +240 -0
  101. package/test/groups.test.js +212 -0
  102. package/test/hot-reload.test.js +153 -0
  103. package/test/i18n.test.js +173 -0
  104. package/test/middleware.test.js +76 -0
  105. package/test/openapi.test.js +67 -0
  106. package/test/schema-validator.test.js +83 -0
  107. package/test/sdk.test.js +90 -0
  108. package/test/seeder.test.js +279 -0
  109. package/test/settings.test.js +109 -0
  110. package/test/telemetry.test.js +254 -0
  111. package/test/test.js +17 -0
  112. package/test/upload.test.js +265 -0
  113. package/test/validation.test.js +96 -0
  114. package/test/yaml-loader.test.js +93 -0
  115. package/utils/logger.js +24 -0
@@ -0,0 +1,207 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" id="html-root">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Sign in — ChadStart Admin</title>
7
+ <script src="/admin/vendor/tailwind.js"></script>
8
+ <style type="text/tailwindcss">
9
+ @theme {
10
+ --color-cs-bg: #121212;
11
+ --color-cs-surface: #1e1e1e;
12
+ --color-cs-border: #2a2a2a;
13
+ --color-cs-primary: #34b1eb;
14
+ --color-cs-accent: #03dac6;
15
+ --color-cs-text: #e1e1e1;
16
+ --color-cs-muted: #888888;
17
+ }
18
+ </style>
19
+ <style>
20
+ /* ── Animated glowing border button ────────────────────────────── */
21
+ @property --glow-a { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
22
+ @keyframes spin-glow { to { --glow-a: 360deg; } }
23
+ .btn-glow {
24
+ border: 2px solid transparent;
25
+ background:
26
+ linear-gradient(#1e1e1e, #1e1e1e) padding-box,
27
+ conic-gradient(from var(--glow-a), #34b1eb 0%, #0e7fc4 40%, transparent 60%, #34b1eb 100%) border-box;
28
+ animation: spin-glow 2s linear infinite;
29
+ color: #34b1eb; font-weight: 500;
30
+ transition: opacity 150ms ease;
31
+ }
32
+ .btn-glow:hover { opacity: 0.8; }
33
+ .btn-glow:disabled { opacity: 0.4; animation: none; }
34
+ </style>
35
+ </head>
36
+ <body class="bg-cs-bg text-cs-text min-h-screen flex items-center justify-center p-4"
37
+ style="font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;">
38
+
39
+ <div class="bg-cs-surface border border-cs-border rounded p-8 w-full max-w-sm" style="box-shadow: 0 2px 16px rgba(52,177,235,.1);">
40
+ <div id="login-project-name" class="font-bold text-cs-text text-xl mb-0.5">Admin</div>
41
+ <div class="text-[11px] text-cs-muted mb-6">Powered by ChadStart.com</div>
42
+
43
+ <div id="login-error" class="text-red-400 text-sm mb-4 hidden"></div>
44
+
45
+ <div id="login-collection-section" class="hidden mb-4">
46
+ <label class="block text-sm text-cs-muted mb-1">Collection</label>
47
+ <input type="text" id="login-collection"
48
+ class="w-full bg-cs-bg border border-cs-border rounded text-cs-text px-3 py-2 text-sm outline-none focus:border-cs-primary"
49
+ style="transition: border-color 150ms ease;"
50
+ placeholder="Admin collection name" autocomplete="off" />
51
+ </div>
52
+ <div class="mb-4 text-right">
53
+ <button type="button" id="toggle-collection-btn" onclick="toggleCollectionField()"
54
+ class="text-xs text-cs-muted hover:text-cs-primary cursor-pointer"
55
+ style="transition: color 150ms ease; background: none; border: none; padding: 0;">
56
+ Change collection
57
+ </button>
58
+ </div>
59
+
60
+ <label class="block text-sm text-cs-muted mb-1">Email</label>
61
+ <input type="email" id="login-email" placeholder="admin@example.com" autocomplete="email"
62
+ class="w-full bg-cs-bg border border-cs-border rounded text-cs-text px-3 py-2 text-sm mb-4 outline-none focus:border-cs-primary"
63
+ style="transition: border-color 150ms ease;" />
64
+
65
+ <label class="block text-sm text-cs-muted mb-1">Password</label>
66
+ <input type="password" id="login-password" placeholder="••••••••" autocomplete="current-password"
67
+ class="w-full bg-cs-bg border border-cs-border rounded text-cs-text px-3 py-2 text-sm mb-5 outline-none focus:border-cs-primary"
68
+ style="transition: border-color 150ms ease;" />
69
+
70
+ <button id="login-btn" onclick="doLogin()" class="btn-glow w-full py-2 px-4 rounded text-sm cursor-pointer">
71
+ <span>Sign in</span>
72
+ </button>
73
+ </div>
74
+
75
+ <script>
76
+ // ── State ──────────────────────────────────────────────────────────────
77
+ let schema = null;
78
+
79
+ function toSlug(name) {
80
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
81
+ }
82
+
83
+ function escHtml(s) {
84
+ return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
85
+ }
86
+
87
+ // ── Bootstrap ──────────────────────────────────────────────────────────
88
+ async function init() {
89
+ // If already authenticated, redirect to admin
90
+ const token = localStorage.getItem('cs_token');
91
+ if (token) {
92
+ // Verify token is still valid before redirect
93
+ try {
94
+ const verifySchema = await fetch('/admin/schema').then(r => r.json());
95
+ const cols = getAdminCollections(verifySchema);
96
+ if (cols.length) {
97
+ const r = await fetch(`/api/auth/${toSlug(cols[0].name)}/me`, {
98
+ headers: { Authorization: `Bearer ${token}` }
99
+ });
100
+ if (r.ok) {
101
+ window.location.replace('/admin');
102
+ return;
103
+ }
104
+ }
105
+ } catch { /* fall through to show login */ }
106
+ localStorage.removeItem('cs_token');
107
+ }
108
+
109
+ // Load schema for collection selection
110
+ try {
111
+ schema = await fetch('/admin/schema').then(r => r.json());
112
+ if (schema.name) {
113
+ document.getElementById('login-project-name').textContent = schema.name;
114
+ document.title = `Sign in — ${escHtml(schema.name)}`;
115
+ }
116
+ populateLoginCollections();
117
+ } catch { /* ignore */ }
118
+ }
119
+
120
+ function getAdminCollections(s) {
121
+ const src = s || schema;
122
+ return (src && src.userCollections) || [];
123
+ }
124
+
125
+ function populateLoginCollections() {
126
+ const cols = getAdminCollections();
127
+ const input = document.getElementById('login-collection');
128
+ const errEl = document.getElementById('login-error');
129
+ const btn = document.getElementById('login-btn');
130
+ if (cols.length) {
131
+ input.value = cols[0].name;
132
+ } else {
133
+ input.value = '';
134
+ errEl.textContent = 'No admin collections found. Please configure an authenticable entity.';
135
+ errEl.classList.remove('hidden');
136
+ btn.disabled = true;
137
+ }
138
+ }
139
+
140
+ function toggleCollectionField() {
141
+ const section = document.getElementById('login-collection-section');
142
+ const btn = document.getElementById('toggle-collection-btn');
143
+ const isHidden = section.classList.contains('hidden');
144
+ section.classList.toggle('hidden', !isHidden);
145
+ btn.textContent = isHidden ? 'Hide collection' : 'Change collection';
146
+ if (isHidden) document.getElementById('login-collection').focus();
147
+ }
148
+
149
+ async function doLogin() {
150
+ const collectionName = document.getElementById('login-collection').value.trim().replace(/[^a-zA-Z0-9 _-]/g, '');
151
+ const email = document.getElementById('login-email').value.trim();
152
+ const password = document.getElementById('login-password').value;
153
+ const errEl = document.getElementById('login-error');
154
+ const btn = document.getElementById('login-btn');
155
+ const btnText = btn.querySelector('span');
156
+
157
+ errEl.classList.add('hidden');
158
+ if (!collectionName || !email || !password) {
159
+ errEl.textContent = 'All fields are required';
160
+ errEl.classList.remove('hidden');
161
+ return;
162
+ }
163
+
164
+ btn.disabled = true;
165
+ btnText.textContent = 'Signing in…';
166
+
167
+ const cols = getAdminCollections();
168
+ const col = cols.find(c => c.name === collectionName);
169
+ if (!col) {
170
+ showLoginError('Collection not found');
171
+ return;
172
+ }
173
+
174
+ try {
175
+ const r = await fetch(`/api/auth/${toSlug(col.name)}/login`, {
176
+ method: 'POST',
177
+ headers: { 'Content-Type': 'application/json' },
178
+ body: JSON.stringify({ email, password }),
179
+ });
180
+ const data = await r.json();
181
+ if (!r.ok) {
182
+ showLoginError(data.error || 'Login failed');
183
+ return;
184
+ }
185
+ localStorage.setItem('cs_token', data.token);
186
+ localStorage.setItem('cs_collection', col.name);
187
+ window.location.replace('/admin');
188
+ } catch {
189
+ showLoginError('Network error');
190
+ }
191
+
192
+ function showLoginError(msg) {
193
+ errEl.textContent = msg;
194
+ errEl.classList.remove('hidden');
195
+ btn.disabled = false;
196
+ btnText.textContent = 'Sign in';
197
+ }
198
+ }
199
+
200
+ document.addEventListener('keydown', e => {
201
+ if (e.key === 'Enter') doLogin();
202
+ });
203
+
204
+ init();
205
+ </script>
206
+ </body>
207
+ </html>
@@ -0,0 +1,416 @@
1
+ # chadstart.example.yml
2
+ # ─────────────────────────────────────────────────────────────────────────────
3
+ # Reference file that lists every available option in a ChadStart YAML config.
4
+ # Copy this file to chadstart.yaml and remove / adjust the sections you need.
5
+ # Secrets (JWT_SECRET, DB passwords, SENTRY_DSN, OTLP auth headers …) must
6
+ # always be stored in .env — never put them in this file.
7
+ # ─────────────────────────────────────────────────────────────────────────────
8
+
9
+ # ── App identity ──────────────────────────────────────────────────────────────
10
+
11
+ # (required) Human-readable name of your application.
12
+ name: My App
13
+
14
+ # TCP port the HTTP server listens on. Overridable by PORT env var.
15
+ # Default: 3000
16
+ port: 3000
17
+
18
+ # Path to the SQLite database file (relative to this config file).
19
+ # Overridable by DB_PATH env var.
20
+ # Default: data/chadstart.db
21
+ database: data/chadstart.db
22
+
23
+ # ── Entities ──────────────────────────────────────────────────────────────────
24
+ # Define every data model here. ChadStart auto-generates REST + SDK CRUD for
25
+ # each entity. Every entity gets id (UUID), createdAt, and updatedAt for free.
26
+
27
+ entities:
28
+
29
+ # ── Authenticable (user-like) entity ────────────────────────────────────────
30
+ User:
31
+ # Makes this entity a user: adds email + password fields and enables
32
+ # login / signup / token-refresh endpoints.
33
+ authenticable: true
34
+
35
+ # Identifier property shown in the admin panel (defaults to first string field).
36
+ mainProp: name
37
+
38
+ # Overrides for the auto-generated singular / plural display names.
39
+ nameSingular: user
40
+ namePlural: users
41
+
42
+ # Number of records created when running `npm run seed`. Default: 50
43
+ seedCount: 10
44
+
45
+ # Custom kebab-case slug used in API paths (default: pluralised entity name).
46
+ slug: users
47
+
48
+ properties:
49
+ # Short syntax — type defaults to "string".
50
+ - name
51
+
52
+ # Long syntax — all available property options shown below.
53
+ - name: bio
54
+ type: text # string | text | richText | number | link | money |
55
+ # date | timestamp | email | boolean | file | image |
56
+ # password | choice | location | group | json |
57
+ # integer | float | real
58
+ hidden: false # Exclude this field from API responses. Default: false
59
+ helpText: A short bio # Hint displayed in the admin UI
60
+ default: '' # Default value when property is omitted on create
61
+
62
+ - name: email
63
+ type: email
64
+ validation:
65
+ required: true
66
+ isEmail: true
67
+
68
+ - name: age
69
+ type: integer
70
+ validation:
71
+ min: 0
72
+ max: 120
73
+
74
+ - name: website
75
+ type: link
76
+
77
+ - name: balance
78
+ type: money
79
+ options:
80
+ currency: USD # ISO 4217 currency code. Default: USD
81
+
82
+ - name: avatar
83
+ type: image
84
+ options:
85
+ sizes:
86
+ thumbnail:
87
+ width: 80
88
+ height: 80
89
+ fit: cover # cover | contain | fill | inside | outside (Sharp)
90
+ medium:
91
+ width: 400
92
+
93
+ - name: resume
94
+ type: file
95
+
96
+ - name: password
97
+ type: password # Value is automatically hashed before storage
98
+
99
+ - name: role
100
+ type: choice
101
+ options:
102
+ values: [admin, editor, viewer]
103
+ sequential: false # true = values follow a logical progression
104
+
105
+ - name: coordinates
106
+ type: location # Stored as { lat, lng }
107
+
108
+ - name: metadata
109
+ type: json
110
+
111
+ - name: publishedAt
112
+ type: timestamp # ISO 8601 datetime
113
+
114
+ - name: birthDate
115
+ type: date
116
+
117
+ - name: isActive
118
+ type: boolean
119
+ default: true
120
+
121
+ # ── Access policies ────────────────────────────────────────────────────────
122
+ # Each CRUD operation can have one or more policy rules evaluated in order.
123
+ # access values: public | restricted | admin | forbidden
124
+ # Emoji shortcuts: 🌐 = public | 🔒 = restricted | 👨🏻‍💻 = admin | 🚫 = forbidden
125
+ policies:
126
+ create:
127
+ - access: public # Anyone can sign up
128
+ read:
129
+ - access: restricted # Must be authenticated
130
+ allow: User # …and belong to the User entity
131
+ condition: self # …and only read their own record
132
+ update:
133
+ - access: restricted
134
+ allow: User
135
+ condition: self
136
+ delete:
137
+ - access: admin # Only admins
138
+ # signup policy controls who can call the signup endpoint (authenticable only)
139
+ signup:
140
+ - access: public
141
+
142
+ # ── Entity-level validation ────────────────────────────────────────────────
143
+ # Validation rules applied per property on create / update requests.
144
+ # All available validation rules are listed here (use only what you need).
145
+ validation:
146
+ name:
147
+ required: true # Field must be present in the request
148
+ isNotEmpty: true # Value must not be an empty string
149
+ minLength: 2
150
+ maxLength: 100
151
+ email:
152
+ isDefined: true # Value must not be undefined
153
+ isEmail: true
154
+ age:
155
+ isOptional: true
156
+ min: 0
157
+ max: 120
158
+ role:
159
+ isIn: [admin, editor, viewer]
160
+ bio:
161
+ isOptional: true
162
+ maxLength: 500
163
+ # Additional validators (not all will apply to every type):
164
+ # equals: someValue
165
+ # notEquals: someValue
166
+ # isEmpty: false
167
+ # isNotIn: [banned, blocked]
168
+ # contains: '@'
169
+ # notContains: spam
170
+ # isAlpha: true
171
+ # isAlphanumeric: true
172
+ # isAscii: true
173
+ # isJSON: false
174
+ # matches: '^[a-z]+$'
175
+ # isMimeType: true
176
+
177
+ # ── Webhooks (lifecycle hooks) ─────────────────────────────────────────────
178
+ # Send HTTP requests to external services on entity lifecycle events.
179
+ hooks:
180
+ afterCreate:
181
+ - url: https://hooks.example.com/user-created
182
+ method: POST # GET | POST | PUT | PATCH | DELETE. Default: POST
183
+ headers:
184
+ X-Api-Key: my-key
185
+ afterUpdate:
186
+ - url: https://hooks.example.com/user-updated
187
+ method: POST
188
+ beforeDelete:
189
+ - url: https://hooks.example.com/before-user-deleted
190
+ method: POST
191
+ # Other available events: beforeCreate | beforeUpdate | afterDelete
192
+
193
+ # ── Middlewares ────────────────────────────────────────────────────────────
194
+ # Run a custom function at specific lifecycle points.
195
+ # Handler files live in the /functions (or CHADSTART_FUNCTIONS_FOLDER) folder.
196
+ middlewares:
197
+ beforeCreate:
198
+ - function: hashApiKey.js
199
+ afterCreate:
200
+ - function: sendWelcomeEmail.js
201
+ # Other available events: beforeUpdate | afterUpdate | beforeDelete | afterDelete
202
+
203
+ # ── Standard collection entity ────────────────────────────────────────────
204
+ Post:
205
+ mainProp: title
206
+ properties:
207
+ - title
208
+ - { name: content, type: richText }
209
+ - { name: publishedAt, type: timestamp }
210
+ - { name: coverImage, type: image, options: { sizes: { large: { width: 1200 } } } }
211
+
212
+ # ── Relations ───────────────────────────────────────────────────────────
213
+ # belongsTo → many-to-one (adds a foreign key on this entity)
214
+ # belongsToMany → many-to-many (uses a join table)
215
+
216
+ # Short syntax: just the related entity name.
217
+ belongsTo:
218
+ - User
219
+
220
+ # Long syntax: rename the relation and/or add extra options.
221
+ # belongsTo:
222
+ # - name: author
223
+ # entity: User
224
+ # helpText: The author of this post
225
+ # eager: false # true = automatically load this relation in all queries
226
+
227
+ belongsToMany:
228
+ - Tag
229
+
230
+ policies:
231
+ create:
232
+ - { access: restricted, allow: User }
233
+ read:
234
+ - access: public
235
+ update:
236
+ - { access: restricted, allow: User, condition: self }
237
+ delete:
238
+ - access: admin
239
+
240
+ Tag:
241
+ properties:
242
+ - name
243
+ policies:
244
+ create:
245
+ - access: admin
246
+ read:
247
+ - access: public
248
+ update:
249
+ - access: admin
250
+ delete:
251
+ - access: admin
252
+
253
+ # ── Single entity ──────────────────────────────────────────────────────────
254
+ # A single entity has exactly one record — Create and Delete are disabled.
255
+ # Useful for site settings, homepage content, etc.
256
+ SiteSettings:
257
+ single: true
258
+ slug: site-settings
259
+ properties:
260
+ - { name: siteName, type: string }
261
+ - { name: tagline, type: text }
262
+ - { name: logo, type: image }
263
+ validation:
264
+ siteName: { required: true }
265
+
266
+ # ── Reusable property groups ──────────────────────────────────────────────────
267
+ # Groups are sets of properties you can embed in multiple entities using
268
+ # type: group. They are stored as JSON in the parent entity's row.
269
+
270
+ groups:
271
+ Address:
272
+ properties:
273
+ - { name: street, type: string }
274
+ - { name: city, type: string }
275
+ - { name: zipCode, type: string }
276
+ - { name: country, type: string }
277
+ validation:
278
+ city: { isNotEmpty: true }
279
+
280
+ # Usage in an entity property (multiple items by default):
281
+ # - name: addresses
282
+ # type: group
283
+ # options:
284
+ # group: Address # Name of the group defined above
285
+ # multiple: true # false = single nested object; Default: true
286
+
287
+ # ── Custom functions ──────────────────────────────────────────────────────────
288
+ # Add your own functions — each can have multiple triggers (HTTP, cron, event).
289
+ # Runtime is optional; defaults to 'js'. Files live in /functions (or CHADSTART_FUNCTIONS_FOLDER).
290
+ #
291
+ # Supported runtimes: js | bash | python | go | c++ | ruby | php
292
+ # Supported triggers: http | cron | event
293
+ #
294
+ # Predefined cron aliases: @yearly @annually @monthly @weekly @daily @midnight @hourly
295
+ # HTTP policies: public (default) | restricted | admin | forbidden
296
+
297
+ functions:
298
+ hello:
299
+ runtime: js # js (default) | bash | python | go | c++ | ruby | php
300
+ function: hello.js # path relative to CHADSTART_FUNCTIONS_FOLDER (default: functions/)
301
+ triggers:
302
+ - type: http
303
+ method: GET
304
+ path: /hello
305
+ # policies omitted → public (anyone can call)
306
+ - type: event
307
+ name: user.created # Emitted by middleware/other functions via eventBus
308
+ - type: cron
309
+ schedule: "*/10 * * * *" # Every 10 minutes
310
+ - type: cron
311
+ schedule: "@daily" # Predefined alias (resolves to "0 0 * * *")
312
+
313
+ sendPasswordReset:
314
+ function: sendPasswordReset.js
315
+ description: Sends a password-reset email. Open to anyone.
316
+ triggers:
317
+ - type: http
318
+ method: POST
319
+ path: /auth/password-reset
320
+ policies:
321
+ - access: public
322
+
323
+ adminStats:
324
+ function: adminStats.js
325
+ description: Usage statistics. Requires any valid auth token.
326
+ triggers:
327
+ - type: http
328
+ method: GET
329
+ path: /admin/stats
330
+ policies:
331
+ - access: admin
332
+
333
+ myProfile:
334
+ function: myProfile.js
335
+ description: User-specific data. Only Customer tokens may access it.
336
+ triggers:
337
+ - type: http
338
+ method: GET
339
+ path: /me
340
+ policies:
341
+ - access: restricted
342
+ allow: Customer
343
+
344
+ # ── File buckets ──────────────────────────────────────────────────────────────
345
+ # Named storage locations for uploaded files.
346
+
347
+ files:
348
+ uploads:
349
+ path: ./uploads # Local directory (relative to this config file)
350
+ public: true # Serve files at /files/uploads/*. Default: false
351
+
352
+ privateDocuments:
353
+ path: ./private-docs
354
+ public: false
355
+
356
+ # ── Static public folder ──────────────────────────────────────────────────────
357
+ # Serve a folder of static assets (HTML, CSS, images, …) from the root path.
358
+
359
+ public:
360
+ folder: ./public
361
+
362
+ # ── Plugins ───────────────────────────────────────────────────────────────────
363
+ # Extend the server with additional Express routes and logic.
364
+ # ⚠ Remote plugins execute arbitrary code — only load plugins from trusted sources.
365
+
366
+ plugins:
367
+ - repo: https://github.com/org/chadstart-plugin-example # Remote GitHub repo
368
+ - path: ./my-local-plugin # Local directory
369
+ name: my-local-plugin # Optional display name
370
+
371
+ # ── Admin UI & Admin Entity ───────────────────────────────────────────────────
372
+ # Control the built-in Admin UI and Admin entity (superuser access).
373
+ #
374
+ # By default:
375
+ # - An Admin entity is auto-created with email + password (authenticable).
376
+ # - The Admin UI is available at /admin (login page at /login).
377
+ # - The admin UI requires admin-level access.
378
+ #
379
+ # If you define entities.Admin in your YAML, it will be merged with the default.
380
+
381
+ admin:
382
+ enable_app: true # Serve the Admin UI at /admin. Default: true
383
+ enable_entity: true # Create the built-in Admin entity. Default: true
384
+ policies:
385
+ - access: admin # Who can access /admin. Default: access: admin
386
+
387
+ # ── Rate limiting ─────────────────────────────────────────────────────────────
388
+ # Protect your API from abuse.
389
+
390
+ rateLimits:
391
+ - name: default
392
+ limit: 100 # Max requests allowed in the time window
393
+ ttl: 60000 # Time window in milliseconds (60 s here)
394
+ - name: strict
395
+ limit: 10
396
+ ttl: 60000
397
+
398
+ # ── OpenTelemetry tracing ──────────────────────────────────────────────────────
399
+ # Non-secret values can be set here; secret values (auth headers / API keys)
400
+ # must be set via environment variables only — see .env.example.
401
+
402
+ telemetry:
403
+ enabled: false # Also configurable via OTEL_ENABLED env var
404
+ serviceName: my-app # Overridable via OTEL_SERVICE_NAME env var
405
+ endpoint: http://localhost:4318 # Overridable via OTEL_EXPORTER_OTLP_ENDPOINT env var
406
+ # Auth headers (e.g., "authorization=Bearer <token>") must be set via
407
+ # OTEL_EXPORTER_OTLP_HEADERS env var — never place secrets in this file.
408
+
409
+ # ── Error reporting (Sentry / Bugsink) ────────────────────────────────────────
410
+ # Set SENTRY_DSN in your .env to enable. The DSN is a secret — never put it here.
411
+ # Bugsink (https://www.bugsink.com) is a self-hosted Sentry-compatible alternative.
412
+
413
+ sentry:
414
+ environment: production # Label sent to Sentry. Defaults to NODE_ENV.
415
+ tracesSampleRate: 1.0 # Fraction of transactions to sample (0.0–1.0)
416
+ debug: false # Enable Sentry SDK debug logging