create-volt 0.55.1 → 0.56.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 (43) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/index.js +1 -1
  3. package/package.json +1 -1
  4. package/templates/blog/public/volt.js +7 -2
  5. package/templates/blog/server.js +87 -3
  6. package/templates/blog/setup/setup.js +59 -10
  7. package/templates/business/Dockerfile +20 -0
  8. package/templates/business/Procfile +1 -0
  9. package/templates/business/README.md +25 -0
  10. package/templates/business/dockerignore +6 -0
  11. package/templates/business/ecosystem.config.cjs +5 -0
  12. package/templates/business/env +2 -0
  13. package/templates/business/fly.toml +15 -0
  14. package/templates/business/gitignore +5 -0
  15. package/templates/business/package.json +21 -0
  16. package/templates/business/pages/_theme.js +65 -0
  17. package/templates/business/pages/about.md +30 -0
  18. package/templates/business/pages/contact.md +23 -0
  19. package/templates/business/pages/index.md +41 -0
  20. package/templates/business/pages/products.md +27 -0
  21. package/templates/business/public/app.js +89 -0
  22. package/templates/business/public/favicon.webp +0 -0
  23. package/templates/business/public/logo.webp +0 -0
  24. package/templates/business/public/volt-ssr.js +63 -0
  25. package/templates/business/public/volt.js +355 -0
  26. package/templates/business/render.yaml +15 -0
  27. package/templates/business/server.js +1051 -0
  28. package/templates/business/setup/index.html +46 -0
  29. package/templates/business/setup/logs.html +29 -0
  30. package/templates/business/setup/logs.js +58 -0
  31. package/templates/business/setup/setup.js +509 -0
  32. package/templates/business/setup/studio.html +29 -0
  33. package/templates/business/views/index.html +42 -0
  34. package/templates/default/public/volt.js +7 -2
  35. package/templates/default/server.js +87 -3
  36. package/templates/default/setup/setup.js +59 -10
  37. package/templates/docs/public/volt.js +7 -2
  38. package/templates/docs/server.js +87 -3
  39. package/templates/docs/setup/setup.js +59 -10
  40. package/templates/guestbook/public/volt.js +7 -2
  41. package/templates/starter/public/volt.js +7 -2
  42. package/templates/starter/server.js +86 -3
  43. package/templates/starter/setup/setup.js +59 -10
