@velora-cms/server 0.11.0 → 0.12.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.
Files changed (79) hide show
  1. package/README.md +6 -0
  2. package/dist/admin/sync-admin-bundle.d.ts +9 -0
  3. package/dist/admin/sync-admin-bundle.js +120 -0
  4. package/dist/admin-bundle/.velora-version +1 -0
  5. package/dist/admin-bundle/assets/{_virtual_mf-localSharedImportMap___mfe_internal__velora_admin__mf_owner__1-DOKdWYyj.js → _virtual_mf-localSharedImportMap___mfe_internal__velora_admin__mf_owner__1-DlIgIuFP.js} +1 -1
  6. package/dist/admin-bundle/assets/{_virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare___mf_0_velora_mf_2_cms_mf_1_plugin_mf_2_sdk__loadShare__.js-C1-ZAxwO.js → _virtual_mf___mfe_internal__velora_admin__mf_owner__1__loadShare___mf_0_velora_mf_2_cms_mf_1_plugin_mf_2_sdk__loadShare__.js-Bm9EahCG.js} +2 -2
  7. package/dist/admin-bundle/assets/{content-trash.schema-DfXHkC-w.js → content-trash.schema-tvJ4Lbos.js} +5 -5
  8. package/dist/admin-bundle/assets/{hostInit--C4xK6nj.js → hostInit-B-6-34XY.js} +1 -1
  9. package/dist/admin-bundle/assets/{index-DhMY-0sC.js → index-CHCGuSbb.js} +1 -1
  10. package/dist/admin-bundle/assets/{index-BIAF0ShP.js → index-DYO7-IK8.js} +33 -33
  11. package/dist/admin-bundle/assets/{mf-entry-bootstrap-0-aef015cd.js → mf-entry-bootstrap-0-7b0ce2fe.js} +2 -2
  12. package/dist/admin-bundle/assets/{remoteEntry-CNyN68bc.js → remoteEntry-J2yJXsGB.js} +2 -2
  13. package/dist/admin-bundle/index.html +4 -4
  14. package/dist/app.d.ts +16 -1
  15. package/dist/app.js +63 -4
  16. package/dist/auth/api-keys.d.ts +1 -0
  17. package/dist/auth/api-keys.js +5 -1
  18. package/dist/auth/route-rate-limits.d.ts +4 -0
  19. package/dist/auth/route-rate-limits.js +13 -0
  20. package/dist/bin-sync-admin.d.ts +2 -0
  21. package/dist/bin-sync-admin.js +39 -0
  22. package/dist/bundled-templates/com.velora.blog/package.tar.gz +0 -0
  23. package/dist/config/public-url.d.ts +3 -0
  24. package/dist/config/public-url.js +45 -0
  25. package/dist/db/core-migrations/steps.js +95 -16
  26. package/dist/docs-bundle/pagefind/fragment/en_42f5356.pf_fragment +0 -0
  27. package/dist/docs-bundle/pagefind/fragment/en_6c5ee82.pf_fragment +0 -0
  28. package/dist/docs-bundle/pagefind/fragment/en_80941b8.pf_fragment +0 -0
  29. package/dist/docs-bundle/pagefind/fragment/en_a64440b.pf_fragment +0 -0
  30. package/dist/docs-bundle/pagefind/fragment/en_eb168aa.pf_fragment +0 -0
  31. package/dist/docs-bundle/pagefind/fragment/en_f053753.pf_fragment +0 -0
  32. package/dist/docs-bundle/pagefind/index/en_33ea61e.pf_index +0 -0
  33. package/dist/docs-bundle/pagefind/index/en_bb7c383.pf_index +0 -0
  34. package/dist/docs-bundle/pagefind/index/en_ff6eddf.pf_index +0 -0
  35. package/dist/docs-bundle/pagefind/pagefind-entry.json +1 -1
  36. package/dist/docs-bundle/pagefind/pagefind.en_6bbff64ea9.pf_meta +0 -0
  37. package/dist/docs-bundle/v1/index.html +4 -4
  38. package/dist/docs-bundle/v1/using-velora/getting-started/index.html +5 -3
  39. package/dist/docs-bundle/v1/using-velora/headless-api/index.html +10 -4
  40. package/dist/docs-bundle/v1/using-velora/index.html +6 -4
  41. package/dist/docs-bundle/v1/using-velora/installation/index.html +73 -15
  42. package/dist/docs-bundle/v1/using-velora/media/index.html +118 -34
  43. package/dist/graphql/schema.d.ts +1 -0
  44. package/dist/graphql/schema.js +5 -2
  45. package/dist/lib.d.ts +1 -0
  46. package/dist/lib.js +45 -11
  47. package/dist/media/media-service.d.ts +14 -15
  48. package/dist/media/media-service.js +121 -59
  49. package/dist/media/media-tokens.d.ts +6 -0
  50. package/dist/media/media-tokens.js +88 -0
  51. package/dist/media/storage/local.d.ts +16 -0
  52. package/dist/media/storage/local.js +99 -0
  53. package/dist/media/storage/s3.d.ts +25 -0
  54. package/dist/media/storage/s3.js +134 -0
  55. package/dist/media/storage/types.d.ts +21 -0
  56. package/dist/media/storage/types.js +44 -0
  57. package/dist/routes/admin-static.d.ts +2 -0
  58. package/dist/routes/admin-static.js +42 -4
  59. package/dist/routes/media-public.d.ts +6 -0
  60. package/dist/routes/media-public.js +265 -0
  61. package/dist/routes/media.d.ts +2 -1
  62. package/dist/routes/media.js +95 -5
  63. package/dist/routes/plugin-storage.js +5 -1
  64. package/dist/routes/public-api.js +5 -3
  65. package/dist/routes/published-item.d.ts +2 -1
  66. package/dist/routes/published-item.js +28 -5
  67. package/dist/scripts/copy-admin-bundle.js +9 -1
  68. package/package.json +12 -11
  69. package/prisma/schema.prisma +16 -3
  70. package/dist/docs-bundle/pagefind/fragment/en_1d9eb24.pf_fragment +0 -0
  71. package/dist/docs-bundle/pagefind/fragment/en_1ef320c.pf_fragment +0 -0
  72. package/dist/docs-bundle/pagefind/fragment/en_3099406.pf_fragment +0 -0
  73. package/dist/docs-bundle/pagefind/fragment/en_9aa652e.pf_fragment +0 -0
  74. package/dist/docs-bundle/pagefind/fragment/en_d855550.pf_fragment +0 -0
  75. package/dist/docs-bundle/pagefind/fragment/en_e6d88d8.pf_fragment +0 -0
  76. package/dist/docs-bundle/pagefind/index/en_5feeed7.pf_index +0 -0
  77. package/dist/docs-bundle/pagefind/index/en_c6981f9.pf_index +0 -0
  78. package/dist/docs-bundle/pagefind/index/en_ce4f6bf.pf_index +0 -0
  79. package/dist/docs-bundle/pagefind/pagefind.en_1e94abd852.pf_meta +0 -0
@@ -71,7 +71,7 @@
71
71
  })();
72
72
  </script></sl-sidebar-state-persist><div class="md:sl-hidden"><div class="mobile-preferences sl-flex astro-yfet6llj"><div class="social-icons astro-yfet6llj"></div><starlight-theme-select><label style="--sl-select-width: 6.25em" class="astro-7uz3f5wr"><span class="sr-only astro-7uz3f5wr">Select theme</span><svg aria-hidden="true" class="icon label-icon astro-7uz3f5wr astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg><select autocomplete="off" class="astro-7uz3f5wr"><option value="dark" class="astro-7uz3f5wr">Dark</option><option value="light" class="astro-7uz3f5wr">Light</option><option value="auto" selected class="astro-7uz3f5wr">Auto</option></select><svg aria-hidden="true" class="icon caret astro-7uz3f5wr astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M17 9.17a1 1 0 0 0-1.41 0L12 12.71 8.46 9.17a1 1 0 1 0-1.41 1.42l4.24 4.24a1.002 1.002 0 0 0 1.42 0L17 10.59a1.002 1.002 0 0 0 0-1.42Z"/></svg></label></starlight-theme-select><script>
73
73
  StarlightThemeProvider.updatePickers();