@@ -0,0 +1,89 @@
1
+ // app.js — demo app. Shows BOTH authoring styles on one shared signal engine,
2
+ // with live hot reload. Edit anything here (or in index.html) and save: the
3
+ // dev server pushes a reload over Socket.io and the page refreshes.
4
+
5
+ import { signal, computed, el, html, mount } from "/volt.js";
6
+
7
+ // --- Counter, written with el() helpers (imperative, zero template parsing) ---
8
+ function Counter() {
9
+ const n = signal(0);
10
+ return el("div", { class: "card-x p-4 mb-4" },
11
+ el("h2", { class: "h5 mb-3" }, "Counter — built with el()"),
12
+ el("div", { class: "d-flex align-items-center gap-3" },
13
+ el("button", { class: "btn btn-outline-secondary", onClick: () => n(n() - 1) }, "−"),
14
+ el("span", { class: "fs-4 fw-bold", style: "min-width:3ch;text-align:center" },
15
+ () => String(n())), // function-child: only this text node updates
16
+ el("button", { class: "btn btn-primary", onClick: () => n(n() + 1) }, "+"),
17
+ el("span", { class: "text-muted ms-2" },
18
+ () => (n() % 2 === 0 ? "even" : "odd")),
19
+ ),
20
+ );
21
+ }
22
+
23
+ // --- Todo list, written with html`` templates (markup-first) ---
24
+ function Todos() {
25
+ const items = signal([]); // [{ id, text, done }]
26
+ const draft = signal("");
27
+ const remaining = computed(() => items().filter((t) => !t.done).length);
28
+
29
+ const add = () => {
30
+ const text = draft().trim();
31
+ if (!text) return;
32
+ items([...items(), { id: Date.now() + Math.random(), text, done: false }]);
33
+ draft("");
34
+ };
35
+ const toggle = (id) =>
36
+ items(items().map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
37
+ const remove = (id) => items(items().filter((t) => t.id !== id));
38
+
39
+ const row = (t) => html`
40
+ <li class="list-group-item d-flex align-items-center gap-2">
41
+ <input class="form-check-input mt-0" type="checkbox"
42
+ checked=${t.done} onchange=${() => toggle(t.id)} />
43
+ <span class=${() => "flex-grow-1 " + (t.done ? "text-decoration-line-through text-muted" : "")}>
44
+ ${t.text}
45
+ </span>
46
+ <button class="btn btn-sm btn-outline-danger" onclick=${() => remove(t.id)}>✕</button>
47
+ </li>`;
48
+
49
+ return html`
50
+ <div class="card-x p-4">
51
+ <h2 class="h5 mb-3">Todos — built with html\`\`</h2>
52
+ <div class="input-group mb-3">
53
+ <input class="form-control" placeholder="Add a task…"
54
+ value=${draft}
55
+ oninput=${(e) => draft(e.target.value)}
56
+ onkeydown=${(e) => e.key === "Enter" && add()} />
57
+ <button class="btn btn-primary" onclick=${add}>Add</button>
58
+ </div>
59
+ <ul class="list-group mb-2">
60
+ ${() => items().map(row)}
61
+ </ul>
62
+ <small class="text-muted">${remaining} remaining</small>
63
+ </div>`;
64
+ }
65
+
66
+ // Mount the demo, plus the UI for any enabled add-ons (auth, realtime, …).
67
+ // Add-ons serve their own /…-ui.js when turned on in the setup wizard.
68
+ const nodes = [Counter(), Todos()];
69
+ let enabled = [];
70
+ try {
71
+ enabled = await (await fetch("/__volt/addons")).json();
72
+ } catch {
73
+ /* older app without the endpoint — just the demo */
74
+ }
75
+ if (enabled.includes("auth")) {
76
+ try {
77
+ nodes.unshift((await import("/auth-ui.js")).authPanel());
78
+ } catch {
79
+ /* auth UI unavailable */
80
+ }
81
+ }
82
+ if (enabled.includes("realtime")) {
83
+ try {
84
+ nodes.push((await import("/chat-ui.js")).chatPanel());
85
+ } catch {
86
+ /* realtime UI unavailable */
87
+ }
88
+ }
89
+ mount("#app", ...nodes);
@@ -0,0 +1,63 @@
1
+ // volt-ssr.js — server-side rendering for Volt. Renders the same html`` markup,
2
+ // h() elements, and signal values to an HTML string in Node (no DOM), so a Volt
3
+ // app can be fully server-rendered for SEO and hydrate interactive islands with
4
+ // volt.js on the client.
5
+ //
6
+ // import { html, h, raw, renderToString } from "./volt-ssr.js";
7
+ // renderToString(html`<p>${name}</p>`) // → "<p>Ada</p>" (name is escaped)
8
+ //
9
+ // Authoring matches the client: html`` interpolations render as escaped text;
10
+ // nest html``/h() nodes for structure; use raw() for trusted pre-built HTML.
11
+
12
+ const VOID = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
13
+ const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
14
+
15
+ // trusted, pre-rendered HTML — emitted verbatim. Use only for content you control.
16
+ export const raw = (s) => ({ __raw: String(s) });
17
+
18
+ // tagged-template markup; the literal chunks are trusted, ${values} are escaped.
19
+ export const html = (strings, ...values) => ({ __tpl: true, strings, values });
20
+
21
+ const isNode = (x) => x && typeof x === "object" && (x.__tpl || x.__raw || x.__el);
22
+
23
+ // hyperscript element: h(tag, props?, ...children)
24
+ export function h(tag, props, ...children) {
25
+ if (props === undefined || props === null || isNode(props) || Array.isArray(props) || typeof props !== "object") {
26
+ if (props !== undefined && props !== null) children.unshift(props);
27
+ props = {};
28
+ }
29
+ return { __el: true, tag, props, children };
30
+ }
31
+
32
+ const read = (v) => (typeof v === "function" ? v() : v); // resolve signals/thunks (once)
33
+
34
+ function attrs(props) {
35
+ let out = "";
36
+ for (const [k, rawVal] of Object.entries(props)) {
37
+ if (k === "children" || k.startsWith("on")) continue; // event handlers don't SSR
38
+ const v = read(rawVal);
39
+ if (v == null || v === false) continue;
40
+ const name = k === "className" ? "class" : k;
41
+ out += v === true ? ` ${name}` : ` ${name}="${esc(v)}"`;
42
+ }
43
+ return out;
44
+ }
45
+
46
+ export function renderToString(node) {
47
+ const v = read(node);
48
+ if (v == null || v === false || v === true) return "";
49
+ if (typeof v === "string" || typeof v === "number") return esc(v);
50
+ if (v.__raw != null) return v.__raw;
51
+ if (Array.isArray(v)) return v.map(renderToString).join("");
52
+ if (v.__tpl) {
53
+ let out = v.strings[0];
54
+ for (let i = 0; i < v.values.length; i++) out += renderToString(v.values[i]) + v.strings[i + 1];
55
+ return out;
56
+ }
57
+ if (v.__el) {
58
+ if (typeof v.tag === "function") return renderToString(v.tag({ ...v.props, children: v.children }));
59
+ const open = `<${v.tag}${attrs(v.props)}>`;
60
+ return VOID.has(v.tag) ? open : `${open}${v.children.map(renderToString).join("")}</${v.tag}>`;
61
+ }
62
+ return esc(String(v));
63
+ }
@@ -0,0 +1,355 @@
1
+ // volt.js — a tiny, no-build, signals-based UI library.
2
+ //
3
+ // Not React: there is no JSX, no virtual DOM, and no "re-render the whole
4
+ // component" step. State lives in *signals*; reading a signal inside a piece of
5
+ // UI subscribes that exact piece; writing the signal re-runs only those
6
+ // subscribers and touches only the precise text node / attribute that changed.
7
+ //
8
+ // Two ways to author UI, same engine underneath:
9
+ // 1. html`` — tagged-template markup with ${signal} holes
10
+ // 2. el(...) — imperative DOM helpers with function-children
11
+ // They interoperate freely (drop an el() node into an html`` template, etc.).
12
+ //
13
+ // Public API: signal, computed, effect, el, html, mount.
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Reactive core (signals + effects with ownership-based disposal)
17
+ // ---------------------------------------------------------------------------
18
+
19
+ let activeEffect = null;
20
+
21
+ // A signal is a function: call with no args to read, one arg to write.
22
+ // const n = signal(0); n(); // read → 0
23
+ // n(n()+1); // write → notifies subscribers
24
+ export function signal(value) {
25
+ const subs = new Set();
26
+ return function sig(...args) {
27
+ if (args.length) {
28
+ const next = args[0];
29
+ if (next === value) return value; // no-op on identical value
30
+ value = next;
31
+ for (const eff of [...subs]) eff.run(); // copy: run() mutates subs
32
+ return value;
33
+ }
34
+ if (activeEffect) {
35
+ subs.add(activeEffect);
36
+ activeEffect.deps.add(subs);
37
+ }
38
+ return value;
39
+ };
40
+ }
41
+
42
+ // effect(fn) runs fn now, tracks every signal it reads, and re-runs it whenever
43
+ // any of those change. Effects created *inside* another effect are owned by it
44
+ // and disposed before each re-run — so dynamic regions clean up after themselves.
45
+ export function effect(fn) {
46
+ const eff = {
47
+ deps: new Set(),
48
+ children: new Set(),
49
+ parent: activeEffect,
50
+ disposed: false,
51
+ run() {
52
+ // A signal write notifies a *snapshot* of subscribers; a parent re-render
53
+ // can dispose this effect before its turn in that snapshot — so skip if so.
54
+ if (eff.disposed) return;
55
+ disposeChildren(eff);
56
+ cleanupDeps(eff);
57
+ const prev = activeEffect;
58
+ activeEffect = eff;
59
+ try {
60
+ fn();
61
+ } finally {
62
+ activeEffect = prev;
63
+ }
64
+ },
65
+ dispose() {
66
+ eff.disposed = true;
67
+ disposeChildren(eff);
68
+ cleanupDeps(eff);
69
+ if (eff.parent) eff.parent.children.delete(eff);
70
+ },
71
+ };
72
+ if (activeEffect) activeEffect.children.add(eff);
73
+ eff.run();
74
+ return () => eff.dispose();
75
+ }
76
+
77
+ // computed(fn) is a read-only derived signal: () => value, auto-updating.
78
+ export function computed(fn) {
79
+ const s = signal(undefined);
80
+ effect(() => s(fn()));
81
+ return () => s();
82
+ }
83
+
84
+ function cleanupDeps(eff) {
85
+ for (const subs of eff.deps) subs.delete(eff);
86
+ eff.deps.clear();
87
+ }
88
+
89
+ function disposeChildren(eff) {
90
+ for (const child of [...eff.children]) child.dispose();
91
+ eff.children.clear();
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // DOM helpers
96
+ // ---------------------------------------------------------------------------
97
+
98
+ // el(tag, props?, ...children) → a real DOM element.
99
+ // props: { onClick: fn } → event listener
100
+ // { class: () => ... } → reactive attribute (function = live)
101
+ // { id: 'x' } → static attribute
102
+ // children: strings, numbers, nodes, arrays, or functions (functions = live)
103
+ export function el(tag, props, ...children) {
104
+ const node = document.createElement(tag);
105
+ if (props) {
106
+ for (const [key, val] of Object.entries(props)) {
107
+ if (key.startsWith("on") && typeof val === "function") {
108
+ node.addEventListener(key.slice(2).toLowerCase(), val);
109
+ } else if (typeof val === "function") {
110
+ effect(() => setAttr(node, key, val()));
111
+ } else {
112
+ setAttr(node, key, val);
113
+ }
114
+ }
115
+ }
116
+ for (const child of children) appendChild(node, child);
117
+ return node;
118
+ }
119
+
120
+ // mount(target, ...children) appends children into target (selector or element).
121
+ // Top-level function-children are reactive too.
122
+ export function mount(target, ...children) {
123
+ const parent = typeof target === "string" ? document.querySelector(target) : target;
124
+ for (const child of children) appendChild(parent, child);
125
+ return parent;
126
+ }
127
+
128
+ // Boolean attributes: presence means "on", so a false/null/"false" value must turn
129
+ // them OFF (readonly="false" is still readonly in HTML). Set via the DOM property;
130
+ // readonly's property is readOnly, so map it.
131
+ const BOOL_ATTRS = new Set(["checked", "disabled", "selected", "readonly", "required", "multiple", "hidden", "autofocus", "open"]);
132
+ const BOOL_PROP = { readonly: "readOnly" };
133
+ function setAttr(node, name, value) {
134
+ if (name === "value") {
135
+ const v = value ?? "";
136
+ if (node.value !== v) node.value = v; // skip redundant writes — they reset the caret while typing
137
+ return;
138
+ }
139
+ if (BOOL_ATTRS.has(name)) {
140
+ node[BOOL_PROP[name] || name] = !!value && value !== "false";
141
+ return;
142
+ }
143
+ if (value === false || value == null) {
144
+ node.removeAttribute(name);
145
+ return;
146
+ }
147
+ node.setAttribute(name, value);
148
+ }
149
+
150
+ // Append a child, making function-children into self-updating dynamic regions
151
+ // bounded by two comment anchors (so they can render text, nodes, or lists).
152
+ function appendChild(parent, child) {
153
+ if (typeof child === "function") {
154
+ const start = document.createComment("");
155
+ const end = document.createComment("");
156
+ parent.appendChild(start);
157
+ parent.appendChild(end);
158
+ effect(() => renderRange(start, end, child()));
159
+ return;
160
+ }
161
+ for (const node of toNodes(child)) parent.appendChild(node);
162
+ }
163
+
164
+ // Replace everything between the start/end anchors with `value`'s nodes.
165
+ function renderRange(start, end, value) {
166
+ if (!end.parentNode) return; // range detached (parent re-rendered) — nothing to do
167
+ let n = start.nextSibling;
168
+ while (n && n !== end) {
169
+ const t = n.nextSibling;
170
+ n.remove();
171
+ n = t;
172
+ }
173
+ for (const node of toNodes(value)) end.parentNode.insertBefore(node, end);
174
+ }
175
+
176
+ // Normalize any child value into an array of DOM nodes.
177
+ function toNodes(value) {
178
+ if (value == null || value === false || value === true) return [];
179
+ if (Array.isArray(value)) return value.flatMap(toNodes);
180
+ if (value instanceof Node) return [value];
181
+ return [document.createTextNode(String(value))];
182
+ }
183
+
184
+ // ---------------------------------------------------------------------------
185
+ // html`` template layer (parses once, wires holes to the same primitives)
186
+ // ---------------------------------------------------------------------------
187
+
188
+ const PH = (i) => `__voltph${i}__`;
189
+ const PH_RE = /__voltph(\d+)__/g;
190
+
191
+ // We're inside an open tag (attribute context) if the last '<' comes after the
192
+ // last '>' in the accumulated string.
193
+ function isAttrContext(str) {
194
+ return str.lastIndexOf("<") > str.lastIndexOf(">");
195
+ }
196
+
197
+ export function html(strings, ...values) {
198
+ let acc = "";
199
+ strings.forEach((str, i) => {
200
+ acc += str;
201
+ if (i < values.length) {
202
+ acc += isAttrContext(acc) ? PH(i) : `<!--${PH(i)}-->`;
203
+ }
204
+ });
205
+
206
+ const tpl = document.createElement("template");
207
+ tpl.innerHTML = acc.trim();
208
+
209
+ // Bind attribute holes.
210
+ for (const node of tpl.content.querySelectorAll("*")) {
211
+ for (const attr of [...node.attributes]) {
212
+ PH_RE.lastIndex = 0;
213
+ if (PH_RE.test(attr.value)) bindAttr(node, attr, values);
214
+ }
215
+ }
216
+
217
+ // Bind node holes (comment placeholders).
218
+ const walker = document.createTreeWalker(tpl.content, NodeFilter.SHOW_COMMENT);
219
+ const holes = [];
220
+ let c;
221
+ while ((c = walker.nextNode())) {
222
+ const m = c.data.match(/^__voltph(\d+)__$/);
223
+ if (m) holes.push([c, Number(m[1])]);
224
+ }
225
+ for (const [comment, i] of holes) bindNodeHole(comment, values[i]);
226
+
227
+ const nodes = [...tpl.content.childNodes];
228
+ return nodes.length === 1 ? nodes[0] : nodes;
229
+ }
230
+
231
+ function bindAttr(node, attr, values) {
232
+ const name = attr.name;
233
+ const raw = attr.value;
234
+ const single = raw.match(/^__voltph(\d+)__$/);
235
+ node.removeAttribute(name);
236
+
237
+ // onX=${fn} → event listener
238
+ if (name.startsWith("on") && single) {
239
+ node.addEventListener(name.slice(2).toLowerCase(), values[Number(single[1])]);
240
+ return;
241
+ }
242
+
243
+ // Otherwise a (possibly mixed) attribute value. If any hole is a function it
244
+ // is read inside the effect, so the attribute stays live.
245
+ effect(() => {
246
+ const text = raw.replace(PH_RE, (_, j) => {
247
+ const v = values[Number(j)];
248
+ return String(typeof v === "function" ? v() : v ?? "");
249
+ });
250
+ setAttr(node, name, text);
251
+ });
252
+ }
253
+
254
+ function bindNodeHole(comment, value) {
255
+ const start = document.createComment("");
256
+ comment.parentNode.insertBefore(start, comment); // `comment` becomes the end anchor
257
+ if (typeof value === "function") {
258
+ effect(() => renderRange(start, comment, value()));
259
+ } else {
260
+ renderRange(start, comment, value);
261
+ }
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Hot reload client — listens for the dev server's reload event over Socket.io
266
+ // ---------------------------------------------------------------------------
267
+
268
+ (function startHotReload() {
269
+ if (typeof window === "undefined") return; // not a browser (SSR / Node imports / tests)
270
+
271
+ // Patch `from` to match `to` in place — only changed nodes are touched, so
272
+ // focus, caret, scroll, and untouched subtrees survive. Positional (no keys),
273
+ // which is plenty for a dev reload; the caller falls back to a full reload if
274
+ // anything throws.
275
+ const morph = (from, to) => {
276
+ const active = document.activeElement;
277
+ if (from.nodeType !== to.nodeType || from.nodeName !== to.nodeName) {
278
+ from.replaceWith(document.importNode(to, true));
279
+ return;
280
+ }
281
+ if (from.nodeType === 3 || from.nodeType === 8) {
282
+ if (from.nodeValue !== to.nodeValue) from.nodeValue = to.nodeValue;
283
+ return;
284
+ }
285
+ if (from.nodeType !== 1) return;
286
+ for (const a of [...to.attributes]) {
287
+ if (from === active && a.name === "value") continue; // don't fight the typist
288
+ if (from.getAttribute(a.name) !== a.value) from.setAttribute(a.name, a.value);
289
+ }
290
+ for (const a of [...from.attributes]) if (!to.hasAttribute(a.name)) from.removeAttribute(a.name);
291
+ let f = from.firstChild,
292
+ t = to.firstChild;
293
+ while (f && t) {
294
+ const nf = f.nextSibling,
295
+ nt = t.nextSibling;
296
+ morph(f, t);
297
+ f = nf;
298
+ t = nt;
299
+ }
300
+ while (f) {
301
+ const nf = f.nextSibling;
302
+ from.removeChild(f);
303
+ f = nf;
304
+ }
305
+ while (t) {
306
+ const nt = t.nextSibling;
307
+ from.appendChild(document.importNode(t, true));
308
+ t = nt;
309
+ }
310
+ };
311
+
312
+ const bustStyles = () => {
313
+ for (const link of document.querySelectorAll('link[rel="stylesheet"]')) {
314
+ const u = new URL(link.getAttribute("href"), location.href);
315
+ u.searchParams.set("_hr", Date.now());
316
+ link.setAttribute("href", u.pathname + u.search);
317
+ }
318
+ };
319
+
320
+ let busy = false;
321
+ const onChange = async (info) => {
322
+ const file = (info && info.file) || "";
323
+ if (/\.css(\?|$)/i.test(file)) return bustStyles(); // pure CSS → swap, no reload
324
+ if (/\.(js|mjs)(\?|$)/i.test(file)) return location.reload(); // JS changed → must re-run
325
+ if (busy) return;
326
+ busy = true;
327
+ try {
328
+ const html = await (await fetch(location.href, { headers: { "x-volt-hot": "1" } })).text();
329
+ const doc = new DOMParser().parseFromString(html, "text/html");
330
+ // a client-rendered app (#app filled by JS, empty in server HTML) can't be
331
+ // morphed without wiping it — full reload instead.
332
+ const cur = document.querySelector("#app");
333
+ const next = doc.querySelector("#app");
334
+ if (cur && cur.children.length && next && !next.children.length) {
335
+ location.reload();
336
+ return;
337
+ }
338
+ morph(document.body, doc.body);
339
+ if (document.title !== doc.title) document.title = doc.title;
340
+ if (/_theme/.test(file)) bustStyles(); // theme/layout edit may change CSS too
341
+ } catch {
342
+ location.reload();
343
+ } finally {
344
+ busy = false;
345
+ }
346
+ };
347
+
348
+ const connect = () => {
349
+ if (!window.io) return false;
350
+ window.io().on("volt:reload", onChange);
351
+ console.log("[volt] hot reload connected (live morph)");
352
+ return true;
353
+ };
354
+ if (!connect()) window.addEventListener("load", connect);
355
+ })();
@@ -0,0 +1,15 @@
1
+ # Render blueprint — https://render.com/docs/blueprint-spec
2
+ # Push this repo to GitHub, then in Render: New → Blueprint → pick the repo.
3
+ # Render builds the Dockerfile, gives you HTTPS + a domain, and runs it.
4
+ services:
5
+ - type: web
6
+ name: volt-app
7
+ runtime: docker
8
+ plan: starter
9
+ envVars:
10
+ - key: VOLT_ADDONS
11
+ sync: false # set your add-ons (e.g. "db,auth") in the dashboard
12
+ # Add the rest in the dashboard as needed:
13
+ # DB_DRIVER, MONGODB_URI / DATABASE_URL,
14
+ # MEDIA_DRIVER, S3_ENDPOINT/S3_REGION/S3_BUCKET/S3_KEY/S3_SECRET,
15
+ # SMTP_URL, MAIL_FROM