74
- </script></div></div></div></div></nav><div class="main-frame astro-5auw3bkx"><script type="module">var e=document.getElementById(`starlight__sidebar`),t=e?.querySelector(`sl-sidebar-state-persist`),n=`sl-sidebar-state`,r=()=>{let r=[],i=t?.dataset.hash||``;try{let e=sessionStorage.getItem(n),t=JSON.parse(e||`{}`);Array.isArray(t.open)&&t.hash===i&&(r=t.open)}catch{}return{hash:i,open:r,scroll:e?.scrollTop||0}},i=e=>{try{sessionStorage.setItem(n,JSON.stringify(e))}catch{}},a=()=>i(r()),o=(e,t)=>{let n=r();n.open[t]=e,i(n)};t?.addEventListener(`click`,e=>{if(!(e.target instanceof Element))return;let t=e.target.closest(`summary`)?.closest(`details`);if(!t)return;let n=t.querySelector(`sl-sidebar-restore`),r=parseInt(n?.dataset.index||``);isNaN(r)||o(!t.open,r)}),addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&a()});</script><div class="lg:sl-flex astro-perbdhd2"><aside class="right-sidebar-container print:hidden astro-perbdhd2"><div class="right-sidebar astro-perbdhd2"><div class="lg:sl-hidden astro-epm5tkrc"><mobile-starlight-toc data-min-h="2" data-max-h="3" class="astro-bnvebtr4"><nav aria-labelledby="starlight__on-this-page--mobile" class="astro-bnvebtr4"><details id="starlight__mobile-toc" class="astro-bnvebtr4"><summary id="starlight__on-this-page--mobile" class="sl-flex astro-bnvebtr4"><span class="toggle sl-flex astro-bnvebtr4">On this page<svg aria-hidden="true" class="caret astro-bnvebtr4 astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1rem;"><path d="m14.83 11.29-4.24-4.24a1 1 0 1 0-1.42 1.41L12.71 12l-3.54 3.54a1 1 0 0 0 0 1.41 1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.24-4.24a1.002 1.002 0 0 0 0-1.42Z"/></svg></span><span class="display-current astro-bnvebtr4"></span></summary><div class="dropdown astro-bnvebtr4"><ul class="isMobile astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#about-this-documentation" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">About this documentation</span></a></li></ul></div></details></nav></mobile-starlight-toc><script type="module" src="/docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.BcSo_yiZ.js"></script></div><div class="right-sidebar-panel sl-hidden lg:sl-block astro-epm5tkrc"><div class="sl-container astro-epm5tkrc"><starlight-toc data-min-h="2" data-max-h="3"><nav aria-labelledby="starlight__on-this-page"><h2 id="starlight__on-this-page">On this page</h2><ul class="astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#about-this-documentation" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">About this documentation</span></a></li></ul></nav></starlight-toc><script type="module" src="/docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.Csloo1VZ.js"></script></div></div></div></aside><div class="main-pane astro-perbdhd2"><main data-pagefind-body class="astro-ewhz5xxr" lang="en" dir="ltr"><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><h1 id="_top" class="astro-oef2efwn">Velora</h1></div></div><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><div class="sl-markdown-content"><p>Velora is an open-source CMS framework built on Node.js and React. Content is
74
+ </script></div></div></div></div></nav><div class="main-frame astro-5auw3bkx"><script type="module">var e=document.getElementById(`starlight__sidebar`),t=e?.querySelector(`sl-sidebar-state-persist`),n=`sl-sidebar-state`,r=()=>{let r=[],i=t?.dataset.hash||``;try{let e=sessionStorage.getItem(n),t=JSON.parse(e||`{}`);Array.isArray(t.open)&&t.hash===i&&(r=t.open)}catch{}return{hash:i,open:r,scroll:e?.scrollTop||0}},i=e=>{try{sessionStorage.setItem(n,JSON.stringify(e))}catch{}},a=()=>i(r()),o=(e,t)=>{let n=r();n.open[t]=e,i(n)};t?.addEventListener(`click`,e=>{if(!(e.target instanceof Element))return;let t=e.target.closest(`summary`)?.closest(`details`);if(!t)return;let n=t.querySelector(`sl-sidebar-restore`),r=parseInt(n?.dataset.index||``);isNaN(r)||o(!t.open,r)}),addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&a()});</script><div class="lg:sl-flex astro-perbdhd2"><aside class="right-sidebar-container print:hidden astro-perbdhd2"><div class="right-sidebar astro-perbdhd2"><div class="lg:sl-hidden astro-epm5tkrc"><mobile-starlight-toc data-min-h="2" data-max-h="3" class="astro-bnvebtr4"><nav aria-labelledby="starlight__on-this-page--mobile" class="astro-bnvebtr4"><details id="starlight__mobile-toc" class="astro-bnvebtr4"><summary id="starlight__on-this-page--mobile" class="sl-flex astro-bnvebtr4"><span class="toggle sl-flex astro-bnvebtr4">On this page<svg aria-hidden="true" class="caret astro-bnvebtr4 astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1rem;"><path d="m14.83 11.29-4.24-4.24a1 1 0 1 0-1.42 1.41L12.71 12l-3.54 3.54a1 1 0 0 0 0 1.41 1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.24-4.24a1.002 1.002 0 0 0 0-1.42Z"/></svg></span><span class="display-current astro-bnvebtr4"></span></summary><div class="dropdown astro-bnvebtr4"><ul class="isMobile astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#about-this-documentation" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">About this documentation</span></a></li></ul></div></details></nav></mobile-starlight-toc><script type="module" src="/docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.BcSo_yiZ.js"></script></div><div class="right-sidebar-panel sl-hidden lg:sl-block astro-epm5tkrc"><div class="sl-container astro-epm5tkrc"><starlight-toc data-min-h="2" data-max-h="3"><nav aria-labelledby="starlight__on-this-page"><h2 id="starlight__on-this-page">On this page</h2><ul class="astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#about-this-documentation" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">About this documentation</span></a></li></ul></nav></starlight-toc><script type="module" src="/docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.Csloo1VZ.js"></script></div></div></div></aside><div class="main-pane astro-perbdhd2"><main data-pagefind-body class="astro-ewhz5xxr" lang="en" dir="ltr"><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><h1 id="_top" class="astro-oef2efwn">Velora</h1></div></div><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><div class="sl-markdown-content"><p>Velora is a CMS framework built on Node.js and React. Content is
75
75
  immutable — every save writes a new version and rollback only changes which
76
76
  version is current — updates never break a running site, the content tree
77
77
  scales from a handful of pages to very large ones, and the same instance can
@@ -81,6 +81,6 @@ be used headless through its API or traditionally through its own rendering.</p>
81
81
  <p>These docs describe <strong>v1</strong>, the first public release line. Everything lives
82
82
  under <code dir="auto">/v1/</code> so that future versions can be published alongside it without
83
83
  breaking links to this one.</p>
84
- <p>Installing a Velora instance today follows the contributor path in
85
- <a href="/docs/v1/using-velora/installation/">Installation</a>: clone the source and run the
86
- setup wizard. A one-command <code dir="auto">npm create velora</code> installer is coming.</p></div><footer class="sl-flex astro-gjkl2a5w"><div class="meta sl-flex astro-gjkl2a5w"></div><div class="pagination-links print:hidden astro-3tj6ibx2" dir="ltr"></div></footer><div class="velora-bundle-stamp astro-llmj63sw">Velora 0.9.0 &middot; bundled documentation</div></div></div></main></div></div></div></div></body></html>
84
+ <p>Get a Velora instance running with <a href="/docs/v1/using-velora/installation/">Installation</a>:
85
+ <code dir="auto">npm create velora@latest my-site</code> scaffolds a runnable project in one
86
+ command, and the setup wizard takes it from there.</p></div><footer class="sl-flex astro-gjkl2a5w"><div class="meta sl-flex astro-gjkl2a5w"></div><div class="pagination-links print:hidden astro-3tj6ibx2" dir="ltr"></div></footer><div class="velora-bundle-stamp astro-llmj63sw">Velora 0.9.0 &middot; bundled documentation</div></div></div></main></div></div></div></div></body></html>
@@ -83,9 +83,11 @@ fresh, unconfigured instance instead, the same shape a real deployment
83
83
  boots into on first run (see <code dir="auto">DEPLOYMENT.md</code> at the repo root). The
84
84
  <a href="https://docs.velora-cms.com/v1/contributing/local-setup/">Local setup</a> page’s prerequisites still
85
85
  apply — a checkout of the Velora repo, Node.js 22.12+, and <code dir="auto">pnpm</code> — so
86
- this walkthrough is a contributor path run from a source-checkout sandbox,
87
- not the way to try Velora today. A one-command install, <code dir="auto">npm create velora</code>, is coming; until then, the fastest way to see Velora working is
88
- the hosted demo, and the <a href="/docs/v1/using-velora/build-a-headless-frontend/">headless tutorial</a>
86
+ this walkthrough is a contributor path run from a source-checkout sandbox.
87
+ The fastest way to run Velora yourself is the one-command install
88
+ <code dir="auto">npm create velora@latest my-site</code>, which writes a <code dir="auto">server.js</code> project you
89
+ run directly — covered in <a href="/docs/v1/using-velora/installation/">Installation</a>;
90
+ the hosted demo needs no install at all, and the <a href="/docs/v1/using-velora/build-a-headless-frontend/">headless tutorial</a>
89
91
  walks the public API this same Blog content is served through.</p>
90
92
  <div class="sl-heading-wrapper level-h2"><h2 id="environment-variables-this-tutorial-sets">Environment variables this tutorial sets</h2><a class="sl-anchor-link" href="#environment-variables-this-tutorial-sets"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Environment variables this tutorial sets”</span></a></div>
91
93
  <p>You’ll set four environment variables along the way, each explained in
@@ -1198,7 +1198,7 @@ those.</p>
1198
1198
 
1199
1199
  <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">:id</code></td><td>string (uuid), path parameter</td><td>The media file’s identity.</td><td><code dir="auto">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</code></td></tr></tbody></table>
1200
1200
  <p>Nothing is uploaded on the demo, so this shape is schema-derived:</p>
1201
- <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="json"><code><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"id"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"filename"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"mimeType"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">image/png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"size"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#F78C6C;--1:#AA0982">48213</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"downloadUrl"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">https://s3.example.com/velora-media/9f1d3b7c/cover.png?X-Amz-Expires=900</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"uploadedAt"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">2026-08-31T09:12:04.006Z</span><span style="--0:#D9F5DD;--1:#111111">"</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="{ &#x22;id&#x22;: &#x22;9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218&#x22;, &#x22;filename&#x22;: &#x22;cover.png&#x22;, &#x22;mimeType&#x22;: &#x22;image/png&#x22;, &#x22;size&#x22;: 48213, &#x22;downloadUrl&#x22;: &#x22;https://s3.example.com/velora-media/9f1d3b7c/cover.png?X-Amz-Expires=900&#x22;, &#x22;uploadedAt&#x22;: &#x22;2026-08-31T09:12:04.006Z&#x22;}"><div></div></button></div></figure></div>
1201
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="json"><code><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"id"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"filename"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"mimeType"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">image/png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"size"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#F78C6C;--1:#AA0982">48213</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"downloadUrl"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">https://demo.velora-cms.com/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"visibility"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">public</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"uploadedAt"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">2026-08-31T09:12:04.006Z</span><span style="--0:#D9F5DD;--1:#111111">"</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="{ &#x22;id&#x22;: &#x22;9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218&#x22;, &#x22;filename&#x22;: &#x22;cover.png&#x22;, &#x22;mimeType&#x22;: &#x22;image/png&#x22;, &#x22;size&#x22;: 48213, &#x22;downloadUrl&#x22;: &#x22;https://demo.velora-cms.com/media/9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218/cover.png&#x22;, &#x22;visibility&#x22;: &#x22;public&#x22;, &#x22;uploadedAt&#x22;: &#x22;2026-08-31T09:12:04.006Z&#x22;}"><div></div></button></div></figure></div>
1202
1202
  <p>Every property:</p>
1203
1203
 
1204
1204
 
@@ -1247,9 +1247,15 @@ those.</p>
1247
1247
 
1248
1248
 
1249
1249
 
1250
- <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">id</code></td><td>string (uuid)</td><td>The file’s identity — this is the value an Image field stores in <code dir="auto">data</code>.</td><td><code dir="auto">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</code></td></tr><tr><td><code dir="auto">filename</code></td><td>string, non-empty</td><td>The original filename, kept verbatim.</td><td><code dir="auto">cover.png</code></td></tr><tr><td><code dir="auto">mimeType</code></td><td>string, non-empty</td><td>Content type recorded at upload.</td><td><code dir="auto">image/png</code></td></tr><tr><td><code dir="auto">size</code></td><td>integer, 0 or more</td><td>Byte length, measured server-side.</td><td><code dir="auto">48213</code></td></tr><tr><td><code dir="auto">downloadUrl</code></td><td>string (URL)</td><td>A <strong>presigned GET</strong>, generated per response and valid for 900 seconds. Never persisted and never the same twice — refetch the record rather than caching the URL.</td><td><code dir="auto">https://s3.example.com/velora-media/…?X-Amz-Expires=900</code></td></tr><tr><td><code dir="auto">uploadedAt</code></td><td>ISO datetime string</td><td>When the record was written.</td><td><code dir="auto">2026-08-31T09:12:04.006Z</code></td></tr></tbody></table>
1251
- <p>The uploader’s user id and the canonical object URL are on the admin shape
1252
- only, deliberately — see <a href="/docs/v1/using-velora/media/">Media</a>. There is no
1250
+
1251
+
1252
+
1253
+
1254
+
1255
+
1256
+ <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">id</code></td><td>string (uuid)</td><td>The file’s identity — this is the value an Image field stores in <code dir="auto">data</code>.</td><td><code dir="auto">9f1d3b7c-52ae-4d08-9b31-6c74a0f5e218</code></td></tr><tr><td><code dir="auto">filename</code></td><td>string, non-empty</td><td>The original filename, kept verbatim.</td><td><code dir="auto">cover.png</code></td></tr><tr><td><code dir="auto">mimeType</code></td><td>string, non-empty</td><td>Content type recorded at upload.</td><td><code dir="auto">image/png</code></td></tr><tr><td><code dir="auto">size</code></td><td>integer, 0 or more</td><td>Byte length, measured server-side.</td><td><code dir="auto">48213</code></td></tr><tr><td><code dir="auto">downloadUrl</code></td><td>string (absolute URL)</td><td>The stable <code dir="auto">/media/:id/:filename</code> path, made absolute against <code dir="auto">VELORA_PUBLIC_URL</code> when set, or the request’s own host otherwise. <strong>Permanent</strong> for a public file — safe to persist or embed, never re-signed, never expires. For a private file it carries a freshly minted, short-lived <code dir="auto">?token=</code> instead, and the bare path 404s for anyone without that token, a session, or a read-scoped key — same body as an unknown id, so this route never confirms a private id exists.</td><td><code dir="auto">https://cms.example.com/media/9f1d3b7c…/cover.png</code></td></tr><tr><td><code dir="auto">visibility</code></td><td><code dir="auto">"public"</code> | <code dir="auto">"private"</code></td><td>Whether <code dir="auto">downloadUrl</code> needed a token.</td><td><code dir="auto">public</code></td></tr><tr><td><code dir="auto">uploadedAt</code></td><td>ISO datetime string</td><td>When the record was written.</td><td><code dir="auto">2026-08-31T09:12:04.006Z</code></td></tr></tbody></table>
1257
+ <p>The uploader’s user id and the path-relative admin <code dir="auto">url</code> are on the admin
1258
+ shape only, deliberately — see <a href="/docs/v1/using-velora/media/">Media</a>. There is no
1253
1259
  public upload route: writes to the media library go through the admin API
1254
1260
  with a signed-in session.</p>
1255
1261
  <div class="sl-heading-wrapper level-h2"><h2 id="templates">Templates</h2><a class="sl-anchor-link" href="#templates"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Templates”</span></a></div>
@@ -209,7 +209,9 @@ a worked frontend that renders content from a live Velora instance.</li>
209
209
  so it travels with your instance and always matches the version you are
210
210
  running. Documentation for <em>writing</em> plugins is hosted only, at
211
211
  <a href="https://docs.velora-cms.com/v1/building-plugins/">docs.velora-cms.com/v1/building-plugins</a>.</p></div></aside>
212
- <aside aria-label="Note" class="starlight-aside starlight-aside--note"><p class="starlight-aside__title" aria-hidden="true"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" class="starlight-aside__icon"><path d="M12 11a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0v-4a1 1 0 0 0-1-1Zm.38-3.92a1 1 0 0 0-.76 0 1 1 0 0 0-.33.21 1.15 1.15 0 0 0-.21.33 1 1 0 0 0 .21 1.09c.097.088.209.16.33.21A1 1 0 0 0 13 8a1.05 1.05 0 0 0-.29-.71 1 1 0 0 0-.33-.21ZM12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm0 18a8 8 0 1 1 0-16.001A8 8 0 0 1 12 20Z"></path></svg>Note</p><div class="starlight-aside__content"><p>A one-command install — <code dir="auto">npm create velora</code> — is coming. Today, running your
213
- own instance means working from source (available to contributors on
214
- request); <a href="/docs/v1/using-velora/getting-started/">Getting started</a> walks that
215
- path end to end, and the hosted demo above needs no install at all.</p></div></aside></div><footer class="sl-flex astro-gjkl2a5w"><div class="meta sl-flex astro-gjkl2a5w"></div><div class="pagination-links print:hidden astro-3tj6ibx2" dir="ltr"><a href="/docs/v1/using-velora/getting-started/" rel="next" class="astro-3tj6ibx2"><svg aria-hidden="true" class="astro-3tj6ibx2 astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.5rem;"><path d="M17.92 11.62a1.001 1.001 0 0 0-.21-.33l-5-5a1.003 1.003 0 1 0-1.42 1.42l3.3 3.29H7a1 1 0 0 0 0 2h7.59l-3.3 3.29a1.002 1.002 0 0 0 .325 1.639 1 1 0 0 0 1.095-.219l5-5a1 1 0 0 0 .21-.33 1 1 0 0 0 0-.76Z"/></svg><span class="astro-3tj6ibx2">Next<br class="astro-3tj6ibx2"><span class="link-title astro-3tj6ibx2">Getting started</span></span></a></div></footer><div class="velora-bundle-stamp astro-llmj63sw">Velora 0.9.0 &middot; bundled documentation</div></div></div></main></div></div></div></div></body></html>
212
+ <aside aria-label="Tip" class="starlight-aside starlight-aside--tip"><p class="starlight-aside__title" aria-hidden="true"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" class="starlight-aside__icon"><path fill-rule="evenodd" d="M1.44 8.855v-.001l3.527-3.516c.34-.344.802-.541 1.285-.548h6.649l.947-.947c3.07-3.07 6.207-3.072 7.62-2.868a1.821 1.821 0 0 1 1.557 1.557c.204 1.413.203 4.55-2.868 7.62l-.946.946v6.649a1.845 1.845 0 0 1-.549 1.286l-3.516 3.528a1.844 1.844 0 0 1-3.11-.944l-.858-4.275-4.52-4.52-2.31-.463-1.964-.394A1.847 1.847 0 0 1 .98 10.693a1.843 1.843 0 0 1 .46-1.838Zm5.379 2.017-3.873-.776L6.32 6.733h4.638l-4.14 4.14Zm8.403-5.655c2.459-2.46 4.856-2.463 5.89-2.33.134 1.035.13 3.432-2.329 5.891l-6.71 6.71-3.561-3.56 6.71-6.711Zm-1.318 15.837-.776-3.873 4.14-4.14v4.639l-3.364 3.374Z" clip-rule="evenodd"></path><path d="M9.318 18.345a.972.972 0 0 0-1.86-.561c-.482 1.435-1.687 2.204-2.934 2.619a8.22 8.22 0 0 1-1.23.302c.062-.365.157-.79.303-1.229.415-1.247 1.184-2.452 2.62-2.935a.971.971 0 1 0-.62-1.842c-.12.04-.236.084-.35.13-2.02.828-3.012 2.588-3.493 4.033a10.383 10.383 0 0 0-.51 2.845l-.001.016v.063c0 .536.434.972.97.972H2.24a7.21 7.21 0 0 0 .878-.065c.527-.063 1.248-.19 2.02-.447 1.445-.48 3.205-1.472 4.033-3.494a5.828 5.828 0 0 0 .147-.407Z"></path></svg>Tip</p><div class="starlight-aside__content"><p>Run your own instance with one command: <code dir="auto">npm create velora@latest my-site</code> —
213
+ it writes a plain Node project (a <code dir="auto">server.js</code> you own, plus the admin UI
214
+ copied into <code dir="auto">public/</code>); see <a href="/docs/v1/using-velora/installation/">Installation</a>
215
+ for the full layout. The hosted demo above needs no install at all, and
216
+ contributors can still work from source via
217
+ <a href="/docs/v1/using-velora/getting-started/">Getting started</a>.</p></div></aside></div><footer class="sl-flex astro-gjkl2a5w"><div class="meta sl-flex astro-gjkl2a5w"></div><div class="pagination-links print:hidden astro-3tj6ibx2" dir="ltr"><a href="/docs/v1/using-velora/getting-started/" rel="next" class="astro-3tj6ibx2"><svg aria-hidden="true" class="astro-3tj6ibx2 astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1.5rem;"><path d="M17.92 11.62a1.001 1.001 0 0 0-.21-.33l-5-5a1.003 1.003 0 1 0-1.42 1.42l3.3 3.29H7a1 1 0 0 0 0 2h7.59l-3.3 3.29a1.002 1.002 0 0 0 .325 1.639 1 1 0 0 0 1.095-.219l5-5a1 1 0 0 0 .21-.33 1 1 0 0 0 0-.76Z"/></svg><span class="astro-3tj6ibx2">Next<br class="astro-3tj6ibx2"><span class="link-title astro-3tj6ibx2">Getting started</span></span></a></div></footer><div class="velora-bundle-stamp astro-llmj63sw">Velora 0.9.0 &middot; bundled documentation</div></div></div></main></div></div></div></div></body></html>
@@ -70,7 +70,7 @@
70
70
  })();
71
71
  </script></sl-sidebar-state-persist><div class="md:sl-hidden"><div class="mobile-preferences sl-flex astro-yfet6llj"><div class="social-icons astro-yfet6llj"></div><starlight-theme-select><label style="--sl-select-width: 6.25em" class="astro-7uz3f5wr"><span class="sr-only astro-7uz3f5wr">Select theme</span><svg aria-hidden="true" class="icon label-icon astro-7uz3f5wr astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z"/></svg><select autocomplete="off" class="astro-7uz3f5wr"><option value="dark" class="astro-7uz3f5wr">Dark</option><option value="light" class="astro-7uz3f5wr">Light</option><option value="auto" selected class="astro-7uz3f5wr">Auto</option></select><svg aria-hidden="true" class="icon caret astro-7uz3f5wr astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1em;"><path d="M17 9.17a1 1 0 0 0-1.41 0L12 12.71 8.46 9.17a1 1 0 1 0-1.41 1.42l4.24 4.24a1.002 1.002 0 0 0 1.42 0L17 10.59a1.002 1.002 0 0 0 0-1.42Z"/></svg></label></starlight-theme-select><script>
72
72
  StarlightThemeProvider.updatePickers();
73
- </script></div></div></div></div></nav><div class="main-frame astro-5auw3bkx"><script type="module">var e=document.getElementById(`starlight__sidebar`),t=e?.querySelector(`sl-sidebar-state-persist`),n=`sl-sidebar-state`,r=()=>{let r=[],i=t?.dataset.hash||``;try{let e=sessionStorage.getItem(n),t=JSON.parse(e||`{}`);Array.isArray(t.open)&&t.hash===i&&(r=t.open)}catch{}return{hash:i,open:r,scroll:e?.scrollTop||0}},i=e=>{try{sessionStorage.setItem(n,JSON.stringify(e))}catch{}},a=()=>i(r()),o=(e,t)=>{let n=r();n.open[t]=e,i(n)};t?.addEventListener(`click`,e=>{if(!(e.target instanceof Element))return;let t=e.target.closest(`summary`)?.closest(`details`);if(!t)return;let n=t.querySelector(`sl-sidebar-restore`),r=parseInt(n?.dataset.index||``);isNaN(r)||o(!t.open,r)}),addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&a()});</script><div class="lg:sl-flex astro-perbdhd2"><aside class="right-sidebar-container print:hidden astro-perbdhd2"><div class="right-sidebar astro-perbdhd2"><div class="lg:sl-hidden astro-epm5tkrc"><mobile-starlight-toc data-min-h="2" data-max-h="3" class="astro-bnvebtr4"><nav aria-labelledby="starlight__on-this-page--mobile" class="astro-bnvebtr4"><details id="starlight__mobile-toc" class="astro-bnvebtr4"><summary id="starlight__on-this-page--mobile" class="sl-flex astro-bnvebtr4"><span class="toggle sl-flex astro-bnvebtr4">On this page<svg aria-hidden="true" class="caret astro-bnvebtr4 astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1rem;"><path d="m14.83 11.29-4.24-4.24a1 1 0 1 0-1.42 1.41L12.71 12l-3.54 3.54a1 1 0 0 0 0 1.41 1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.24-4.24a1.002 1.002 0 0 0 0-1.42Z"/></svg></span><span class="display-current astro-bnvebtr4"></span></summary><div class="dropdown astro-bnvebtr4"><ul class="isMobile astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#what-the-wizard-does" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What the wizard does</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#step-1--the-first-admin-account" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Step 1 — the first admin account</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#step-2--the-database" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Step 2 — the database</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#step-3--the-starter-template" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Step 3 — the starter template</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#the-wizards-api" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">The wizard’s API</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#choosing-a-database" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Choosing a database</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#configuring-the-server" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Configuring the server</span></a><ul class="isMobile astro-lptfoybu" style="--depth: 1;"><li style="--depth: 1;" class="astro-lptfoybu"><a href="#core" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Core</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#features" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Features</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#media-storage" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Media storage</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#limits" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Limits</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#licensing" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Licensing</span></a></li></ul></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#after-setup" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">After setup</span></a></li></ul></div></details></nav></mobile-starlight-toc><script type="module" src="/docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.BcSo_yiZ.js"></script></div><div class="right-sidebar-panel sl-hidden lg:sl-block astro-epm5tkrc"><div class="sl-container astro-epm5tkrc"><starlight-toc data-min-h="2" data-max-h="3"><nav aria-labelledby="starlight__on-this-page"><h2 id="starlight__on-this-page">On this page</h2><ul class="astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#what-the-wizard-does" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What the wizard does</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#step-1--the-first-admin-account" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Step 1 — the first admin account</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#step-2--the-database" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Step 2 — the database</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#step-3--the-starter-template" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Step 3 — the starter template</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#the-wizards-api" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">The wizard’s API</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#choosing-a-database" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Choosing a database</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#configuring-the-server" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Configuring the server</span></a><ul class="astro-lptfoybu" style="--depth: 1;"><li style="--depth: 1;" class="astro-lptfoybu"><a href="#core" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Core</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#features" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Features</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#media-storage" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Media storage</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#limits" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Limits</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#licensing" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Licensing</span></a></li></ul></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#after-setup" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">After setup</span></a></li></ul></nav></starlight-toc><script type="module" src="/docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.Csloo1VZ.js"></script></div></div></div></aside><div class="main-pane astro-perbdhd2"><main data-pagefind-body class="astro-ewhz5xxr" lang="en" dir="ltr"><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><h1 id="_top" class="astro-oef2efwn">Installation</h1></div></div><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><div class="sl-markdown-content"><p>Velora ships as a self-hosted application: <code dir="auto">apps/server</code> (the API) and
73
+ </script></div></div></div></div></nav><div class="main-frame astro-5auw3bkx"><script type="module">var e=document.getElementById(`starlight__sidebar`),t=e?.querySelector(`sl-sidebar-state-persist`),n=`sl-sidebar-state`,r=()=>{let r=[],i=t?.dataset.hash||``;try{let e=sessionStorage.getItem(n),t=JSON.parse(e||`{}`);Array.isArray(t.open)&&t.hash===i&&(r=t.open)}catch{}return{hash:i,open:r,scroll:e?.scrollTop||0}},i=e=>{try{sessionStorage.setItem(n,JSON.stringify(e))}catch{}},a=()=>i(r()),o=(e,t)=>{let n=r();n.open[t]=e,i(n)};t?.addEventListener(`click`,e=>{if(!(e.target instanceof Element))return;let t=e.target.closest(`summary`)?.closest(`details`);if(!t)return;let n=t.querySelector(`sl-sidebar-restore`),r=parseInt(n?.dataset.index||``);isNaN(r)||o(!t.open,r)}),addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&a()});</script><div class="lg:sl-flex astro-perbdhd2"><aside class="right-sidebar-container print:hidden astro-perbdhd2"><div class="right-sidebar astro-perbdhd2"><div class="lg:sl-hidden astro-epm5tkrc"><mobile-starlight-toc data-min-h="2" data-max-h="3" class="astro-bnvebtr4"><nav aria-labelledby="starlight__on-this-page--mobile" class="astro-bnvebtr4"><details id="starlight__mobile-toc" class="astro-bnvebtr4"><summary id="starlight__on-this-page--mobile" class="sl-flex astro-bnvebtr4"><span class="toggle sl-flex astro-bnvebtr4">On this page<svg aria-hidden="true" class="caret astro-bnvebtr4 astro-24n5ceuf" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="--sl-icon-size: 1rem;"><path d="m14.83 11.29-4.24-4.24a1 1 0 1 0-1.42 1.41L12.71 12l-3.54 3.54a1 1 0 0 0 0 1.41 1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.24-4.24a1.002 1.002 0 0 0 0-1.42Z"/></svg></span><span class="display-current astro-bnvebtr4"></span></summary><div class="dropdown astro-bnvebtr4"><ul class="isMobile astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#what-the-wizard-does" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What the wizard does</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#whats-in-the-scaffolded-folder" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What’s in the scaffolded folder</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#step-1--the-first-admin-account" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Step 1 — the first admin account</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#step-2--the-database" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Step 2 — the database</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#step-3--the-starter-template" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Step 3 — the starter template</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#the-wizards-api" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">The wizard’s API</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#choosing-a-database" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Choosing a database</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#configuring-the-server" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Configuring the server</span></a><ul class="isMobile astro-lptfoybu" style="--depth: 1;"><li style="--depth: 1;" class="astro-lptfoybu"><a href="#core" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Core</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#shared-hosting" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Shared hosting</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#features" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Features</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#media-storage" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Media storage</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#limits" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Limits</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#licensing" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Licensing</span></a></li></ul></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#after-setup" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">After setup</span></a></li></ul></div></details></nav></mobile-starlight-toc><script type="module" src="/docs/_astro/MobileTableOfContents.astro_astro_type_script_index_0_lang.BcSo_yiZ.js"></script></div><div class="right-sidebar-panel sl-hidden lg:sl-block astro-epm5tkrc"><div class="sl-container astro-epm5tkrc"><starlight-toc data-min-h="2" data-max-h="3"><nav aria-labelledby="starlight__on-this-page"><h2 id="starlight__on-this-page">On this page</h2><ul class="astro-lptfoybu" style="--depth: 0;"><li style="--depth: 0;" class="astro-lptfoybu"><a href="#_top" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Overview</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#what-the-wizard-does" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What the wizard does</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#whats-in-the-scaffolded-folder" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">What’s in the scaffolded folder</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#step-1--the-first-admin-account" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Step 1 — the first admin account</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#step-2--the-database" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Step 2 — the database</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#step-3--the-starter-template" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Step 3 — the starter template</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#the-wizards-api" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">The wizard’s API</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#choosing-a-database" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Choosing a database</span></a></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#configuring-the-server" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">Configuring the server</span></a><ul class="astro-lptfoybu" style="--depth: 1;"><li style="--depth: 1;" class="astro-lptfoybu"><a href="#core" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Core</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#shared-hosting" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Shared hosting</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#features" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Features</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#media-storage" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Media storage</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#limits" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Limits</span></a></li><li style="--depth: 1;" class="astro-lptfoybu"><a href="#licensing" style="--depth: 1;" class="astro-lptfoybu"><span style="--depth: 1;" class="astro-lptfoybu">Licensing</span></a></li></ul></li><li style="--depth: 0;" class="astro-lptfoybu"><a href="#after-setup" style="--depth: 0;" class="astro-lptfoybu"><span style="--depth: 0;" class="astro-lptfoybu">After setup</span></a></li></ul></nav></starlight-toc><script type="module" src="/docs/_astro/TableOfContents.astro_astro_type_script_index_0_lang.Csloo1VZ.js"></script></div></div></div></aside><div class="main-pane astro-perbdhd2"><main data-pagefind-body class="astro-ewhz5xxr" lang="en" dir="ltr"><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><h1 id="_top" class="astro-oef2efwn">Installation</h1></div></div><div class="content-panel astro-ahkzlqop"><div class="sl-container astro-ahkzlqop"><div class="sl-markdown-content"><p>Velora ships as a self-hosted application: <code dir="auto">apps/server</code> (the API) and
74
74
  <code dir="auto">apps/admin-ui</code> (the back office) run against a database you own. There
75
75
  is no hosted signup step — the first time the server boots against an
76
76
  unconfigured database, it serves the setup wizard instead of the normal
@@ -115,12 +115,26 @@ this off with <code dir="auto">VELORA_ADMIN=off</code> (see <code dir="auto">VEL
115
115
  affecting the API; and if the admin bundle is missing entirely, <code dir="auto">/velora/*</code>
116
116
  degrades to a clear 404 naming the cause rather than breaking anything
117
117
  else.</p>
118
- <aside aria-label="Note" class="starlight-aside starlight-aside--note"><p class="starlight-aside__title" aria-hidden="true"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" class="starlight-aside__icon"><path d="M12 11a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0v-4a1 1 0 0 0-1-1Zm.38-3.92a1 1 0 0 0-.76 0 1 1 0 0 0-.33.21 1.15 1.15 0 0 0-.21.33 1 1 0 0 0 .21 1.09c.097.088.209.16.33.21A1 1 0 0 0 13 8a1.05 1.05 0 0 0-.29-.71 1 1 0 0 0-.33-.21ZM12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm0 18a8 8 0 1 1 0-16.001A8 8 0 0 1 12 20Z"></path></svg>Note</p><div class="starlight-aside__content"><p>A packaged install <code dir="auto">npm create velora</code>, scaffolding a full site from
119
- npm is coming. Today, running your own instance means working from
120
- source (available to contributors on request); see
121
- <a href="https://docs.velora-cms.com/v1/contributing/local-setup/">Local setup</a>. To try Velora right now,
122
- start with the <a href="/docs/v1/using-velora/build-a-headless-frontend/">headless tutorial</a>
123
- or <a href="https://docs.velora-cms.com/v1/building-plugins/build-a-plugin/">build a plugin</a>.</p></div></aside>
118
+ <aside aria-label="Tip" class="starlight-aside starlight-aside--tip"><p class="starlight-aside__title" aria-hidden="true"><svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" class="starlight-aside__icon"><path fill-rule="evenodd" d="M1.44 8.855v-.001l3.527-3.516c.34-.344.802-.541 1.285-.548h6.649l.947-.947c3.07-3.07 6.207-3.072 7.62-2.868a1.821 1.821 0 0 1 1.557 1.557c.204 1.413.203 4.55-2.868 7.62l-.946.946v6.649a1.845 1.845 0 0 1-.549 1.286l-3.516 3.528a1.844 1.844 0 0 1-3.11-.944l-.858-4.275-4.52-4.52-2.31-.463-1.964-.394A1.847 1.847 0 0 1 .98 10.693a1.843 1.843 0 0 1 .46-1.838Zm5.379 2.017-3.873-.776L6.32 6.733h4.638l-4.14 4.14Zm8.403-5.655c2.459-2.46 4.856-2.463 5.89-2.33.134 1.035.13 3.432-2.329 5.891l-6.71 6.71-3.561-3.56 6.71-6.711Zm-1.318 15.837-.776-3.873 4.14-4.14v4.639l-3.364 3.374Z" clip-rule="evenodd"></path><path d="M9.318 18.345a.972.972 0 0 0-1.86-.561c-.482 1.435-1.687 2.204-2.934 2.619a8.22 8.22 0 0 1-1.23.302c.062-.365.157-.79.303-1.229.415-1.247 1.184-2.452 2.62-2.935a.971.971 0 1 0-.62-1.842c-.12.04-.236.084-.35.13-2.02.828-3.012 2.588-3.493 4.033a10.383 10.383 0 0 0-.51 2.845l-.001.016v.063c0 .536.434.972.97.972H2.24a7.21 7.21 0 0 0 .878-.065c.527-.063 1.248-.19 2.02-.447 1.445-.48 3.205-1.472 4.033-3.494a5.828 5.828 0 0 0 .147-.407Z"></path></svg>Tip</p><div class="starlight-aside__content"><p>The fastest install is one command:</p><div class="expressive-code"><link rel="stylesheet" href="/docs/_astro/ec.w36nc.css"><script type="module" src="/docs/_astro/ec.0vx5m.js"></script><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#82AAFF;--1:#3B61B0">npm</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">create</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">velora@latest</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">my-site</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="npm create velora@latest my-site"><div></div></button></div></figure></div><p>It scaffolds a project folder, installs the server from npm, and either
119
+ hands you this wizard in the browser (Quick) or completes the whole
120
+ setup from the terminal (Custom). <code dir="auto">cd my-site &#x26;&#x26; npm run dev</code>, open
121
+ <code dir="auto">http://localhost:3000/velora</code>, and continue below. Contributors who
122
+ want to run from source instead: see
123
+ <a href="https://docs.velora-cms.com/v1/contributing/local-setup/">Local setup</a>.</p></div></aside>
124
+ <div class="sl-heading-wrapper level-h2"><h2 id="whats-in-the-scaffolded-folder">What’s in the scaffolded folder</h2><a class="sl-anchor-link" href="#whats-in-the-scaffolded-folder"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “What’s in the scaffolded folder”</span></a></div>
125
+ <p><code dir="auto">npm create velora@latest</code> writes a normal Node project you can read and
126
+ extend — the Velora engine itself lives in <code dir="auto">node_modules</code> and updates with
127
+ <code dir="auto">npm update</code>; everything else is a visible file that’s yours:</p>
128
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="plaintext"><code><div class="ec-line"><div class="code"><span style="--0:#d6deeb;--1:#403f53">my-site/</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">server.js your entry point — imports the Velora engine from</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">node_modules and starts it; add your own routes,</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">middleware, or redirects here</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">public/</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">velora/</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">admin/ the admin UI, copied from the installed server</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">package (re-copy with `npm run sync-admin`)</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">media/ uploaded files (in .gitignore, like velora.db)</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">templates/ reserved for a future release — templates created</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">in the admin will land here as editable files</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">velora.db content + settings — SQLite only, appears after</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">setup. With PostgreSQL/MySQL the data lives in the</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">database server instead, and only DATABASE_URL in</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">.env points at it.</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">.env JWT_SECRET, VELORA_ADMIN_DIR, and (PostgreSQL/MySQL)</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">DATABASE_URL</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">.gitignore</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">package.json</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">README.md</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#d6deeb;--1:#403f53"> </span></span><span style="--0:#d6deeb;--1:#403f53">docker-compose.yml Custom mode with PostgreSQL or MySQL only</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="my-site/ server.js your entry point — imports the Velora engine from node_modules and starts it; add your own routes, middleware, or redirects here public/ velora/ admin/ the admin UI, copied from the installed server package (re-copy with &#x60;npm run sync-admin&#x60;) media/ uploaded files (in .gitignore, like velora.db) templates/ reserved for a future release — templates created in the admin will land here as editable files velora.db content + settings — SQLite only, appears after setup. With PostgreSQL/MySQL the data lives in the database server instead, and only DATABASE_URL in .env points at it. .env JWT_SECRET, VELORA_ADMIN_DIR, and (PostgreSQL/MySQL) DATABASE_URL .gitignore package.json README.md docker-compose.yml Custom mode with PostgreSQL or MySQL only"><div></div></button></div></figure></div>
129
+ <p><code dir="auto">server.js</code> is yours to extend — nothing between <code dir="auto">createApp()</code> and
130
+ <code dir="auto">listen()</code> is ever overwritten by an update. Add a route, for example:</p>
131
+ <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="js"><code><div class="ec-line"><div class="code"><span style="--0:#7FDBCA;--1:#096E72">app</span><span style="--0:#C792EA;--1:#8844AE">.</span><span style="--0:#82AAFF;--1:#3B61B0">get</span><span style="--0:#D6DEEB;--1:#403F53">(</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">/hello</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">, </span><span style="--0:#C792EA;--1:#8844AE">async</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#D9F5DD;--1:#111111">()</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#C792EA;--1:#8844AE">=></span><span style="--0:#D6DEEB;--1:#403F53"> ({ hello: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#ECC48D;--1:#984E4D">world</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53"> }));</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="app.get(&#x22;/hello&#x22;, async () => ({ hello: &#x22;world&#x22; }));"><div></div></button></div></figure></div>
132
+ <p><code dir="auto">public/velora/admin/</code> is a copy of the admin UI, not a live link into
133
+ <code dir="auto">node_modules</code>, so keep it in sync after an update:</p>
134
+ <div class="expressive-code"><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#82AAFF;--1:#3B61B0">npm</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">update</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">@velora-cms/server</span><span style="--0:#D6DEEB;--1:#403F53"> &#x26;&#x26; </span><span style="--0:#82AAFF;--1:#3B61B0">npm</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">run</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">sync-admin</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="npm update @velora-cms/server &#x26;&#x26; npm run sync-admin"><div></div></button></div></figure></div>
135
+ <p>Skip that and the server still boots and serves the (stale) copy — it
136
+ just logs one warning at startup naming the version mismatch, rather than
137
+ overwriting files you might have touched.</p>
124
138
  <div class="sl-heading-wrapper level-h2"><h2 id="step-1--the-first-admin-account">Step 1 — the first admin account</h2><a class="sl-anchor-link" href="#step-1--the-first-admin-account"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Step 1 — the first admin account”</span></a></div>
125
139
  <p>The wizard opens on “Set up Velora”. All four fields are validated in the
126
140
  browser before it will advance, and the first three are re-validated on
@@ -260,7 +274,7 @@ admin can install the template later from the marketplace instead.</p>
260
274
  <p>The wizard is an ordinary client of two public routes; you can drive them
261
275
  directly, which is what an automated provisioning script would do. Ask a
262
276
  fresh instance what it still needs:</p>
263
- <div class="expressive-code"><link rel="stylesheet" href="/docs/_astro/ec.w36nc.css"><script type="module" src="/docs/_astro/ec.0vx5m.js"></script><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#82AAFF;--1:#3B61B0">curl</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">-s</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">http://localhost:3000/api/setup/status</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="curl -s http://localhost:3000/api/setup/status"><div></div></button></div></figure></div>
277
+ <div class="expressive-code"><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#82AAFF;--1:#3B61B0">curl</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#82AAFF;--1:#3B61B0">-s</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">http://localhost:3000/api/setup/status</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="curl -s http://localhost:3000/api/setup/status"><div></div></button></div></figure></div>
264
278
  <p>On an unconfigured instance it reports that setup is required, that no
265
279
  database is configured, and which templates it can offer:</p>
266
280
  <div class="expressive-code"><figure class="frame not-content"><figcaption class="header"></figcaption><pre data-language="json"><code><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"setupRequired"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#FF6A83;--1:#A24848">true</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"bundledTemplates"</span><span style="--0:#D6DEEB;--1:#403F53">: [</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">{</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"pluginId"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">com.velora.blog</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"version"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">0.1.0</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"name"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">Blog</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"description"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#C789D6;--1:#7C5686">The official Velora blog template — posts, authors and tags, ready to publish.</span><span style="--0:#D9F5DD;--1:#111111">"</span><span style="--0:#D6DEEB;--1:#403F53">,</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"sampleContent"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#FF6A83;--1:#A24848">true</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div><div class="ec-line"><div class="code"><span class="indent"><span style="--0:#D6DEEB;--1:#403F53"> </span></span><span style="--0:#D6DEEB;--1:#403F53">],</span></div></div><div class="ec-line"><div class="code"><span class="indent"> </span><span style="--0:#7FDBCA;--1:#096E72">"databaseConfigured"</span><span style="--0:#D6DEEB;--1:#403F53">: </span><span style="--0:#FF6A83;--1:#A24848">false</span></div></div><div class="ec-line"><div class="code"><span style="--0:#D6DEEB;--1:#403F53">}</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="{ &#x22;setupRequired&#x22;: true, &#x22;bundledTemplates&#x22;: [ { &#x22;pluginId&#x22;: &#x22;com.velora.blog&#x22;, &#x22;version&#x22;: &#x22;0.1.0&#x22;, &#x22;name&#x22;: &#x22;Blog&#x22;, &#x22;description&#x22;: &#x22;The official Velora blog template — posts, authors and tags, ready to publish.&#x22;, &#x22;sampleContent&#x22;: true } ], &#x22;databaseConfigured&#x22;: false}"><div></div></button></div></figure></div>
@@ -590,6 +604,12 @@ sandbox and test setup and aren’t listed here.</p>
590
604
 
591
605
 
592
606
  <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">DATABASE_URL</code></td><td>string (URL)</td><td>Connection string for the configured database. No default: unset means the server boots into the full setup wizard, and the wizard writes this variable itself when it finishes. A relative SQLite path is made absolute once, at boot.</td><td><code dir="auto">postgresql://velora:velora@localhost:5432/velora</code></td></tr><tr><td><code dir="auto">JWT_SECRET</code></td><td>string</td><td>Signs and verifies the session tokens issued by <code dir="auto">/api/auth/*</code>. No default and no fallback — the server refuses to start with the error <code dir="auto">JWT_SECRET must be set — see .env.example</code>. Generate one with <code dir="auto">openssl rand -hex 32</code>.</td><td><code dir="auto">9f2c…</code> (64 hex chars)</td></tr><tr><td><code dir="auto">PORT</code></td><td>integer</td><td>TCP port the server listens on. Default <code dir="auto">3000</code>.</td><td><code dir="auto">8080</code></td></tr><tr><td><code dir="auto">HOST</code></td><td>string</td><td>Interface to bind. Default <code dir="auto">127.0.0.1</code> — loopback only. Set <code dir="auto">0.0.0.0</code> to accept connections from outside the host (behind a reverse proxy).</td><td><code dir="auto">0.0.0.0</code></td></tr><tr><td><code dir="auto">VELORA_ENV_FILE</code></td><td>string (path)</td><td>Which env file the server reads at boot and the setup wizard writes to. Default: <code dir="auto">.env</code> resolved against the working directory. A relative override is also resolved against the working directory.</td><td><code dir="auto">/srv/velora/config/velora.env</code></td></tr></tbody></table>
607
+ <div class="sl-heading-wrapper level-h3"><h3 id="shared-hosting">Shared hosting</h3><a class="sl-anchor-link" href="#shared-hosting"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Shared hosting”</span></a></div>
608
+ <p>Because <code dir="auto">server.js</code> is a plain Node entry point, a scaffolded project runs
609
+ on hosts that expect one — cPanel, Passenger, and similar shared hosts —
610
+ with no separate build step:</p>
611
+ <div class="expressive-code"><figure class="frame is-terminal not-content"><figcaption class="header"><span class="title"></span><span class="sr-only">Terminal window</span></figcaption><pre data-language="sh"><code><div class="ec-line"><div class="code"><span style="--0:#82AAFF;--1:#3B61B0">node</span><span style="--0:#D6DEEB;--1:#403F53"> </span><span style="--0:#ECC48D;--1:#3B61B0">server.js</span></div></div></code></pre><div class="copy"><div aria-live="polite"></div><button title="Copy to clipboard" data-copied="Copied!" data-code="node server.js"><div></div></button></div></figure></div>
612
+ <p><code dir="auto">PORT</code> and <code dir="auto">HOST</code> above control where it listens.</p>
593
613
  <div class="sl-heading-wrapper level-h3"><h3 id="features">Features</h3><a class="sl-anchor-link" href="#features"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Features”</span></a></div>
594
614
 
595
615
 
@@ -686,12 +706,26 @@ sandbox and test setup and aren’t listed here.</p>
686
706
 
687
707
 
688
708
 
689
- <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">VELORA_HEADLESS</code></td><td><code dir="auto">"true"</code> | <code dir="auto">"1"</code></td><td>Registers the key-gated public <code dir="auto">/api/v1/*</code> namespace at boot. Default unset = off, and anything other than those two values is off. Deploy-time only; there is deliberately no runtime toggle, so changing it needs a restart.</td><td><code dir="auto">true</code></td></tr><tr><td><code dir="auto">VELORA_MARKETPLACE_URL</code></td><td>string (origin) | <code dir="auto">off</code></td><td>Which marketplace the admin browses. Default <code dir="auto">https://marketplace.velora-cms.com</code>. <code dir="auto">off</code> (any case) or a blank value removes the in-CMS marketplace routes entirely, for air-gapped installs. Any other value is treated as a custom marketplace origin; trailing slashes are stripped.</td><td><code dir="auto">https://marketplace.velora-cms.com</code></td></tr><tr><td><code dir="auto">VELORA_PUBLIC_DOCS</code></td><td><code dir="auto">"false"</code> | <code dir="auto">"0"</code></td><td>Serves the public OpenAPI UI at <code dir="auto">/api/v1/docs</code>. <strong>On by default</strong> — only an explicit <code dir="auto">false</code>/<code dir="auto">0</code> disables it, which 404s that URL without touching the <code dir="auto">/api/v1</code> API itself.</td><td><code dir="auto">false</code></td></tr><tr><td><code dir="auto">VELORA_ADMIN</code></td><td><code dir="auto">"off"</code></td><td>Serves the admin SPA at <code dir="auto">/velora</code>. <strong>On by default</strong> — only the literal <code dir="auto">off</code> (trimmed, case-insensitive) disables it; every other value, including unset, leaves it on. Turning it off never touches the API, only <code dir="auto">/velora/*</code> and the <code dir="auto">/</code> redirect below.</td><td><code dir="auto">off</code></td></tr><tr><td><code dir="auto">VELORA_ADMIN_DIR</code></td><td>string (path)</td><td>Override for the bundled admin UI served at <code dir="auto">/velora/</code>. No default; without it the server tries <code dir="auto">&#x3C;server dist>/admin-bundle</code>, then <code dir="auto">&#x3C;repo>/apps/admin-ui/dist-bundle</code>. The directory must exist and contain an <code dir="auto">index.html</code>, or <code dir="auto">/velora/*</code> answers a 404 naming the override as the cause — a missing bundle never fails boot.</td><td><code dir="auto">/srv/velora/admin-bundle</code></td></tr><tr><td><code dir="auto">VELORA_DOCS_DIR</code></td><td>string (path)</td><td>Override for the bundled documentation served at <code dir="auto">/docs/</code>. No default; without it the server tries <code dir="auto">&#x3C;server dist>/docs-bundle</code>, then <code dir="auto">&#x3C;repo>/apps/docs/dist-bundle</code>. The directory must exist and contain a <code dir="auto">v1</code> subdirectory, or <code dir="auto">/docs/*</code> answers a 404 that names the cause — a missing bundle never fails boot.</td><td><code dir="auto">/srv/velora/docs-bundle</code></td></tr><tr><td><code dir="auto">VELORA_PLUGINS_DIR</code></td><td>string (path)</td><td>Where installed plugin packages are stored on disk. Default <code dir="auto">.velora/plugins</code> under the working directory.</td><td><code dir="auto">/var/lib/velora/plugins</code></td></tr><tr><td><code dir="auto">VELORA_TRUSTED_SIGNING_KEYS</code></td><td>JSON string</td><td>Extra plugin signing keys this instance trusts, merged over the set shipped with the server so a key can be added or a shipped one overridden without waiting for a release. A marketplace install blocks unless the package is signed by one of these keys; a sideload never blocks either way. Invalid JSON is warned about and ignored rather than stopping the server. Tabled in full, with the key format, under <a href="/docs/v1/using-velora/installing-plugins/">Installing plugins</a>.</td><td><code dir="auto">{"keys":[{"keyId":"a1b2…","publicKey":"MCowBQ…"}]}</code></td></tr><tr><td><code dir="auto">VELORA_BUNDLED_TEMPLATES_DIR</code></td><td>string (path)</td><td>Override for the starter templates the wizard offers. No default; without it the server tries <code dir="auto">&#x3C;server dist>/bundled-templates</code>, then <code dir="auto">apps/server/bundled-templates</code>. A missing or malformed tree degrades to an empty list rather than failing.</td><td><code dir="auto">/srv/velora/bundled-templates</code></td></tr><tr><td><code dir="auto">VELORA_SITE_DOMAIN</code></td><td>string (host)</td><td>This instance’s own domain, as the operator declares it — never taken from a request’s <code dir="auto">Host</code> header. A paid plugin’s license binds to this value once, so it must be stable. A scheme and trailing slash are tolerated and stripped; the value is lowercased. No default.</td><td><code dir="auto">example.com</code></td></tr><tr><td><code dir="auto">VELORA_PREVIEW_BASE_URL</code></td><td>string (origin)</td><td>Base URL preview links are built against, as <code dir="auto">&#x3C;base>/api/preview?token=…&#x26;id=…</code>. No default: unset (or blank) means no preview URL can be built. Trailing slashes are stripped.</td><td><code dir="auto">https://www.example.com</code></td></tr><tr><td><code dir="auto">VELORA_PREVIEW_TOKEN_TTL</code></td><td>integer (seconds)</td><td>How long a minted preview token stays valid. Default <code dir="auto">1800</code> (30 minutes).</td><td><code dir="auto">600</code></td></tr><tr><td><code dir="auto">VELORA_CORS_ORIGINS</code></td><td>comma-separated origins</td><td>Which browser origins may call public plugin routes cross-origin (<code dir="auto">GET</code>, <code dir="auto">POST</code>, <code dir="auto">OPTIONS</code> only). Default empty = no CORS headers at all, same-origin only. Entries that are not absolute origins — a trailing slash, a bare host, a path — are dropped with a warning.</td><td><code dir="auto">https://example.com,https://www.example.com</code></td></tr><tr><td><code dir="auto">VELORA_TRUST_PROXY</code></td><td><code dir="auto">true</code> | <code dir="auto">false</code> | integer | string</td><td>Whether, and how far, to trust <code dir="auto">X-Forwarded-*</code> when resolving the caller’s IP. Default unset = trust nothing, so every visitor behind a proxy collapses onto the proxy’s address — which flattens the per-IP rate lane. Set it only when genuinely behind a proxy you control; setting it otherwise lets a caller forge any IP.</td><td><code dir="auto">1</code></td></tr></tbody></table>
709
+
710
+
711
+
712
+
713
+
714
+
715
+ <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">VELORA_HEADLESS</code></td><td><code dir="auto">"true"</code> | <code dir="auto">"1"</code></td><td>Registers the key-gated public <code dir="auto">/api/v1/*</code> namespace at boot. Default unset = off, and anything other than those two values is off. Deploy-time only; there is deliberately no runtime toggle, so changing it needs a restart.</td><td><code dir="auto">true</code></td></tr><tr><td><code dir="auto">VELORA_MARKETPLACE_URL</code></td><td>string (origin) | <code dir="auto">off</code></td><td>Which marketplace the admin browses. Default <code dir="auto">https://marketplace.velora-cms.com</code>. <code dir="auto">off</code> (any case) or a blank value removes the in-CMS marketplace routes entirely, for air-gapped installs. Any other value is treated as a custom marketplace origin; trailing slashes are stripped.</td><td><code dir="auto">https://marketplace.velora-cms.com</code></td></tr><tr><td><code dir="auto">VELORA_PUBLIC_DOCS</code></td><td><code dir="auto">"false"</code> | <code dir="auto">"0"</code></td><td>Serves the public OpenAPI UI at <code dir="auto">/api/v1/docs</code>. <strong>On by default</strong> — only an explicit <code dir="auto">false</code>/<code dir="auto">0</code> disables it, which 404s that URL without touching the <code dir="auto">/api/v1</code> API itself.</td><td><code dir="auto">false</code></td></tr><tr><td><code dir="auto">VELORA_ADMIN</code></td><td><code dir="auto">"off"</code></td><td>Serves the admin SPA at <code dir="auto">/velora</code>. <strong>On by default</strong> — only the literal <code dir="auto">off</code> (trimmed, case-insensitive) disables it; every other value, including unset, leaves it on. Turning it off never touches the API, only <code dir="auto">/velora/*</code> and the <code dir="auto">/</code> redirect below.</td><td><code dir="auto">off</code></td></tr><tr><td><code dir="auto">VELORA_ADMIN_DIR</code></td><td>string (path)</td><td>Override for the bundled admin UI served at <code dir="auto">/velora/</code>. A scaffolded project’s <code dir="auto">.env</code> sets this to <code dir="auto">public/velora/admin</code> by default, pointing at the copy <code dir="auto">npm create velora</code> writes there (see “What’s in the scaffolded folder” above); without it — or outside a scaffold — the server tries <code dir="auto">&#x3C;server dist>/admin-bundle</code>, then <code dir="auto">&#x3C;repo>/apps/admin-ui/dist-bundle</code>. The directory must exist and contain an <code dir="auto">index.html</code>, or <code dir="auto">/velora/*</code> answers a 404 naming the override as the cause — a missing bundle never fails boot.</td><td><code dir="auto">/srv/velora/admin-bundle</code></td></tr><tr><td><code dir="auto">VELORA_DOCS_DIR</code></td><td>string (path)</td><td>Override for the bundled documentation served at <code dir="auto">/docs/</code>. No default; without it the server tries <code dir="auto">&#x3C;server dist>/docs-bundle</code>, then <code dir="auto">&#x3C;repo>/apps/docs/dist-bundle</code>. The directory must exist and contain a <code dir="auto">v1</code> subdirectory, or <code dir="auto">/docs/*</code> answers a 404 that names the cause — a missing bundle never fails boot.</td><td><code dir="auto">/srv/velora/docs-bundle</code></td></tr><tr><td><code dir="auto">VELORA_PLUGINS_DIR</code></td><td>string (path)</td><td>Where installed plugin packages are stored on disk. Default <code dir="auto">.velora/plugins</code> under the working directory.</td><td><code dir="auto">/var/lib/velora/plugins</code></td></tr><tr><td><code dir="auto">VELORA_TRUSTED_SIGNING_KEYS</code></td><td>JSON string</td><td>Extra plugin signing keys this instance trusts, merged over the set shipped with the server so a key can be added or a shipped one overridden without waiting for a release. A marketplace install blocks unless the package is signed by one of these keys; a sideload never blocks either way. Invalid JSON is warned about and ignored rather than stopping the server. Tabled in full, with the key format, under <a href="/docs/v1/using-velora/installing-plugins/">Installing plugins</a>.</td><td><code dir="auto">{"keys":[{"keyId":"a1b2…","publicKey":"MCowBQ…"}]}</code></td></tr><tr><td><code dir="auto">VELORA_BUNDLED_TEMPLATES_DIR</code></td><td>string (path)</td><td>Override for the starter templates the wizard offers. No default; without it the server tries <code dir="auto">&#x3C;server dist>/bundled-templates</code>, then <code dir="auto">apps/server/bundled-templates</code>. A missing or malformed tree degrades to an empty list rather than failing.</td><td><code dir="auto">/srv/velora/bundled-templates</code></td></tr><tr><td><code dir="auto">VELORA_SITE_DOMAIN</code></td><td>string (host)</td><td>This instance’s own domain, as the operator declares it — never taken from a request’s <code dir="auto">Host</code> header. A paid plugin’s license binds to this value once, so it must be stable. A scheme and trailing slash are tolerated and stripped; the value is lowercased. No default.</td><td><code dir="auto">example.com</code></td></tr><tr><td><code dir="auto">VELORA_PUBLIC_URL</code></td><td>string (origin)</td><td>Base URL used to make the <strong>public</strong> <code dir="auto">/api/v1/media</code> and GraphQL <code dir="auto">downloadUrl</code> fields absolute, e.g. <code dir="auto">https://cms.example.com/media/&#x3C;id>/&#x3C;filename></code> — a headless frontend fetches that URL from its own origin, so a path-relative value would resolve against the wrong host. Set this behind a reverse proxy/CDN where the request’s own <code dir="auto">Host</code>/protocol isn’t the public-facing one. Unset (or blank) means the base is derived per-request from the incoming <code dir="auto">Host</code> header and protocol instead (honors <code dir="auto">VELORA_TRUST_PROXY</code>) — fine for direct access. Doesn’t affect the backoffice <code dir="auto">/api/media</code> surface, which stays path-relative, same-origin with the admin SPA either way. Trailing slashes are stripped.</td><td><code dir="auto">https://cms.example.com</code></td></tr><tr><td><code dir="auto">VELORA_PREVIEW_BASE_URL</code></td><td>string (origin)</td><td>Base URL preview links are built against, as <code dir="auto">&#x3C;base>/api/preview?token=…&#x26;id=…</code>. No default: unset (or blank) means no preview URL can be built. Trailing slashes are stripped.</td><td><code dir="auto">https://www.example.com</code></td></tr><tr><td><code dir="auto">VELORA_PREVIEW_TOKEN_TTL</code></td><td>integer (seconds)</td><td>How long a minted preview token stays valid. Default <code dir="auto">1800</code> (30 minutes).</td><td><code dir="auto">600</code></td></tr><tr><td><code dir="auto">VELORA_CORS_ORIGINS</code></td><td>comma-separated origins</td><td>Which browser origins may call public plugin routes cross-origin (<code dir="auto">GET</code>, <code dir="auto">POST</code>, <code dir="auto">OPTIONS</code> only). Default empty = no CORS headers at all, same-origin only. Entries that are not absolute origins — a trailing slash, a bare host, a path — are dropped with a warning.</td><td><code dir="auto">https://example.com,https://www.example.com</code></td></tr><tr><td><code dir="auto">VELORA_TRUST_PROXY</code></td><td><code dir="auto">true</code> | <code dir="auto">false</code> | integer | string</td><td>Whether, and how far, to trust <code dir="auto">X-Forwarded-*</code> when resolving the caller’s IP. Default unset = trust nothing, so every visitor behind a proxy collapses onto the proxy’s address — which flattens the per-IP rate lane. Set it only when genuinely behind a proxy you control; setting it otherwise lets a caller forge any IP.</td><td><code dir="auto">1</code></td></tr></tbody></table>
690
716
  <div class="sl-heading-wrapper level-h3"><h3 id="media-storage">Media storage</h3><a class="sl-anchor-link" href="#media-storage"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Media storage”</span></a></div>
691
- <p>Media uploads go to an S3-compatible bucket. All five variables have
692
- in-code defaults matching the repository’s local MinIO service, so they
693
- only need setting when pointing at real S3 or R2. Individual uploads are
694
- capped at 50 MiB.</p>
717
+ <p>Media uploads go through one of two storage drivers see
718
+ <a href="/docs/v1/using-velora/media/#where-the-bytes-live">Media</a> for the full
719
+ picture. Individual uploads are capped at 50 MiB.</p>
720
+
721
+
722
+
723
+
724
+
725
+
726
+
727
+
728
+
695
729
 
696
730
 
697
731
 
@@ -733,7 +767,19 @@ capped at 50 MiB.</p>
733
767
 
734
768
 
735
769
 
736
- <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">S3_ENDPOINT</code></td><td>string (URL)</td><td>Storage endpoint. Default <code dir="auto">http://localhost:9000</code>.</td><td><code dir="auto">https://s3.eu-central-1.amazonaws.com</code></td></tr><tr><td><code dir="auto">S3_REGION</code></td><td>string</td><td>Bucket region. Default <code dir="auto">us-east-1</code>.</td><td><code dir="auto">eu-central-1</code></td></tr><tr><td><code dir="auto">S3_ACCESS_KEY_ID</code></td><td>string</td><td>Access key id. Default <code dir="auto">velora</code>.</td><td><code dir="auto">AKIA…</code></td></tr><tr><td><code dir="auto">S3_SECRET_ACCESS_KEY</code></td><td>string</td><td>Secret access key. Default <code dir="auto">velora-secret</code>.</td><td><code dir="auto">wJalr…</code></td></tr><tr><td><code dir="auto">S3_BUCKET</code></td><td>string</td><td>Bucket name. Default <code dir="auto">velora-media</code>.</td><td><code dir="auto">velora-media</code></td></tr></tbody></table>
770
+
771
+
772
+
773
+ <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">MEDIA_STORAGE</code></td><td><code dir="auto">local</code> | <code dir="auto">s3</code></td><td>Explicit driver choice; wins over the auto-detection below. Unset (or blank) falls through to that detection instead.</td><td><code dir="auto">local</code></td></tr><tr><td><code dir="auto">MEDIA_DIR</code></td><td>string (path)</td><td>Where the local driver writes files, resolved against the working directory like <code dir="auto">.env</code>. Default <code dir="auto">./media</code>, next to <code dir="auto">velora.db</code>. Only read when the local driver is active.</td><td><code dir="auto">./media</code></td></tr><tr><td><code dir="auto">S3_ENDPOINT</code></td><td>string (URL)</td><td>Storage endpoint. Default <code dir="auto">http://localhost:9000</code>, matching the repository’s local MinIO service.</td><td><code dir="auto">https://s3.eu-central-1.amazonaws.com</code></td></tr><tr><td><code dir="auto">S3_REGION</code></td><td>string</td><td>Bucket region. Default <code dir="auto">us-east-1</code>.</td><td><code dir="auto">eu-central-1</code></td></tr><tr><td><code dir="auto">S3_ACCESS_KEY_ID</code></td><td>string</td><td>Access key id. Default <code dir="auto">velora</code>.</td><td><code dir="auto">AKIA…</code></td></tr><tr><td><code dir="auto">S3_SECRET_ACCESS_KEY</code></td><td>string</td><td>Secret access key. Default <code dir="auto">velora-secret</code>.</td><td><code dir="auto">wJalr…</code></td></tr><tr><td><code dir="auto">S3_BUCKET</code></td><td>string</td><td>Bucket name. Default <code dir="auto">velora-media</code>.</td><td><code dir="auto">velora-media</code></td></tr></tbody></table>
774
+ <p>Driver selection, in order: <code dir="auto">MEDIA_STORAGE</code> wins outright when set to
775
+ <code dir="auto">local</code> or <code dir="auto">s3</code> (any other value fails to boot). Otherwise, any <code dir="auto">S3_*</code>
776
+ variable above being set to a non-empty value selects <code dir="auto">s3</code> automatically —
777
+ this is why the five <code dir="auto">S3_*</code> defaults only matter once something has
778
+ already opted into the <code dir="auto">s3</code> driver. With nothing set at all — the case for
779
+ a fresh <code dir="auto">npm create velora</code> scaffold — the instance gets the <code dir="auto">local</code>
780
+ driver with no further configuration, and uploads land under <code dir="auto">MEDIA_DIR</code>.
781
+ An empty string (<code dir="auto">MEDIA_STORAGE=</code> or <code dir="auto">S3_ENDPOINT=</code> left over in <code dir="auto">.env</code>)
782
+ counts as unset, the same as omitting the line.</p>
737
783
  <div class="sl-heading-wrapper level-h3"><h3 id="limits">Limits</h3><a class="sl-anchor-link" href="#limits"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Limits”</span></a></div>
738
784
  <p>Rate limiting runs in three lanes, keyed by what the request actually
739
785
  presents: a <code dir="auto">vk_</code> token that resolves to a live API key (<code dir="auto">key:</code>), a
@@ -843,7 +889,19 @@ Windows accept any duration string the limiter can parse (“1 minute”,
843
889
 
844
890
 
845
891
 
846
- <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">VELORA_RATE_LIMIT_MAX</code></td><td>integer > 0</td><td>Requests per window for a caller presenting a valid API key (<code dir="auto">key:</code> lane). Default <code dir="auto">100</code>.</td><td><code dir="auto">500</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_IP_MAX</code></td><td>integer > 0</td><td>Requests per window for anonymous callers (<code dir="auto">ip:</code> lane), including public plugin routes. Also the ceiling for the setup scope. Default <code dir="auto">60</code>.</td><td><code dir="auto">120</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_WINDOW</code></td><td>duration string</td><td>The window for the shared three-lane limiter — the default for auth, docs, plugin routes, and the public API. Default <code dir="auto">1 minute</code>.</td><td><code dir="auto">1 minute</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_LOGIN</code></td><td>integer > 0</td><td><code dir="auto">POST /api/auth/login</code> attempts per window, keyed on IP alone. The tightest ceiling in the system. Default <code dir="auto">10</code>.</td><td><code dir="auto">10</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_LOGIN_WINDOW</code></td><td>duration string</td><td>Window for the login ceiling. Default <code dir="auto">15 minutes</code>.</td><td><code dir="auto">15 minutes</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_SETUP</code></td><td>integer > 0</td><td><code dir="auto">POST /api/setup/complete</code> attempts per window. Default <code dir="auto">5</code>.</td><td><code dir="auto">5</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_SETUP_WINDOW</code></td><td>duration string</td><td>Window for the setup ceiling. Default <code dir="auto">1 hour</code>.</td><td><code dir="auto">1 hour</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_DOCS</code></td><td>integer > 0</td><td>Requests per window for the bundled documentation served at <code dir="auto">/docs/</code>. Default <code dir="auto">120</code>.</td><td><code dir="auto">120</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_DOCS_WINDOW</code></td><td>duration string</td><td>Window for the bundled-documentation rate limit above. Default <code dir="auto">1 minute</code>.</td><td><code dir="auto">1 minute</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_ADMIN</code></td><td>integer > 0</td><td>Requests per window for the admin SPA served at <code dir="auto">/velora/</code>, keyed on IP. Covers the page shell, the SPA fallback, and every hashed asset file the bundle serves — they share this one plugin-level ceiling because the asset routes carry no per-route rate config of their own. Default <code dir="auto">120</code>.</td><td><code dir="auto">120</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_ADMIN_WINDOW</code></td><td>duration string</td><td>Window for the admin rate limit above. Default <code dir="auto">1 minute</code>.</td><td><code dir="auto">1 minute</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_KEY_CACHE_TTL_MS</code></td><td>integer > 0 (milliseconds)</td><td>How long the limiter caches whether a presented API key is live, so repeat calls with the same key avoid a database lookup. Default <code dir="auto">30000</code>.</td><td><code dir="auto">30000</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_KEY_CACHE_MAX_ENTRIES</code></td><td>integer > 0</td><td>Hard ceiling on that cache, so a flood of never-repeating fake tokens cannot grow it without bound. Default <code dir="auto">5000</code>.</td><td><code dir="auto">5000</code></td></tr><tr><td><code dir="auto">VELORA_GRAPHQL_MAX_DEPTH</code></td><td>integer > 0</td><td>Maximum nesting depth accepted on the public GraphQL endpoint. Default <code dir="auto">8</code>.</td><td><code dir="auto">8</code></td></tr><tr><td><code dir="auto">VELORA_GRAPHQL_MAX_COMPLEXITY</code></td><td>integer > 0</td><td>Maximum field-count complexity accepted on the same endpoint. Default <code dir="auto">500</code>.</td><td><code dir="auto">500</code></td></tr></tbody></table>
892
+
893
+
894
+
895
+
896
+
897
+
898
+
899
+
900
+
901
+
902
+
903
+
904
+ <table><thead><tr><th>Name</th><th>Type</th><th>Meaning</th><th>Example</th></tr></thead><tbody><tr><td><code dir="auto">VELORA_RATE_LIMIT_MAX</code></td><td>integer > 0</td><td>Requests per window for a caller presenting a valid API key (<code dir="auto">key:</code> lane). Default <code dir="auto">100</code>.</td><td><code dir="auto">500</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_IP_MAX</code></td><td>integer > 0</td><td>Requests per window for anonymous callers (<code dir="auto">ip:</code> lane), including public plugin routes. Also the ceiling for the setup scope. Default <code dir="auto">60</code>.</td><td><code dir="auto">120</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_WINDOW</code></td><td>duration string</td><td>The window for the shared three-lane limiter — the default for auth, docs, plugin routes, and the public API. Default <code dir="auto">1 minute</code>.</td><td><code dir="auto">1 minute</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_LOGIN</code></td><td>integer > 0</td><td><code dir="auto">POST /api/auth/login</code> attempts per window, keyed on IP alone. The tightest ceiling in the system. Default <code dir="auto">10</code>.</td><td><code dir="auto">10</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_LOGIN_WINDOW</code></td><td>duration string</td><td>Window for the login ceiling. Default <code dir="auto">15 minutes</code>.</td><td><code dir="auto">15 minutes</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_SETUP</code></td><td>integer > 0</td><td><code dir="auto">POST /api/setup/complete</code> attempts per window. Default <code dir="auto">5</code>.</td><td><code dir="auto">5</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_SETUP_WINDOW</code></td><td>duration string</td><td>Window for the setup ceiling. Default <code dir="auto">1 hour</code>.</td><td><code dir="auto">1 hour</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_DOCS</code></td><td>integer > 0</td><td>Requests per window for the bundled documentation served at <code dir="auto">/docs/</code>. Default <code dir="auto">120</code>.</td><td><code dir="auto">120</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_DOCS_WINDOW</code></td><td>duration string</td><td>Window for the bundled-documentation rate limit above. Default <code dir="auto">1 minute</code>.</td><td><code dir="auto">1 minute</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_MEDIA</code></td><td>integer > 0</td><td>Requests per window for the stable <code dir="auto">GET /media/:id/:filename</code> route (see <a href="/docs/v1/using-velora/media/">Media</a>), keyed on IP — it serves anonymous public-asset requests and headless consumers directly, so it can’t bucket on the three-lane limiter. Generous by design: one page load can fire many requests against it. Default <code dir="auto">300</code>.</td><td><code dir="auto">300</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_MEDIA_WINDOW</code></td><td>duration string</td><td>Window for the media rate limit above. Default <code dir="auto">1 minute</code>.</td><td><code dir="auto">1 minute</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_ADMIN</code></td><td>integer > 0</td><td>Requests per window for the admin SPA served at <code dir="auto">/velora/</code>, keyed on IP. Covers the page shell, the SPA fallback, and every hashed asset file the bundle serves — they share this one plugin-level ceiling because the asset routes carry no per-route rate config of their own. Default <code dir="auto">120</code>.</td><td><code dir="auto">120</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_ADMIN_WINDOW</code></td><td>duration string</td><td>Window for the admin rate limit above. Default <code dir="auto">1 minute</code>.</td><td><code dir="auto">1 minute</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_KEY_CACHE_TTL_MS</code></td><td>integer > 0 (milliseconds)</td><td>How long the limiter caches whether a presented API key is live, so repeat calls with the same key avoid a database lookup. Default <code dir="auto">30000</code>.</td><td><code dir="auto">30000</code></td></tr><tr><td><code dir="auto">VELORA_RATE_LIMIT_KEY_CACHE_MAX_ENTRIES</code></td><td>integer > 0</td><td>Hard ceiling on that cache, so a flood of never-repeating fake tokens cannot grow it without bound. Default <code dir="auto">5000</code>.</td><td><code dir="auto">5000</code></td></tr><tr><td><code dir="auto">VELORA_GRAPHQL_MAX_DEPTH</code></td><td>integer > 0</td><td>Maximum nesting depth accepted on the public GraphQL endpoint. Default <code dir="auto">8</code>.</td><td><code dir="auto">8</code></td></tr><tr><td><code dir="auto">VELORA_GRAPHQL_MAX_COMPLEXITY</code></td><td>integer > 0</td><td>Maximum field-count complexity accepted on the same endpoint. Default <code dir="auto">500</code>.</td><td><code dir="auto">500</code></td></tr></tbody></table>
847
905
  <div class="sl-heading-wrapper level-h3"><h3 id="licensing">Licensing</h3><a class="sl-anchor-link" href="#licensing"><span aria-hidden="true" class="sl-anchor-icon"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"></path></svg></span><span class="sr-only" data-pagefind-ignore="">Section titled “Licensing”</span></a></div>
848
906
  <p>Paid plugins re-validate against the marketplace’s license server on a
849
907
  schedule; both knobs are read fresh, so a change takes effect without a